agentsclimarketplace

Demo director

Skill baldiga/headless-studio/skills/demo-director

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.From its SKILL.md

Install
npx -y skills add baldiga/headless-studio --skill demo-director

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 11 stars11 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

11.8 KB, as published. Nobody here has run it

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

  1. 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).
  2. 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.
  3. Create jobs/<slug>/project.json matching ProjectSchema (see src/schemas.ts). Slug: lowercase-kebab from app name.

Stage 1 — Explore

  1. 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.
  2. Run npx tsx scripts/explore.ts <url> jobs/<slug>/discovery --profile jobs/<slug>/browser-profile --max 15.
  3. 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).
  4. Read discovery/raw-screens.json; view each screenshot; read each ariaSnapshot.
  5. 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

  1. Run npx tsx scripts/brand.ts <url> jobs/<slug>/brand.
  2. 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

  1. 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.
  2. 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.
  1. 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.
  2. 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.
  3. 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 spellingnarration respellingrationale
ZOOQZukeTTS would otherwise say "Zoo-cue" or "Zok"
JSONJasonTTS says "Jay-son" for the respelling; Jayson → natural
SQLsequelstandard dev pronunciation
APIA-P-Iread letter by letter; no respelling needed (TTS handles it)
MCPM-C-Psame — 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:

  1. Read jobs/<slug>/audio/<sceneId>.timing.json (the words[] array: { word, startMs, endMs }).
  2. Find the startMs of each keyword you want to caption.
  3. Set atMs in the storyboard caption to that startMs (±100ms for feel).
  4. Ensure consecutive captions don't overlap: atMs[i+1] ≥ atMs[i] + durationMs[i].
  5. 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

  1. 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).
  2. 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.

Keep looking

Skills are one crate of 626,474. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.