| name | demo-director |
| description | Turn a SaaS web app into a polished 60-90s marketing demo video — explore the app with Playwright, extract its brand, write a playbook-driven script, narrate with local Kokoro TTS, capture real footage, and render with Remotion. Use when the user asks for a product demo video, marketing video, or promo video of a web app. |
DemoDirector
You are directing a marketing demo video of a real SaaS app. CLI scripts do the mechanics; you do the judgment. All commands run from the repo root; all artifacts live in jobs/<slug>/. Read references/demo-playbook.md BEFORE writing any script or storyboard, and follow it mechanically. When anything breaks, check references/troubleshooting.md first.
Stage 0 — Brief
- Run
npm run doctor (= npx tsx scripts/doctor.ts). If anything is MISS, give the user the printed install hints and stop until resolved. espeak-ng showing WARN is fine — TTS bundles espeak via espeakng-loader. If uv is MISS only because the shell predates a fresh install, set $env:UV to the full uv.exe path and use it in Stage 4 instead of uv (see troubleshooting).
- Ask the user (one message, all questions): app URL; login needed? (offer: manual login in a headed browser / .env credentials / none); preset (default narrated-75s); target audience; any features they insist on including; optional music file path.
- Create
jobs/<slug>/project.json matching ProjectSchema (see src/schemas.ts). Slug: lowercase-kebab from app name.
Stage 1 — Explore
- If login is needed:
npx tsx scripts/explore.ts <url> jobs/<slug>/discovery --profile jobs/<slug>/browser-profile --login — a headed browser opens at the app and stays open; tell the user to log in there, then press Enter in the terminal to save the profile. Then continue with the full explore.
- Run
npx tsx scripts/explore.ts <url> jobs/<slug>/discovery --profile jobs/<slug>/browser-profile --max 15.
- SPA fallback: if fewer than 3 screens come back, read the first screen's ariaSnapshot, derive the app's main routes from its nav, and re-run with
--routes /route1,/route2,.... For apps that navigate only by clicking (no route URLs), capture the missing screens later with tiny shot-only playbooks via capture.ts (record: false, click steps, a final shot).
- Read
discovery/raw-screens.json; view each screenshot; read each ariaSnapshot.
- Write
jobs/<slug>/discovery/feature-map.json (FeatureMapSchema): name each screen, its purpose, a 1-5 marketingScore (5 = shows the aha moment), and keyActions with a11y targets you saw in the snapshots — never invent UI labels.
Stage 2 — Brand
- Run
npx tsx scripts/brand.ts <url> jobs/<slug>/brand.
- Read
brand/brand-raw.json. Curate brand/brand.json (BrandSchema): pick primary/accent/background/text (prefer css-var > theme-color > manifest > vibrant), choose the best logo candidate (view the images), set fonts from computed values, record sources. Logo paths must be brand/<file> (public-dir relative).
GATE A
Show the user: the feature map (table: screen, purpose, score) and the brand kit (palette swatches as hex, logo file, fonts). Ask: "Demo will focus on the top-scored features: <list 3>. Brand kit look right?" Adjust per feedback before continuing.
Stage 3 — Creative
- Re-read BOTH
references/demo-playbook.md AND references/storyboard-spec.md before writing anything. Write jobs/<slug>/script.md: narration per beat (hook / problem / solution / 3 features / proof / cta), word counts per the WPM budget (seconds × 2.5), hook from the playbook formulas, benefit-first lines, exactly one CTA.
- Write
jobs/<slug>/storyboard.json (StoryboardSchema) and one capture playbook YAML per clip/still scene in jobs/<slug>/playbooks/ (exact contracts + a worked example of each: references/storyboard-spec.md). Targets MUST come from feature-map keyActions. Every click needs a done. Vary transitions and scene rhythm; zoom only on trigger+payoff.
- Caption placeholder rule: write
"captions": [] for every scene that will get captions. Do NOT invent atMs values at this stage — they must come from timing.json word timestamps produced in Stage 4. See the caption-authoring step below.
v2 composition rules (apply to every storyboard from template v2 onward):
- Prefer
component scenes for hero moments. One billboard-scale recreated UI element per beat beats a full-page clip every time. Use kinetic, clip, or still for context-setting and transitions; use component for the beats where you need the viewer to feel "oh, that's the product."
- Kinetic punch for hook and problem. The cold-open hook and the problem beat should almost always be
kinetic with layout: "punch" — one short claim per line, emphasis words in the brand accent, no footage needed.
- Hard-cut budget: ≤1 per 10s. Everything else transitions via
"fade" plus camera moves and element fly-ins over a persistent background. Back-to-back "cut" transitions are a machine-generated tell. Reserve "cut" for chapter boundaries only.
- Set
musicBpm when music is known. Add "musicBpm" to project.json whenever you have a track — compose will snap all scene durations to the beat grid automatically. Do not hand-pad minDurationMs to approximate beats.
- Camera variety across consecutive scenes. Never assign the same
camera preset to two consecutive component or clip/still scenes. A sequence like tiltIn → floatSlow → pushIn reads as intentional; floatSlow → floatSlow → floatSlow reads as a default dump.
- Real data only in component props. Every prop you pass to a
component scene must come from what you actually saw in the app (snapshots, screenshots, feature map). The default props in each component are ZOOQ-specific and must be overridden for any other product.
- Capture physics: the screencast only emits frames when pixels change — a flow that should breathe on screen needs explicit
wait steps (they extend the last frame's encoded duration; idle without them adds nothing). Place waits where the viewer must absorb the UI.
- Pick
fit per clip scene: speed-fit for flows longer than their narration, hold for payoff moments that should freeze on the result, trim rarely.
- Validate: every storyboard playbook file exists; each playbook's
scene: equals its storyboard scene id; narration word counts fit scene durations; ≤3 feature beats.
GATE B
Show the user script.md (full text) and a scene-by-scene storyboard summary (beat, visual, narration line, planned zooms/overlays). Iterate until approved.
Stage 4 — Narrate
Pronunciation respelling rule
Brand names, initialisms, and unusual proper nouns must be respelled phonetically in the narration text only — on-screen text (overlays, kinetic lines, captions, component props) always keeps the correct spelling.
| correct spelling | narration respelling | rationale |
|---|
| ZOOQ | Zuke | TTS would otherwise say "Zoo-cue" or "Zok" |
| JSON | Jason | TTS says "Jay-son" for the respelling; Jayson → natural |
| SQL | sequel | standard dev pronunciation |
| API | A-P-I | read letter by letter; no respelling needed (TTS handles it) |
| MCP | M-C-P | same — letter-by-letter is correct |
General technique: test a respelling by running TTS on just that word/phrase, listen to the output, adjust the spelling until the pronunciation is natural, then use it throughout the narration. Keep narration and on-screen visuals decoupled — the viewer reads "JSON" on screen while hearing "Jason" in the audio; they are the same word to the viewer.
The zooq-v3 project shows both in action — project.json userNotes documents the respellings used, narration says "Zuke" and "Jason", while the on-screen title card and terminal component show "ZOOQ" and "JSON".
Voice and speed guidance
af_bella @ speed 1.15–1.2 — punchy launch cuts, energetic tone. Use for 20–45s social/teaser formats. Speed 1.2 is the sweet spot for keeping energy high without sounding rushed. At 1.2 speed, target ≈ duration_target_seconds × 2.4 words for VO (not the base 2.5 — the speedup buys ~4%).
am_michael @ speed 1.0 — calm technical walkthroughs, enterprise audience. Measured pace, clear enunciation. Target the base 2.5 words/second budget.
- Set
"voice" in project.json once; all TTS calls pick it up automatically.
Running TTS
uv run --project scripts/tts scripts/tts/speak.py --text "<narration>" --out jobs/<slug>/audio/<sceneId> --scene <sceneId> --voice <project.voice>
(If uv isn't on PATH, substitute the full path: & $env:UV run --project scripts/tts ....)
Writes audio/<sceneId>.wav + .timing.json and prints {"durationMs":...,"words":...} on stdout. Check durationMs against the scene's word budget; if a scene runs >15% long, tighten the line and regenerate. First run downloads the ~300MB model — be patient.
Caption authoring step (after TTS)
Captions need timing.json word timestamps — this step cannot happen before TTS.
For each scene with captions planned:
- Read
jobs/<slug>/audio/<sceneId>.timing.json (the words[] array: { word, startMs, endMs }).
- Find the
startMs of each keyword you want to caption.
- Set
atMs in the storyboard caption to that startMs (±100ms for feel).
- Ensure consecutive captions don't overlap:
atMs[i+1] ≥ atMs[i] + durationMs[i].
- Edit
storyboard.json to fill the previously-empty captions arrays.
Only then run Stage 5 (capture) and Stage 6 (compose).
Stage 5 — Capture
For each clip/still scene:
npx tsx scripts/capture.ts jobs/<slug>/playbooks/<scene>.yaml <baseUrl> jobs/<slug>/footage/<sceneId> --profile jobs/<slug>/browser-profile
The out dir MUST be footage/<sceneId> — compose joins footage to scenes by id. If a step fails, the error names the step and the scene's footage dir gets failure-step-<i>.png + failure-step-<i>.aria.yml + a partial events.json — view them, fix the playbook target/done, re-run that scene only. Spot-check each clip.mp4 and shot png.
Stage 6 — Compose
npx tsx scripts/compose.ts <slug> → render/preview.mp4 (HALF resolution, narration only — music joins only at --final; use npm run studio when iterating on look rather than content).
- Self-critique against BOTH checklists in
references/demo-playbook.md (scriptwriting + visual): extract stills with ffmpeg -i jobs/<slug>/render/preview.mp4 -vf fps=1/10 jobs/<slug>/render/review-%02d.png (≈6-8 stills on a 60-90s video; adjust the rate to length), view them, and verify pacing variance, overlay word counts, zoom restraint, brand colors, narration sync (listen at least once). Fix and re-compose until clean.
GATE C
Send the user the preview path + your self-critique. Iterate on their notes (storyboard edits → re-run only affected stages).
Stage 7 — Deliver
npx tsx scripts/compose.ts <slug> --final → render/final.mp4 (full-res master, loudness-normalized to −14 LUFS / −1 dBTP; if project.musicPath is set, music is mixed in with true sidechain ducking under the narration) + render/captions.srt. Summarize what was produced and where.
Rules
- Never invent UI copy, labels, or data — only what you saw in snapshots/screenshots.
- Never put credentials in artifacts. Profile dirs and jobs/ are gitignored.
- Re-running a stage only regenerates its own artifacts; downstream stages must be re-run after edits.
- If capture quality looks wrong (blur, clipped UI), check viewport vs app responsive breakpoints before blaming the encoder.
- Playbook scene/shot names and storyboard scene ids must be lowercase-kebab (
/^[a-z0-9-]+$/), and each playbook's scene: must equal its storyboard scene id.
- Transition overlap padding is automatic (compose pads non-cut scenes by the transition length) — don't hand-pad narration.