Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 275 additions & 42 deletions .claude/skills/open-edit/SKILL.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ veed/.veed-voice-rates.json
# Harness local state
.claude/worktrees
.claude/settings.local.json
.weave/
.weave-subs/
.weave-runs/

Expand All @@ -45,3 +46,6 @@ plans/

# Python bytecode from the generators
__pycache__/

# Engine probe renders — regenerable output of the probe harness
internal/motion/probes/*/probe.mp4
44 changes: 41 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,37 @@ fast-path/refinement switch.
built on `pipeline/recipes/lib.ts` (the shared assembly rules); turns `meta.json` + `word-timings.json`
into the final .wv document deterministically. Authored + validated offline (derived from the sheet);
`hasRecipe` keys on the module existing.
- `pipeline/scripts/` — `sample-style.ts` (facet-scored seeded style pick) · `generate-recipe.ts` (runs the compiled recipe: generate → lint → `--verify` fix loop → `--record` → probe) · `lint-template.ts` (mechanical engine-limit gate) · `probe-qa.ts` (frame QA vs the source) · `resolve-video.ts` (the video-arg resolution rule shared by both entry points) · `synth-word-timings.ts` (imported by prep) · `extract-beat-frames.ts` (imported by prep) · `mux-audio.sh`, `preflight.sh`, `install-veed-engine.sh` (deterministic).
- `pipeline/scripts/` — `sample-style.ts` (facet-scored seeded style pick) · `generate-recipe.ts` (runs the compiled recipe: generate → lint → `--verify` fix loop → `--record` → probe) · `lint-template.ts` (mechanical engine-limit gate) · `design-gate.ts` (a run's documents read back against its own design system) · `probe-qa.ts` (frame QA vs the source) · `resolve-video.ts` (the video-arg resolution rule shared by both entry points) · `synth-word-timings.ts` (imported by prep) · `extract-beat-frames.ts` (imported by prep) · `gates.sh` (THE chain: design → lint → `--verify` → WCAG → `--record` → probe-qa → mux, one
command, `--doc` to gate one chapter of a longer piece) · `wcag-pass.ts` · `expect-windows.ts` ·
`scoped-edit.ts` · `brand.ts` · `creative-log.ts` · `mix-audio.ts` · `cut-frames.ts` (frames at every
shot boundary, where a uniform grid never looks) · `scene-frames.ts` (stills for a clip with no beats) ·
`concat-chapters.ts` (stream-copies the chapters of one film, refusing parts that disagree) ·
`concat-videos.ts` (re-encodes generated clips that DO disagree onto one canvas) ·
`mux-audio.sh`, `preflight.sh`, `install-veed-engine.sh` (deterministic). Run `gates.sh` rather
than retyping the gates it drives.
- `pipeline/design/` — the authored path's substrate. `system.ts` is the per-run design system
(`runs/<key>/design/system.json`): fonts, the type ladder with tracking per rung, palette, spacing,
named easings and durations, the reveal unit, the devices in play, and `donors` — the recipe ids it
was seeded from, checked against the runtime index. `captions.ts` builds a caption block from the
real per-word times: glyphs at each word's own delay, every page of a long cue rendered, a cursor
whose windows meet so one bar travels the line, and lines as BLOCK elements.
- `pipeline/recipes/type.ts` · `devices.ts` · `geometry.ts` — the craft, given away as arithmetic
rather than frozen into a look. `opticalTracking(px)` is the measured tracking curve; `devices.ts`
carries the compositional vocabulary read off the delivered corpus — the BEST instances, not the
average ones: a divider is a hairline under 50% alpha with a shadow and an opaque stub at its origin,
drawn with `scaleX`, where a plain bar is what a default draws; contrast over footage is a two-layer
ground shadow in the document's own colour, not a scrim box. `geometry.ts` is arcs,
lattices, springs and clip polygons.
- `pipeline/providers/` — the asset seam. `assets.ts` (`runs/<key>/assets/manifest.json`: records
APPEND, so a regenerated plate never erases the call that was already billed) · `fal.ts` (the user's
own key; `submitOnce` refuses to buy an identical request twice) · `queue-ledger.ts` (what was
accepted, so a lost poll resumes instead of re-submitting) · `stills.ts` (a real picture with its
licence and credit; a file whose terms are absent reads as `unknown`, never as permissive — it has a
CLI: `stills.ts search <query>` · `show <File:…>` · `save <File:…|url> --run <dir> --id <id>`).
- `pipeline/scripts/mix-audio.ts` — one soundtrack out of many pieces, for a run whose audio is not
simply the source clip's. Each track states where it starts and how loud it sits; a bed marked
`duck` is ducked by the `voice` tracks themselves rather than by a guessed gain. `mux-audio.sh`
still restores a single source track; this is what a film needs instead.
- `veed/` — VEED-native transcription + login (one writer of `transcript.json`; real per-word timings). `prep/prep.ts` — `meta.json` + `word-timings.json` + base frames (needs a transcript from any provider). `refs/` — `html/` refs + `tags.json` (v3, the RUNTIME INDEX — recipes only, facet taxonomy, `fit` = aspect SOT) + per-ref `recipe.md` (the prose recipe sheet a compiled recipe is derived from — the fast path runs the compiled module, never the sheet; the creative pass reads sheets as craft substrate and REMIX donors).
- `config.ts` — all machine paths (ffmpeg / ffprobe / veed-engine). `docs/` — FLOW (orchestration) · recipe-format (the recipe law). Engine support matrix = the `feature-support.md` asset downloaded with the engine release into `.veed-engine/` (not vendored here).

Expand All @@ -74,6 +104,14 @@ fast-path/refinement switch.
- **The runtime style pool is RECIPES-ONLY — architecturally.** `refs/tags.json` (v3) contains nothing
but recipe-backed refs (`sample-style` fail-louds on an entry missing its sheet or module); Selection is by
FACETS — never by image.
- **An authored run writes its design system down before it authors a document, and the gate enforces
it.** Not because anyone forgets the contract — a 12-minute film was delivered with 23 font sizes, one
easing curve used 934 times and display tracking on its body text while the skill and the brief were
both still in context — but because the design was authored early, from a blank page, as fill-in work
while other steps ran. `assertGrounded` refuses a system whose `groundedIn` files do not exist, which
is what makes the early version impossible rather than merely discouraged. Compose from
`pipeline/recipes/devices.ts` and `pipeline/design/captions.ts`; a value the system does not declare
is a finding, not a judgement call.
- On creative face-1 the **user's materials are the design authority** — they must reach the design pass
and be looked at; recipe sheets contribute mechanics only; DIRECTION never names fonts/palette/device,
and on any collision the user's materials win.
Expand Down Expand Up @@ -120,7 +158,7 @@ listed first because it transcribes best, not because it wins ties.
| --- | --- | --- |
| `veed` | Premium quality, hosted. One-time browser sign-in; limits are the VEED account's. | `veed/go.ts` |
| `whisperx` | Free, local, offline. Two tiers: `medium` (slower, better) and `small.en` (fastest, weaker on names). CPU-bound here — CTranslate2 has no GPU path on Apple Silicon. | `prep/transcribe.ts` |
| `custom` | The user's own service or MCP. **We provide no support code**: you obtain a Whisper-family JSON however their tool works, then map it. No credential ever passes through OpenEdit. | `prep/whisper.ts <json> <video>` |
| `custom` | The user's own service or MCP, and the route for GENERATED narration — the media argument may be an audio file, so a film's own voice track reaches `transcript.json` before any picture exists. **We provide no support code**: you obtain a Whisper-family JSON however their tool works, then map it. No credential ever passes through OpenEdit. | `prep/whisper.ts <json> <media>` |

The choice is recorded in `$OPEN_EDIT_ROOT/.open-edit-prefs.json` — the runtime root, which is not the
user's project root when the runtime is a managed clone — as `{ transcription: { provider, model? } }`, and is not
Expand All @@ -140,7 +178,7 @@ bash pipeline/scripts/install-whisperx.sh # uv/pipx, pinne
node --import tsx prep/transcribe.ts <video.mp4> [...] [--model medium] [--language de]

# custom: any Whisper-family JSON the user's service produced, one json per video
node --import tsx prep/whisper.ts transcription.json <video.mp4> [<json> <video.mp4> ...]
node --import tsx prep/whisper.ts transcription.json <media> [<json> <media> ...] # media = video OR audio
```

`prep/whisper-mapper.ts` accepts the Python Whisper family (WhisperX, openai-whisper,
Expand Down
18 changes: 15 additions & 3 deletions docs/FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ SCRIPT steps are deterministic; AGENT steps need an LLM. The default (recipe-bac
end — the only agent is the opt-in analysis pass; creative face-1 (the user brought their own
reference/brand/concept) and creative iteration on a delivered result are authored inline by the orchestrator.
The whole thing is encoded executably in `.claude/skills/open-edit/SKILL.md` (the golden master) — this doc
is the conceptual map. One video → ONE captioned MP4. No per-shot intermediates, and no user-approval gate on
the captioning itself — the only gates are the FOOTAGE step's three, and they exist only when the user brought no
footage.
is the conceptual map. No per-shot intermediates IN THE RENDER — a generated set produces one clip per shot and joins them
before any of this — and no user-approval gate on the captioning itself —
the only gates are the FOOTAGE step's three, and they exist only when the user brought no footage.

**Inputs: any number of videos, INCLUDING NONE.** The captioned single-clip run is the shortest path
through here, not the definition of a run: footage is a layer inside the document, and a run can be built
from stills, slides, generated imagery, audio alone or pure motion graphics. A long piece has one
document per chapter, gates them one at a time (`gates.sh --doc chapters/act-3`) and joins the
gated chapters with `concat-chapters.ts`.

An AUTHORED run — creative face-1, a remix, or a run with no footage — writes `runs/<key>/design/system.json`
before it authors any document, and `pipeline/scripts/design-gate.ts` reads every document back against it.
A compiled recipe needs none of that: the recipe IS the system.

| step | kind | what |
|------|------|------|
Expand All @@ -15,7 +25,9 @@ footage.
| prep | SCRIPT | transcript from the chosen provider (VEED `veed/go.ts`, local WhisperX `prep/transcribe.ts`, or the user's own via `prep/whisper.ts`; chunks = beats, real per-word timings) → `word-timings.json` (per-word absolute-ms reveal delays, synthesized by `prep/prep.ts`) + base frames; auto-detect aspect → write `meta.json` (canvas + duration + paths) |
| analyse | AGENT (vision) | **OPT-IN, skipped by default** — run only when the user asks to refine the style; then ONE nameless bg subagent reads the frames + transcript → `analysis.json` (per-beat shot/bboxes/neg-space/brightness in CANVAS px), and the DESIGN + RENDER step re-runs with it. The **only** vision pass. `analysis.json` existing IS the fast-path/refinement switch. |
| sample ONE style | SCRIPT | `pipeline/scripts/sample-style.ts` — facet-scored seeded draw of one aspect-matched ref from `refs/tags.json` (v3 RUNTIME INDEX, recipes only; `fit` = aspect SOT; transcript energy weights the draw) → `style.json` (facets, energy, coverage, alternates). Zero tokens. The CREATIVE PASS routes here too: face-1 (user brought materials) → the DESIGN + RENDER step, from-scratch; face-2 REMIX (any creative iteration) / RE-ROLL (variants asks) / patch (defects). |
| design system | INLINE (authored runs only) | `runs/<key>/design/system.json` — fonts, the type ladder with each rung's own optical tracking (`pipeline/recipes/type.ts`), palette, spacing, named easings and durations, the reveal unit, the devices in play, and `donors` (recipe ids, checked against the runtime index). Written FROM the run's content: `groundedIn` must name files that exist, so a system cannot be authored before the thing it is for. Compose the documents from `pipeline/recipes/devices.ts`, `pipeline/design/captions.ts` and `pipeline/recipes/geometry.ts` rather than typing each graphic at its point of use. |
| design + render | SCRIPT (recipe) · INLINE (creative face-1 · remix) | default → **compiled recipe** (`pipeline/scripts/generate-recipe.ts`: the generator module emits `final/template.wv` + `manifest.json` deterministically — word delays from `word-timings.json` by construction, zero tokens — then drives the gate chain: lint → `--verify` with the mechanical ladder fix loop (≤2 cycles) → `--record` → probe-qa); **face-1** (INLINE, orchestrator-authored: one design system from the USER'S materials, 1-2 recipe sheets as craft substrate, per `director-brief.md`; lint + `--verify` to exit 0, then `--record` → probe-qa); **REMIX** (inline donor blend per the brief's REMIX MODE in `runs/<key>-remix`, same gates) |
| gates | SCRIPT | `bash pipeline/scripts/gates.sh <run-dir> [--doc <subdir>] [--audio <file>]` — design → lint → `--verify` → WCAG → `--record` → probe-qa → mux, stopping at the first failure and naming the gate. `generate-recipe.ts` drives the same chain for a compiled recipe; every other path calls this instead of retyping it. Runs OUTSIDE any sandbox. |
| mux audio | SCRIPT | ffmpeg muxes the source audio onto the silent render → `final/out.mp4` (the deliverable) |
| preview | SCRIPT (parallel) | `preview/server.ts runs/<key>` — localhost preview opened for the user (read-only): watch and scrub the footage, follow the transcript, preview the subtitles; auto-swaps to the new `final/out.mp4` when an amend re-render lands. Launched in the background; the pipeline never waits on it |

Expand Down
29 changes: 16 additions & 13 deletions docs/recipe-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fix loop · DO NOT → simply how the code behaves.
4. **WORDS + TIMING** — the invariant rules: one span per word (or per glyph, if the ref is glyph-level),
each span's `animation-delay` = that word's `delayMs` from `word-timings.json` **VERBATIM**; the beat's
`.cue` gets `animation-delay:{cueDelayMs}ms; animation-duration:{cueDurMs}ms`. Word spacing per the
engine limits (spacer span or margin — inter-span whitespace is dropped; `display:inline-block`).
engine limits (spacer span or margin — inter-span whitespace is not what sets the gap here; `display:inline-block`).
5. **EMPHASIS** — the ref's own device for hero words + a deterministic pick rule (e.g. "the longest
content word of the beat; numbers win; last beat's last content word") so no judgment is needed.
6. **VERIFY LOOP** — verbatim commands:
Expand Down Expand Up @@ -95,19 +95,22 @@ offline and to the creative pass at run time.
only briefly). Prefer ONE page-level fade-out over per-word fade-outs — staggered per-word exits catch
two pages alive at once. Exit bands narrower than the last ~30% of the duration hold full opacity too
deep into the successor's rise.
- **Never lay out animated children in shrink-to-fit flex** (hit independently by a compiled recipe
and by a from-scratch design pass on the same content): the engine mis-lays-out shrink-to-fit flex lines
whose children animate — fragments, stray offsets, ink clipped at the box edge. Use full-width
blocks + `text-align:center`.
- **Animating spans rasterize at their line-height box** (hook-078 42-23): with `line-height:1`,
a span mid-reveal is sheared at the em-box bottom (hard horizontal cut) even for opacity-only
animations; steady state renders fine. Give word spans vertical `padding` headroom (~0.1em top /
0.15–0.2em bottom), or keep line-height ≥1.2 (refs that have it never shear).
- **Prefer full-width blocks to shrink-to-fit flex for animated children.** Not because the engine
mis-lays them out — it does not (probe: flex-shrink-to-fit-animated), though this was carried for
months as fragments, stray offsets and ink clipped at the box edge. A shrink-to-fit line sizes itself
from its content, so one word changing width moves everything beside it; a full-width block plus
`text-align:center` holds its geometry while the content changes.
- **Give word spans vertical headroom at tight line-heights.** An animating span at `line-height:1`
is NOT sheared at the em-box (probe: lineheight-shear) — that was asserted for months, and enforced
by a lint rule, on a claim nothing had rendered. What a tight line-height does do is bring a
descender within a pixel or two of the next line's caps, so ~0.1em top / 0.15-0.2em bottom padding,
or line-height >= 1.2, is a typographic margin rather than a repair.
- **Word gaps must budget the font's ink overhang** (hook-078 42-23): italic display caps (Playfair T/G/Y)
overhang their advance by up to ~0.5em — a nominal 0.3em margin or spacer renders as FUSED words.
Neither margins, paddings, nor spacer spans differ here (the ink paints over all of them); the engine
also trims plain trailing spaces inside spans. The validated pattern: `margin-right:0.45em` PLUS a
trailing `&#160;` in the span text. Validate gaps by patch-rendering the worst pairs, not from advance
Neither margins, paddings, nor spacer spans differ here (the ink paints over all of them). A plain
trailing space inside a span is NOT trimmed (probe: trailing-space-in-span) and does advance the next
word, but a space is narrower than a half-em overhang, so it does not solve this alone. The validated
pattern: `margin-right:0.45em` PLUS a trailing `&#160;` in the span text. Validate gaps by patch-rendering the worst pairs, not from advance
tables.
- **Light-on-light needs a grounding layer** (hook-224): a prefab's pure-white glow vanishes over light
footage (a white mug) — bake one dark `text-shadow` layer into the sheet; the prefab's demo footage
Expand All @@ -129,7 +132,7 @@ offline and to the creative pass at run time.
it done.
- Every run validates the RECIPE: if it fails, fix the sheet AND its module together (they must not drift).
- `--verify` is necessary, not sufficient: pile-ups, fused words, ghost stacking, light-on-light, and
mid-reveal shear are all invisible to it — a recipe is validated only after an offline frame-by-frame
descender clearance are all invisible to it — a recipe is validated only after an offline frame-by-frame
eyeball (mid-beat AND ~120ms-before-beat-end probes per beat). Compilation removes the arithmetic-slip
class (the module also gets unit tests against the sheet's worked example), but these VISUAL
classes still need the eyeball on ≥2 contents.
Loading