# OverlayMotion > Remotion overlay templates and an edit-spec system built for AI agents: > one JSON spec in, a branded rendered video out. Generated from the overlay-motion package docs. Human version: the /docs pages. --- # Quick start From clone to rendered video in under five minutes. One repo, no extra services. (The `overlay-motion` npm package is coming; today the repo is the distribution.) ## 1. Install and explore ```bash git clone && cd overlay-motion npm install npm run studio # Remotion Studio: every template as a composition ``` Studio is the local gallery: every template is a composition you can scrub, and the `custom` composition previews any spec you paste into its props. The browsable version, with a live player and the exact JSON spec beside each template, is at [overlaymotion.com/templates](https://overlaymotion.com/templates). Every template page has a copy-spec button. ## 2. Write a spec An edit is one JSON document. Start from any template's "copy spec" button, or from scratch: ```json { "version": 1, "format": "vertical", "fps": 30, "durationSec": 8, "source": { "type": "none" }, "overlays": [ { "template": "hero-title", "region": "center", "time": { "start": "0.5s", "appear": 1, "hold": 6 }, "props": { "title": "Ship it", "subtitle": "The overlay system for agents" } } ] } ``` For an agent editing real footage, start with the [Editing agent playbook](agent-playbook.md): analyze the source, preserve protected subjects, record assumptions and validate inexpensive checkpoints. The full rendering grammar is [Edit Spec v1](edit-spec.md). Run the agent on the strongest model you have access to, at its highest reasoning setting. Weaker or low-effort models miss protected subjects and mis-time overlays. Model names are left out on purpose: they turn over faster than this document does. ## 3. Preview it Open Remotion Studio (`npm run studio`): every template is a composition, and the `custom` composition previews any spec you paste into its `defaultProps`. ## 4. Render it Every template is a composition in `remotion/index.ts`; the `custom` composition renders whatever spec you pass: ```bash npx remotion render remotion/index.ts hero-title out/hero.mp4 npx remotion render remotion/index.ts custom out/edit.mp4 \ --props='{"spec": , "theme": }' ``` `--props` accepts a file path too: `--props=./my-edit.json`. Rendering uses Remotion, which is free for individuals and companies of up to 3 people; larger companies need a [Remotion Company License](https://www.remotion.pro/license). See the current [Remotion license](https://www.remotion.dev/license) for details. ## 5. Brand it Pass any `BrandTheme` as `theme`: colors, fonts, radius, and glass/solid surface. Same spec, another theme, rebranded video. Presets live in `src/theme/themes.tsx`; the site's theme picker shows them all. ## Help shape OverlayMotion OverlayMotion launched in August 2026, so this is still an early stage project. Help us develop it: join the [Discord](https://discord.gg/DgRTAQ3Ne) and tell us what you need. We can work on your project with you and turn it into more real world templates everyone gets to use. --- # Editing agent playbook The canonical behavior for an agent editing with OverlayMotion, written for the common case: the user hands over footage and a weak prompt such as "make this good," and for footage that carries spoken editing instructions. You produce two artifacts. An **edit decision plan** records evidence, assumptions, protected subjects, assets and QA checkpoints. An **Edit Spec v1** describes the render. The plan explains judgment; the spec stays a deterministic rendering input. Machine-readable defaults and the plan schema live in `src/agent/`; tool contracts are in [agent-toolkit.md](agent-toolkit.md). This page is what applies to every edit. Anything that applies to one kind of request lives in its own file, and you open that file when the request names it. ## Open only what the job asks for | The request involves | Read this first | | --- | --- | | captions, subtitles, word-timed text | [features/captions.md](features/captions.md) | | removing, replacing or blurring a background | [features/background-removal.md](features/background-removal.md) | | face or head tracking, a corner bubble, a follow crop | [features/tracking.md](features/tracking.md) | | noise, hum, room tone, "clean up the audio" | [features/voice-cleanup.md](features/voice-cleanup.md) | | a music bed | [features/music.md](features/music.md) | | sound cues on overlays | [features/sound.md](features/sound.md) | | a camera move | [camera-motion-spec.md](camera-motion-spec.md) | | color, HDR, or an unexplained shift in the delivered file | [features/delivery-color.md](features/delivery-color.md) | | authoring any spec at all | [edit-spec.md](edit-spec.md) | Nothing else is required reading. A caption job never opens the matting page. Each feature page carries the rules, the tools and the QA for that one job, so opening it late is worse than opening it first: several of those rules decide things before a render, and re-rendering is how they get paid for twice. ## Prime directive Improve comprehension without contradicting the speaker, hiding the important subject or fabricating facts. In priority order: 1. explicit user request; 2. observable transcript and visual evidence; 3. subject, message, rights and platform safety; 4. established brand and platform defaults; 5. decorative style. When an explicit request conflicts with subject safety, satisfy both through layout: split the screen and reframe the speaker into the open panel instead of placing secondary media over their face. ## What you decide, and what you ask Decide without interrupting the job: caption style, conservative motion, exact safe placement, crop focus, phrase grouping, and the shortest faithful copy. Record each one as an assumption. Verify or ask before asserting identity, changing meaning, making a factual claim, selecting unlicensed media, publishing a quote whose wording or attribution is uncertain, or making an ambiguous destructive cut. A low-risk draft may keep an unresolved item as a clearly labeled placeholder. When templates are the choice, show at most three options, each with title, slug, a one-sentence tradeoff and a preview path such as `/templates/speaker-card`, so a UI can render linked choice cards without being coupled to one interface. ## The intake gate Run this before writing the plan, once: ```bash python3 scripts/check-intake.py --source talk.mp4 \ --request "add subtitles and cut it for reels" \ --asset logo=brand.svg ``` It probes the file and reads the request against the assets actually supplied. Exit status is 2 when something blocks. The contract behind it is `INTAKE` in `src/agent/policy.ts`; the script exists so the checks stop depending on an agent remembering them at the end of a long session. | verdict | what it means | what you do | | --- | --- | --- | | `blocking` | the edit cannot be finished as asked | stop and say what is missing | | `ask` | two or three legitimate answers, materially different | one question, options, default already chosen | | `checkpoint` | a preview answers it faster than a question | show the cheap thing, keep going | | `default` | reversible or decorative | choose from source evidence, record it | Only three things block, each because the missing piece cannot be invented: captions asked of a source with no speech, brand treatment with no brand asset, and named b-roll with no media or license. Questions come from facts about the file, never from a fixed questionnaire. A horizontal source going vertical raises "whose framing wins in the crop"; the same request over a source that already fits raises nothing. A round that asks what the file already answers teaches the user to skip the round. Budget: one round, at most three questions, each with the default already selected so silence means proceed. Record every one in the plan's `clarifications` with `resolution` set to `answered`, `defaulted` or `deferred`. `validateEditDecisionPlan` rejects a plan carrying an unanswered blocking question, an answered question with no option named, or more than three open questions, so the budget is enforced rather than requested. ## Environment bootstrap ```bash bash scripts/agent-bootstrap.sh # detect and reuse only, downloads nothing bash scripts/agent-bootstrap.sh --need captions # transcriber AND forced aligner bash scripts/agent-bootstrap.sh --need matting --need denoise bash scripts/agent-bootstrap.sh --all ``` It is idempotent and reuse-first: it detects ffmpeg/ffprobe/whisper-cli (and avconvert on macOS) and symlinks anything already present under `dev-assets/projects/*` into canonical paths, so a warmed machine finishes in seconds with zero network. Downloads happen only for groups named with `--need`; fetch for the edits the footage actually asks for, not up front. Missing items print a `todo:` line with the flag that fetches them, and anything listed `ok` is safe to use while other items are missing. The summary ends in `CAPTIONS BLOCKED` while the forced aligner is missing even when the transcriber is ready, because a transcriber alone is not a caption pipeline. Reading spoken instructions off a draft transcript is fine at any time; timing captions from one is not. Never `pip install` into an existing pinned venv without checking the imports first. A bare `pip install torchvision` into a venv that already has torch silently upgrades torch to the newest release (observed: 2.8.0 dragged to 2.13.0) and breaks the denoise stack sharing that venv. The bootstrap installs matched pairs only when the imports fail. ## Source analysis before effects Create one normalized source record before authoring the spec: - probe container, codec, dimensions, rotation, frame rate, duration, color range and audio streams; - transcribe speech with word start/end timestamps and language. Recognizer word times are a draft and can overrun the real media duration near the tail, so clamp them to the probed duration and confirm any time used as an edit beat against extracted frames; - inspect a contact sheet across the full timeline; - inspect frames around each named person, object, gesture, scene change and requested edit beat; - note face, mouth, active-hand, demonstrated-object and platform-UI regions; - preserve the original; use a rotation-correct proxy only when the renderer needs one. Treat spoken edit commands as commands only when the context supports it. Keep the original words in the transcript either way. ## Source fidelity Preserve the source's perceived color and audio unless the user explicitly requests a creative change. Color grading, LUTs, exposure, contrast, saturation, white balance, filters and HDR/SDR conversion are not reversible aesthetic defaults. - Preserve color primaries, transfer function, range, bit depth and HDR metadata through proxies and delivery when the output permits it. - Never reinterpret or merely retag HDR footage as SDR. A required HDR-to-SDR conversion aims for a perceptual match, not a new look, and uses a color-managed path. - Record every unavoidable conversion in the plan and the completion report. Ask before proceeding when the output needs a materially different look. - Compare representative source and delivered frames through the same reference display transform, checking skin tones, neutral surfaces, shadows and highlights. Metadata alone does not prove color was preserved, and a delivery with an unexplained shift is not approved because its tags are compliant. The renderer's master tags do not describe its pixels reliably, so the final encode has a verification step of its own: [features/delivery-color.md](features/delivery-color.md). ## The default edit for a weak prompt For a spoken social video the useful default is deliberately modest: - trim only obvious dead head and tail space; do not rewrite the speaker; - add word-timed captions; - apply brand tokens and real supplied brand assets; - use restrained emphasis overlays only where the speech supplies a clear semantic beat; - preserve faces, hands and demonstrated objects; - keep source audio primary and avoid decorative sound under important speech; - deliver a preview or checkpoints before paying for a full render. Do not fill silence with effects. Do not turn every sentence into a card. ## Captions, in one paragraph Spoken video gets word-timed captions by default unless the user opts out, and captions have exactly one route: `scripts/align-words.py` force-aligns, `scripts/build-caption-props.py` phrases, `scripts/check-caption-sync.py` verifies. **Timing comes from forced alignment, never from a transcriber.** Whisper drafts the words; a phoneme/CTC aligner decides when each one is said, and both downstream scripts refuse input that does not name a forced aligner. A cue carries at most 4 words, the letters carry a black outline, and captions start in the `caption-zone` region. Everything else, phrasing, presets, props and caption QA, is in [features/captions.md](features/captions.md). ## Protected subjects and the composed frame Faces, mouths, eyes, active hands, a demonstrated object and the current gesture target are protected. Add padding; do not merely avoid their bounding boxes. When showing an image or secondary video: decide which subject is primary at that beat, choose the secondary media region, reframe the base source into the complementary region with `source.reframes` (see [edit-spec.md](edit-spec.md#source-and-the-source-contract)), inspect the subject at entrance, midpoint and exit, and place captions in the remaining safe region. Prefer a stable crop with modest easing; unnecessary tracking looks nervous and costs more to verify. A spec has exactly one base `source` and it is the speaker, the one whose audio survives to delivery. A screen recording, a demo capture, b-roll or a flat color behind them all travel in props. Inverting that is the arrangement that loses the speaker's audio, which is why the base source is defined by audio rather than by whichever image is larger. ## Gesture-aware placement Words such as "here," "there," "this" and "right here" are spatial only when resolved with nearby frames. 1. Find the transcript time of the deictic word. 2. Inspect roughly 0.35s before through 0.65s after it. 3. Identify the active hand, fingertip and direction of the pointing ray. 4. Place the overlay just beyond the fingertip, usually about 3% of the frame past it, keeping hand, face and intended target visible. 5. Inspect the entire overlay window. Track only if the anchor moves enough to break the meaning. Use a custom percentage region for the resolved location. A named corner is only a fallback. ## Camera moves Only the preset grammar is implemented (`preset`, `amount`, `inSec`, `outSec`, `rest`, `focus`, `frequency`, `seed`, `time`, `easing`). Keyframes, rotation and `crop` are not; do not author them. - Outside its window a camera sits at its resting zoom, which is identity unless `rest` says otherwise. So "zoom in and stay zoomed" is either a window ending on the composition's final frame or a `rest` on a zoom preset. Do not leave a gap and expect the framing to persist. - `inSec` and `outSec` pin the approach and the return to exact seconds on the window, which is how a spoken "zoom in right now" becomes a camera rather than an approximation. - Choose the scope by semantics and text safety together. "Zoom in with the camera" is the recording camera, so footage-only `source.camera` fits, and it keeps text out of an animated scale (animated scale over text shimmers glyphs in Chromium renders; see [recipe-scaled-text.md](recipe-scaled-text.md)). - A footage push-in magnifies the subject toward locked overlay regions. Check overlay clearance against the zoomed subject at the overlay's exit time, using the camera's zoom value at that moment. Sequencing the beats usually resolves the collision without tracking. - Camera windows in one scope never overlap, each move has one editorial intent, and repeating or alternating zooms are never generated unless asked. ## Quotes, assets and logos A quote is content, not decoration. Select wording that supports the nearby speech and fits the safe region at a readable size, verify it against an authoritative source, and preserve exact wording, author, work and translation/edition. Never pass an attractive paraphrase as a quotation: label it a paraphrase or drop the quote marks. Prefer a shorter verified excerpt over shrinking a long quote until it is unreadable. Record the source in the plan even when the design shows only a concise attribution. Use assets in this order: user-supplied, brand library, licensed source, then a generated asset when generation is appropriate and disclosed. Record source URL, license and required credit; keep visible credit concise and full provenance in the plan. Never reconstruct a real company logo when the asset is unavailable. `logo-sting` accepts a real transparent `logo`; its monogram is an intentional fallback, not an imitation. ## Efficient QA Quality comes from targeted checks, not repeated full renders. 1. Validate the decision plan and the spec. If validation rejects a template slug as unknown, check `src/templates//` before redesigning around it: a finished template may exist but be unwired, and registration is a three-line change. 2. Render low-cost stills just before the edit, at entrance, midpoint, exit and just after. 3. Add a checkpoint for every spatial decision (face, split, gesture) and every semantic one (quote, number, name, attribution). When a fullscreen-first template sits in a custom region, verify its complete visual bounds; container clipping is not intentional scaling. 4. Run the scripted checks before the visual ones. `check-caption-sync.py` settles onset, silences, cue windows and delivered audio offset without a frame being looked at, so your eyes are spent on what it cannot judge. 5. Compare the decoded delivery audio zero-point with the final cut-only audio and correct codec or container delay before approval. Re-measure after every remux, even a stream copy: edit lists and AAC priming shift on a container change. With no cuts, stream-copy the original track and verify the delivery is sample-identical at zero offset. 6. Compare representative source and delivered frames through the same display transform. Fail unexplained changes in skin tone, neutrals, shadows or highlights. 7. Watch one complete preview at normal speed. Full-render once after these pass. Three frames may be enough for a stable static placement. A moving hand, crop, face or tracked anchor needs more. Spend QA where the uncertainty is. ## Completion report Report what was inferred, what was verified, asset provenance and credit, what the template system could not express, and which QA checks ran. Never claim "perfect sync," "face safe" or "verified quote" without the corresponding evidence. --- # Edit Spec v1 An edit spec is a JSON document that describes a finished edit: one base source, a stack of overlay templates, cameras, and sound. It is the only input an agent needs to produce a video. Zod schemas live in `src/spec/types.ts`; full validation (shape + template cross-checks) is `validateSpec` / `parseSpec` from `src/spec/validate.ts`. ```json { "version": 1, "format": "landscape", "fps": 60, "durationSec": 20, "source": { "type": "video", "src": "your-footage.mp4" }, "overlays": [ { "template": "speaker-card", "region": "lower-third", "time": { "start": "2s", "duration": "9s", "appear": 0.7 }, "props": { "name": "Ana", "role": "Founder", "photo": "ana.png" } } ] } ``` This page is the grammar every spec uses. Three parts of it are big enough to have their own page, and you only need them when the edit does: [captions](features/captions.md), [sound cues](features/sound.md) and [music](features/music.md). ## The two axes: region and time Natural language like "on the bottom, at 2/3 of the video, for 3s" compiles to `region: "lower-third"`, `time: { start: "66%", duration: "3s" }`. - **`region`** (space): a named region (`fullscreen`, `top-banner`, `lower-third`, `upper-third`, `caption-zone`, `left-panel`, `right-panel`, `center`, `corner-tl/tr/bl/br`) or a custom rect in percentages (`{ "x": 31, "y": 70, "w": 38, "h": 18 }`). Unset uses the template's preferred region. - **`time`** (timeline): values are seconds (`3` or `"3s"`), percentages of the owner timeline (`"66%"`), or negative seconds from the end (`"-2s"`). A window is `{ start, duration, appear, hold }`: `appear` is how long the entrance choreography takes, `hold` is how long the finished overlay stays after that. `duration` absent + `hold` present makes the window `appear + hold`. Sequence overlays with about 1s between windows so handoffs read cleanly. ## Source and the source contract `source` is the base layer: `{ type: "video", src }` (options: `muted`, `fit`, `position`, `flipHorizontal`, footage-only `camera`, time-windowed `reframes`), `{ type: "audio", src }`, or `{ type: "none" }`. ### Subject-safe source reframes `source.reframes` moves the base footage into a percentage rectangle during a time window while overlays stay locked to the composition. This is editorial layout, not camera motion. It is intended for split screens where simply covering half of the fullscreen source would hide a face or demonstrated object. Reframe windows may not overlap. ```json { "type": "video", "src": "speaker.mp4", "fit": "cover", "position": "center 30%", "reframes": [ { "time": { "start": "9s", "duration": "8s" }, "region": { "x": 4, "y": 8, "w": 44, "h": 84 }, "position": "60% center", "transitionSec": 0.3 } ] } ``` `fit` defaults to `cover`, `position` to `center`, and `transitionSec` to 0.3. Resolve the position by inspecting the subject across the full window. Every template declares one relationship with that base source, and validation enforces it: | Contract | Meaning | Requires | | --- | --- | --- | | `overlay` | Self-contained; draws over whatever the source is. Most templates. | nothing | | `annotates-video` | Only makes sense over footage (captions, ticker, recording chrome). | `source.type: "video"` | | `wraps-video` | Renders the base video inside its own layout (video-card). One per spec. | `source.type: "video"` | | `visualizes-audio` | Driven by a standalone audio file (audiogram); receives it as `sourceSrc`. | `source.type: "audio"` | A template that ships its own footage via props is still `overlay`: the contract describes the BASE source only. ## Camera motion vs object motion A **camera** moves the frame around finished content; **object motion** is the content moving, and it belongs to the template. A camera never choreographs entrances or exits. Cameras live at three scopes, location = scope: - `spec.camera`: the scene; source and every overlay move as one shot. - `source.camera`: footage only; overlays stay locked to their regions. - `overlay.camera`: one overlay's region viewport, on the overlay's own timeline. Presets: `push-in`, `push-in-out`, `push-in-fast-out`, `pull-out`, `pan-left/right/up/down`, `handheld` (fields `frequency`, `seed`), with `amount`, `focus {x,y}`, `time`, `easing`. Scene scope accepts an array of windows. Full grammar: [camera-motion-spec.md](camera-motion-spec.md). ## The motion language Small on purpose, so mixed templates read as one design system. All three knobs are optional; unset keeps the template's native motion. - **`reveal`** (text entrance hint): `"fade-up"`, `"blur-in"`, `"typewriter"`. Text templates map it to their nearest native mode and may expose richer extensions via props (quote-card `animateIn` adds `"lines"`/`"words"`; tweet-card adds `"paragraphs"`/`"none"`). - **`enter`** (card entrance, renderer-provided, identical on every template): `"slide-left"`, `"slide-right"`, `"spring"`, `"mask"`. - **`exit`**: `"blur-out"` (blur + lift, the signature departure), `"fade-down"`, `"shrink"`, `"vanish"` (anticipation, then a 0.45s implosion with blur and a `pop` cue: the literal "make it disappear"; the other three run 0.9s). Templates with their own `exit` prop (speaker-card) treat the spec value as default, not override. - **`motion`** (object motion, works on EVERY template). Two families, and the family decides which knobs mean anything. **Periodic**, oscillating for as long as their window lasts: `"shake"` (held in a hand: a slow drift, a mid correction and a fine tremor), `"wobble"` (a rock), `"float"` (a bob), `"sway-3d"` (a slow turn through depth, left and right). Shaped by `frequency`, phase-shifted by `seed`, faded by `rampSec` and `rampOutSec`. **One-shot**, traversing their window exactly once and easing to a stop at both ends: `"skew-right"`, `"skew-left"`, `"skew-up"`, `"skew-down"`. Each starts at the opposite extreme, passes through flat, and lands in a strong perspective keystone facing the named direction. The window IS the tempo, so `frequency`, `seed` and the ramps are inert for these; use `time` to say when and how long. The pair on one axis chains seamlessly, because the first ends in exactly the pose the second starts from: ```json "motion": [ { "style": "skew-right", "time": { "duration": "4s" } }, { "style": "skew-left", "time": { "start": "4s", "duration": "4s" } } ] ``` Shared knobs: `style`, `amount` (0..1, unset uses the style's own default: 0.2 for the flat styles, 0.7 for `sway-3d`, 0.175 for the `skew-*` sweeps, which land near 10deg because a sweep HOLDS its end pose and the card has to stay readable there) and `time`. Amplitudes are percentages of the overlay's region box, so one `amount` reads the same everywhere. It is its own transform layer, so it **stacks** with `camera`, `enter`, `exit` and `scale`, and an array of motions stacks with itself: ```json { "template": "quote-card", "enter": "spring", "camera": { "preset": "pull-out", "amount": 0.25 }, "motion": { "style": "shake", "amount": 0.3 } } ``` Use it instead of a `handheld` camera whenever you want the thing to move rather than the view of it: the camera preset zooms the region viewport by 5x `amount` to hide frame edges, which changes how big a card reads and is meaningless for a transparent cutout. Templates may ship a `defaultMotion` (the sticker drifts for free); `motion: { "style": "none" }` turns it off. `src/player/shake.ts`, `src/player/OverlayMotion.tsx`. ## Sticker border `sticker` takes an optional die-cut outline. It is off unless you ask for it, and it follows the artwork's own alpha edge, not its bounding box, because a rectangle around a cutout is exactly the frame this template exists to avoid: ```json { "template": "sticker", "props": { "src": "pickle", "border": { "width": 10, "color": "white" } } } ``` `width` is design pixels at a 1080px short edge and means what it says: the outline is grown one width out from the silhouette in every direction. `color` takes a brand token, `white` / `black`, or a `#rrggbb` literal, the same escape captions open. The border is painted before the contact `shadow`, so the shadow is cast by the outlined silhouette instead of crossing it. Two scaling behaviours worth knowing, because they differ. A camera transforms the whole overlay after the outline is drawn, so a push-in thickens the edge along with the art, which is what something belonging to the sticker should do. `sizePct` does not: the width resolves against the composition, not the element, so it is a fixed thickness on the finished frame and a small sticker reads with a proportionally heavier edge. Drop the width when you shrink the art. ## Bundled sticker art `sticker`'s `src` takes a bundled library NAME as well as a path or URL, so an agent with no artwork of its own still has cutouts it can reach: ```json { "template": "sticker", "props": { "src": "pickle" } } { "template": "sticker", "props": { "src": "/my-brand/mascot.png" } } ``` Names carry no slash and no extension, so they can never collide with a path. The set is `STICKER_LIBRARY` in `src/templates/sticker/library.ts` and the files live in `public/stickers/`; provenance is in `public/stickers/SOURCES.md`. Today it is one entry, `pickle`. Everything else about the template is unchanged: the name only resolves the file. ## Sound and music Cues default on. `sound` at the spec root sets defaults and each overlay may override it; a template's own cue is a prop (`"sfx": "ding"`, `"sfx": false`), while `sound.sounds` remaps by cue name for everything in scope. The core palette is `click`, `pop`, `whoosh`, `ding`, `typewriter`. Full rules, including the two scopes and the bundled library cues: [features/sound.md](features/sound.md). `music` at the spec root is the bed under the whole composition, outside every camera. `volume` is linear gain, defaults to `0.08`, and is the whole job: a bed under speech belongs 15 to 20 LU below it, so measure both with `ffmpeg -af ebur128` and compute the gain instead of judging it in a preview. `validateSpec` rejects a bed above `0.3` while the source audio is unmuted. `fadeInSec` (1), `fadeOutSec` (1.5), `trimStartSec` and `loop` (off) are the rest. The arithmetic, a worked example and the fade/loop reasoning: [features/music.md](features/music.md). ## Captions `caption-classic` is the caption renderer, and captions have a route that starts before the spec does: forced alignment produces the word times, and a transcriber never does. Read [features/captions.md](features/captions.md) before authoring one. The shape, in brief: pass one composition-relative `track` of cues and words (the legacy overlay-relative `lines` remains valid), position with `region`, prefer the named `"caption-zone"`, and configure `preset` (`classic`, `minimal`, `editorial`, `punch`, `extruded`), `grouping`, `layout`, `highlight`, `appearance`, `styles` and word `marks`. ```json { "template": "caption-classic", "region": "caption-zone", "time": {}, "props": { "preset": "classic", "track": { "timebase": "composition", "cues": [ { "start": 1.1, "end": 2.4, "words": [ { "text": "Build", "start": 1.18, "end": 1.45 }, { "text": "OverlayMotion", "start": 1.46, "end": 2.05, "marks": ["buzzword"] } ] } ] } } } ``` ## Brand theme Templates never hardcode style; they read tokens from the active `BrandTheme`: `colors` (primary, onPrimary, surface, onSurface, muted, background, optional secondary/accent), `fonts` (heading, body, optional serif), `radius`, `logoText`, and optional `style` (surface `"solid" | "glass"`, `blur` up to 100, `opacity`, gradients, `borderColor`). Same spec + another theme = rebranded video. Brand comes from the theme, never from props: no hex colors in props, and caption style overrides reference these roles. ## Formats `vertical` 1080×1920, `horizontal` 1920×1080, `landscape` 1620×1080, `square` 1080×1080. Templates scale through the shared `rem()` helper, so one spec renders correctly in all four. ## Validation `parseSpec(raw)` (throws) or `validateSpec(raw)` (safe result) from `src/spec/validate.ts`. Beyond shape, they validate every template's nested props, enforce known template slugs, source-contract requirements, at most one `wraps-video` overlay, and non-overlapping source-reframe windows. Overlay and source-reframe windows may not extend past the composition duration. `editSpec.parse` alone checks shape only; prefer the checked variants. --- # Captions Read this before authoring any request for subtitles, captions or word-timed text. Spoken video gets captions by default unless the user opts out. ## One route, three commands ```bash python3 scripts/align-words.py --audio --transcript copy.txt python3 scripts/build-caption-props.py --dir --source