# Mograph — style-locked motion graphics

The mograph lane turns a script or timed transcript into a fleet of short
motion-graphics clips that all share one look. It has two artifacts, four
commands, and one principle: **style is locked once in a motion sheet; every
clip prompt is action-only choreography.** The engine stacks
`style lock + SHOT + AVOID` per block at plan time — style text is
never re-typed per clip, so the look cannot drift across 100+ clips.
Mograph clips render **silent**: both doors ask the route for no audio
(`mograph-render --enqueue` and the manifest `--emit-batch` writes), because the
clips sit under your own voice-over. A block's `sfx` cues are notes for whoever
mixes that voice-over, not a request to the model.

Creative authoring (the sheet interview, block choreography) lives in
`skills/mograph/SKILL.md`. This doc is the command/artifact contract. The
design rationale is in `docs/design/specs/2026-07-13-mograph-native-port.md`.

## Artifacts

### `artifacts/motion-sheet.json` (schema: `schemas/video/artifacts/motion-sheet.schema.json`)

The single style authority for the project's motion-graphics lane:

- `styleLock` — ≤120 words, attached verbatim to every clip prompt. Must
  contain the layout guard ("do NOT copy the sheet's layout") — without it,
  clips reproduce the reference board's grid as their composition.
  `budget: "film"` lifts the cap to 320 words for a 2–4 block launch /
  product film whose lock also carries palette roles and render rules; the
  pack lint refuses it past 4 blocks (a film lock is paid once per block).
- `hero` — `{ descriptor, anchor }` for a product, mark or building film.
  The anchor is the ONE design feature the generator keeps in every frame (a
  feature, never a material or a colour). Code stacks a `HERO:` line above
  every block prompt, so a split film restates its subject in full without
  the author re-typing it into the action.
- `negative` — must end with the five audio bans (`no music, no soundtrack,
  no voice-over, no narration, no lyrics`): clips sit under your own VO. The
  bans stay in the prompt even though the render is silent, so a block reused
  on a door that does generate audio cannot come back singing.
- `family` — one of the built-in style families (`mograph-sheet --families`):
  paper-editorial, cutout-collage, zine-print, flat-vector, painted-photo,
  kinetic-type, flat-duotone, glass-dark, blueprint, terminal-neon, soft-3d,
  launch-motion.
- `refImage` — the rendered master style board (versioned; regeneration never
  overwrites a prior version), plus optional `stage` (a persistent background
  world for one-continuous-take continuity), `palette`, `typeRoles`,
  `logoRefs`, `gotchas`.

### `artifacts/motion-pack.json` (schema: `schemas/video/artifacts/motion-pack.schema.json`)

One per video: `coverage` rows give every second of the timeline a decision
(`generated` / `v2v` / `talking-head` / `screen-rec` / `reuse`), and `blocks`
carry the clips — id `B###`, time range, priority `P1|P2|P3`, mode
(`t2v|ref2v|v2v-stylize|v2v-overlay|v2v-transition|v2v-inset`), optional VO
line / SFX cues / per-block refs / loop flag / v2v `videoSource`, and the
`action`: pure choreography, ≤90 words, no style vocabulary. Optional
`copyList`: every on-screen string in the film, once, verbatim — the copy-list
contract (`*word*` marks a headline's one accent word). When present, a quoted
string in any block that the list does not carry is an error, and a listed
string no block renders is a warning.

## Commands

```bash
vclaw video mograph-sheet  --project <slug> (--from-json <path> [--write] | --show | --master-prompt | --families)
vclaw video mograph-pack   --project <slug> [--init-from <srt|vtt|whisper-json> --video "<title>" | --check | --stats | --assemble <id> | --list]
vclaw video mograph-render --project <slug> [--priority P1|P2|P3] [--block <id> ...] [--route <route>] [--enqueue | --plan-only] [--emit-batch <path>] [--write-sidecars <dir>] [--stitch]
vclaw video mograph-logos  --project <slug> --brand <name> [--brand <name> ...]
```

- **mograph-sheet** validates and persists the sheet (fail-fast on a missing
  layout guard, missing audio bans, over-budget style lock, unknown family,
  undescribed locked stage). `--master-prompt` composes the deterministic
  art-direction-board prompt — render it via `vclaw video gen-image` at the
  DELIVERY aspect (providers inherit aspect from the attached reference).
  A `locked` sheet refuses overwrite without `--force`.
- **mograph-pack** seeds a coverage-only skeleton from a timed transcript
  (pause-aware ~5s beats), lints the pack (the anti-drift gate: hex codes and
  style words in actions, unquoted or over-long on-screen text, SFX naming
  music, v2v without a source, duplicate ids, coverage gaps), prints scope
  stats (P1 / P1+P2 / all, with `--cost-per-clip` estimates), and assembles
  any block's exact submitted prompt.
- **mograph-render** plans or creates a durable render DAG. **Refs are transport-aware:** on
  the Omni family (`veo-useapi`) IMAGE refs are OMITTED and the clip renders
  prose-only from the style lock (an attached ref hijacks the on-screen text —
  validated across ~20 pilot renders; the plan surfaces `refs-omitted-omni`),
  while Seedance-family routes keep the sheet ref. It lint-gates the pack, then
  either:
  - **default / `--enqueue` (canonical durable queue):** compiles every selected block,
    including its exact prompt, ordered references, video source, route and
    artifact lineage, into one shared Cinema production queue. Each provider
    render owns a dependent zero-cost `local-mograph` post-process task that
    re-hashes the provider output before writing
    `mograph/clips/<block>.mp4` and the exact sidecar. `--stitch` adds one
    local assembly task that waits for every post-process task and writes
    `final/videos/mograph-master.mp4` plus a hash receipt. The command persists
    an immutable compatibility receipt and returns shared task status without
    calling a provider or authorizing spend. Provider-backed renders enter
    `awaiting-quote`; local tasks remain blocked on their dependencies.
  - **`--plan-only`:** inspect the exact render contract without writing queue
    state.
  - **`--emit-batch` (legacy-compatible export):** writes a batch manifest,
    but new submission still enters the canonical queue through
    `batch-submit --project`; the old native submit/monitor-resubmit path is
    retired.
    ```bash
    vclaw video mograph-render --project demo --priority P1 --route runway-useapi --emit-batch out/p1.json
    vclaw video batch-submit --manifest out/p1.json --project demo
    ```
    References ride in the manifest's `characterRefs` (the provider REFERENCE
    slot, never the first frame). v2v blocks are excluded from the manifest
    explicitly. `--write-sidecars` can emit planning sidecars immediately;
    the durable post-process tasks always write verified delivery sidecars under
    `artifacts/mograph-sidecars/`. Clips render silent, so each sidecar carries
    the block's `sfx` cues and, when the sheet sets one, its `audioIdentity` for the
    post mix.
- **mograph-logos** fetches real brand marks (keyless: Clearbit → SimpleIcons
  → favicon fallback) into `projects/<slug>/assets/logos/`. Attach the file as
  a block ref and prompt "the attached <brand> logo, flat, unmodified" — a
  model drawing a mark from memory produces gibberish letterforms.

## Gates — what refuses to render, and why

Two lints stand between a pack and spend. Both are fail-fast: an **error** exits
non-zero and blocks the render; a **warning** prints and proceeds.

**The sheet is validated at RENDER time, not only when written.** It is a file on
disk, so it gets hand-edited after the write — and four defects reached delivered
clips through that gap. Every render plan goes through one choke point, so the
durable DAG, `--plan-only`, `--emit-batch` and sidecars are all covered.
Read-only paths are
deliberately NOT gated: `--show` reports the issues *alongside* the sheet (it is
the command you reach for when the sheet is broken), and `--master-prompt`
composes the board from palette/type/stage and never reads `styleLock`.

| Code | Sev | What it catches |
|---|---|---|
| `style-lock-not-descriptive` | error | A lock that names the family instead of describing it (<35 words of real description, guard clauses stripped so boilerplate cannot pad the count). On the default route image refs are stripped, so this prose is the ONLY style instruction the model gets — a lock reading "use the attached style sheet" shipped a 3D render for a flat-vector film. |
| `style-lock-hex-code` | error | A hex code in the lock. The model reads it as copy to set: `#F5B72E` and `#1B2A4A` were typeset into frame as labels. Name the colours; the board pins the values. |
| `style-lock-over-budget` | error | Lock over 120 words. It rides above every block, so every word is paid for N times. |
| `style-lock-layout-guard-missing` | error | No "do NOT copy the sheet's layout" clause — without it clips reproduce the board's grid as the composition. |
| `style-lock-sample-words-guard-missing` | warning | Only bites routes that keep the board ref (Seedance family); its sample copy bleeds in as on-screen text. |
| `negative-audio-bans-missing` | error | The negative must end with the five audio bans — clips sit under your own VO. |
| `block-verb-repeated` | error | Consecutive blocks that add no motion the previous one did not already use. A pack could "pop up … settle" eleven times and pass everything else. Verbs match on **word forms**: the register writes "tapes down", action prose says "tape down", and matching the register's spelling literally left the gate silent on real content. |
| `block-verb-unrecognised` | warning | A block whose action uses no verb from its family, so the variety check could not evaluate it. Raised for every block including the first. |
| `action-text-slots-unspecified` | warning | The action builds more copy-bearing components (label bars, caption strips, stat tiles) than it gives words for. An unspecified slot is not blank on the generator — the model fills it from the nearest vocabulary it has, which is the style lock riding above every prompt. A pack asking for three label bars with one quoted string rendered two cards reading `QUIET MONO MONO`, straight out of the lock's "quiet mono labels", and it passed every other gate. Quote every slot, or declare the spares positively ("blank label bar"). |
| `family-components-underused` | warning | Fewer than 4 of the family's components appear anywhere in the film. |
| `copy-list-string-unlisted` | error | A quoted on-screen string in a block that the pack's `copyList` does not carry. The list is the contract: every string the film shows is decided once, up front. |
| `copy-list-entry-unused` | warning | A `copyList` entry no block renders — a string the film promises and never shows. |
| `copy-list-accent-word-count` | warning | A list entry marking more than one `*accent*` word; one accent word per headline. |
| `sheet-film-budget-too-many-blocks` | error | A sheet on `budget: film` (320-word lock) driving a pack of more than 4 blocks. The film lock is paid once per block; past a short film, trim to the fleet budget. |
| `hero-anchor-missing` | error | A sheet `hero` with a descriptor but no anchor — without the one feature to keep, the generator keeps nothing. |

## Route notes

| Route | Use for | Notes |
|---|---|---|
| `runway-useapi` (batch default) | free overnight drafts | free Runway queue; low res |
| `seedance-direct` | free/paid finals via the configured adapter | reference budget ≤9 images |
| `dreamina-useapi` | paid hi-res finals | Seedance-family text rendering is mid |
| `veo-useapi` (omni-flash) | Omni and v2v queue tasks | best on-screen text; **ref-free** (image refs omitted — prose-only) |
| `prompt-only` | always free | sidecars to paste into any tool |

**Ref-free is Omni-only.** Seedance-family routes KEEP the sheet ref (they lock
identity differently) but are weaker at on-screen text — keep text-heavy P1
cards minimal there, or render them on Omni. After rendering, the standard
finishing lane applies: `assemble --from-clips`, `media-qc`, the preview portal,
`make-vertical` / `burn-subtitles`.

## Deterministic engine (default for flat brand graphics)

For FLAT brand-token motion graphics (solid grounds, cards, pills, icons,
typography), skip AI-video rendering entirely: the deterministic PIL engine
bundled at `skills/mograph/scripts/deterministic/` draws every frame as code
and pipes rawvideo into ffmpeg — pixel-perfect text, exact brand hexes, real
composited logos, procedural sample-accurate SFX, and 16:9 / 9:16 / 1:1
masters from one codebase, free and reproducible (~3 min per aspect, local).
The provider routes above are for **organic** looks (texture/light/depth) and
v2v work. See the engine's README for the workflow and the PIL/ffmpeg gotchas;
the doctrine and decision rule live in `skills/mograph/SKILL.md`.

## Worked example — first production run (validated 2026-07-13)

The full lane, exactly as first exercised on a real project
(`paper-editorial-explainer`, paper-editorial family, 16:9, locked stage,
calm-premium):

```bash
# 1. Project + sheet (interview happens in the mograph skill; JSON is the output)
vclaw video init paper-editorial-explainer
vclaw video mograph-sheet --project paper-editorial-explainer \
  --from-json sheet.json --write                      # ok:true, 0 issues

# 2. Board image: compose the prompt, render via gen-image, review, then lock
vclaw video mograph-sheet --project paper-editorial-explainer \
  --master-prompt --aspect 16:9                        # 408 words (400-600 window)
vclaw video gen-image --project paper-editorial-explainer \
  --prompt "<master prompt>" --kind overlay --aspect 16:9 \
  --model openai-gpt-image-2 --out .../assets/MG-PAPER-ref_001.png --confirm-spend
# operator approves the board -> write refImage {path, version:1, approvedAt}
# + locked:true back into the sheet JSON and --write again

# 3. Pack (outline route, timing: draft) -> lint -> stats
vclaw video mograph-pack --project paper-editorial-explainer --check   # 0 errors
vclaw video mograph-pack --project paper-editorial-explainer --stats

# 4. One pilot block through the free lane
vclaw video mograph-render --project paper-editorial-explainer \
  --block B001 --route runway-useapi \
  --emit-batch batch/pilot-b001.json --write-sidecars artifacts/mograph-sidecars
vclaw video batch-submit --manifest batch/pilot-b001.json --project paper-editorial-explainer   # compiles the task into the cinema queue
vclaw video cinema-status --project paper-editorial-explainer       # the task id, then quote/authorize/run it with cinema-work
vclaw video cinema-sync --project paper-editorial-explainer --task <taskId>   # poll + download; repeat until terminal
```

Operational notes from that run (historical observations, not current account
entitlement or queue timing guarantees):

- **`--kind overlay` for the board render** — screens/overlays keep text; the
  board's type specimen NEEDS its text. `openai-gpt-image-2` is the house model
  for multi-panel boards with heavy typography; the first render passed QC
  (crisp specimens, correct palette hexes, no gibberish) with zero iterations.
- **The historical run encountered an explore-lane throttle.** Current
  `batch-submit` only compiles tasks: it cannot report a live submission.
  Submit through `cinema-work` with the required approval, then reconcile the
  accepted task with `cinema-sync`. Discover current route availability before
  using the old Runway recipe.
- **Lock order matters**: persist the sheet WITHOUT `refImage` first, render
  and review the board, then write `refImage` + `locked: true` in a second
  `--write`. A locked sheet refuses further overwrites without `--force`.
- **Outline route packs** (`timing: draft`, hand-authored coverage + blocks,
  no `--init-from`) lint the same as transcript-seeded packs — the gate does
  not care where the beats came from. Re-map times and flip `timing: final`
  once the real VO lands.
