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 aHERO: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 optionalstage(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
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-promptcomposes the deterministic art-direction-board prompt — render it viavclaw video gen-imageat the DELIVERY aspect (providers inherit aspect from the attached reference). Alockedsheet 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-clipestimates), 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 surfacesrefs-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-costlocal-mographpost-process task that re-hashes the provider output before writingmograph/clips/<block>.mp4and the exact sidecar.--stitchadds one local assembly task that waits for every post-process task and writesfinal/videos/mograph-master.mp4plus a hash receipt. Widening a stitched run (--priority P1 --stitch, then--priority P2 --stitch) plans a second assembly: when it covers every block of the film the master's receipt names, it replaces that master and keeps the old one inmograph/superseded/<time>/; one that leaves out a block of that film is refused, so a narrower assembly never replaces a fuller one, even when two run at once: the encode writes a temporary file, and the swap holds the master's lock and checks again. A failed encode keeps the master it would have replaced. A refused assembly cannot run; plan one that covers every block with--stitch(#707). The command persists an immutable compatibility receipt and returns shared task status without calling a provider or authorizing spend. Provider-backed renders enterawaiting-quote; local tasks remain blocked on their dependencies. A re-run reuses every unchanged task that can still run. A re-run that would add a second post-process task to a render already queued is refused before anything is written. The refusal lists every such block with the queued task, its status and its render. That happens when the block'svoorsfx, or the sheet'saudioIdentity, changed, because none of them reach the render. Both tasks would write the block's sidecar, and the local worker never overwrites it with different bytes. The pack'sgeneratedAtand the block'sprioritydescribe the plan, not the delivered clip: a sidecar that differs only in them counts as the same delivery, so regenerating the pack or re-prioritising a block is not refused, and the sidecar on disk stays as it is (#708). A sidecar already on disk that differs in a field that matters, for example one a failed post task left behind, refuses the next post task for that render too, and the refusal prints the command that moves it aside. The checks and the enqueue hold one project lock, so two runs on one project take turns. A block whose planned post task is already queued is reused, even beside a second one an older vclaw left on that render. If the render under that post task has not reached a provider, retiring the render withcinema-retiretakes its post task with it, and the re-run then gives the block a fresh render; otherwise restore the fields, leave the blocks out with--block, or keep the queued cues and give the post mix the new ones with--plan-only --write-sidecars <dir>. A re-run whose render for a block could never deliver is refused too, before anything is quoted: the block's clip or sidecar is already delivered, or another render's clip is on its way (submitted, or authorized, under a live authorization, and not yet run). Every render of a block delivers to the samemograph/clips/<block>.mp4andartifacts/mograph-sidecars/<block>.json, which the local worker never overwrites, so that render would be paid for and could never deliver. That covers a new render (the block's action, mode, references, duration, aspect or route changed) and an older render the plan maps back to that was never run, for example after reverting an action. To re-render a delivered block on purpose, move its files aside: the refusal prints the exact commands, also indetails.moveAside, which move the clip, the sidecar and any stitched master intomograph/superseded/<time>/. An earlier render that was never run and never authorized does not block a re-render: it stays in the queue and costs nothing as long as nobody authorizes or runs it. One authorized under a live authorization does block it until it runs (and is paid for) and delivers, the authorization lapses (the refusal gives the time), or you retire it withvclaw video cinema-retire --project <slug> --task <id> --confirm-retire, which also retires every task depending on it (its post task, and a stitched film's assembly, which can no longer run). One waiting for a retry does not, since nothing claims it again. A re-run never re-uses work that will never run (#731). A block whose render, post task or assembly maps onto one that failed, was cancelled or retired, was dead-lettered, or waits for a retry nothing claims gets a fresh attempt instead (attemptfresh-2, thenfresh-3, and so on): a new task, listed inqueueCompatibility.freshAttemptswith the tasks it replaces, and a re-run of the same plan re-uses it rather than adding another. A fresh render is quoted and authorized like any new render and checked like one, so it is refused while the block is delivered or another render's clip is on its way. A render bound to an authorization that has lapsed cannot be given one, because the queue re-uses it as the same paid work: the re-run is refused and names thecinema-retirecommand, and once that render is retired the next re-run gives the block a fresh render. An assemble task stitches only the clips its own post tasks delivered (#731). An earlier run's assembly, run after another render replaced one of its clips (or a clip changed on disk), is refused as it would be claimed, before it encodes anything, and stays queued. For the film the pack plans now, re-runmograph-render --stitch, which plans an assembly of the clips on disk. For the film the old assembly planned, put its clips and sidecars back (a re-render's move-aside left them undermograph/superseded/) and run it again, or retire it withcinema-retire. A clip that changes during the encode leaves no master, and that assembly has failed: re-running the mograph-render that planned it gives it a fresh attempt. The same rule holds for a render already in the queue, run later by its task id:cinema-workandcinema-work-quoterefuse a mograph render whose block is already delivered, or whose clip is on its way from another render (in flight, or done with its post task pending), before anything is quoted, claimed or submitted, and print the same move-aside commands. Whichever render is submitted first wins; a render past its submit is never refused, so its poll and download always go on. The check runs again under the queue lock as the claim is taken, so two drivers starting two renders of one block at once cannot both submit. A claim lost before any submission (its lease ran out and it has no provider job) does not hold the block: any claim puts it back to ready, so its old holder can no longer submit under it, and both guards treat it as ready. --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 throughbatch-submit --project; the old native submit/monitor-resubmit path is retired.bashReferences ride in the manifest'svclaw video mograph-render --project demo --priority P1 --route runway-useapi --emit-batch out/p1.json vclaw video batch-submit --manifest out/p1.json --project democharacterRefs(the provider REFERENCE slot, never the first frame). v2v blocks are excluded from the manifest explicitly.--write-sidecarscan emit planning sidecars immediately, into any directory butartifacts/mograph-sidecars/, which it refuses: the durable post-process tasks write verified delivery sidecars there and never overwrite one with different bytes. Clips render silent, so each sidecar carries the block'ssfxcues and, when the sheet sets one, itsaudioIdentityfor the post mix.
- default /
- 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):
# 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 mograph/planning-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 terminalOperational notes from that run (historical observations, not current account entitlement or queue timing guarantees):
--kind overlayfor the board render — screens/overlays keep text; the board's type specimen NEEDS its text.openai-gpt-image-2is 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-submitonly compiles tasks: it cannot report a live submission. Submit throughcinema-workwith the required approval, then reconcile the accepted task withcinema-sync. Discover current route availability before using the old Runway recipe. - Lock order matters: persist the sheet WITHOUT
refImagefirst, render and review the board, then writerefImage+locked: truein 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 fliptiming: finalonce the real VO lands.
