Produce video
Agent skills that turn any web app into a polished, narrated demo video — Playwright capture, smart zooms, ElevenLabs voiceover, Whisper sync and burned captions.
npx -y skills add AlexAnsart/demo-studio --skill produce-videoAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 11 days oldThe repository was created 11 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 2 stars2 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.
What its author says it does
Copied from the file, not written here
End-to-end production of a narrated, captioned demo video for any web app — chains narration generation (narrate), silent Playwright recording (film-demo, smart zooms + guardrails), and narration sync with burned captions (sync-narration) into one final .mp4. Use when the user asks to produce, make, or film a demo/product/tutorial video end to end, "with voice and subtitles", or "from A to Z".
SKILL.md
12.3 KB, as published. Nobody here has run it
Produce a demo video, A → Z
One request in → one finished .mp4 out (narrated + captioned if narration is
enabled, silent otherwise). This skill is the orchestrator: it owns the
chaining logic and the order of operations. The three stage skills own their
domain detail and remain the source of truth for it.
MANDATORY — read all three stage skills before starting (this file only adds what happens between them; skipping them is how quality gets lost):
| Stage | Skill to read | Owns |
|---|---|---|
| Voice | narrate | ElevenLabs voice/model defaults, script script-writing rules, credits |
| Silent video | film-demo (+ its reference.md) | Camera model (focus/wide), cursor, speed-ups, guardrails, inspect/verify loop |
| Sync + captions | sync-narration | Whisper alignment, segmentTargets, gaps, pre/post-roll, caption burn, ffmpeg gotchas |
If this is the first time this skill runs in a project, run demo-setup
first (or check that demo.config.json already exists at the project root) —
every stage below reads it for the base URL, output directory, narration
provider, and style preset. For scaffold patterns (login off-camera, beat plan,
compose caps), read demo-setup/examples/ and demo-setup/references/.
Pipeline
Request (what the app does, what to say, output path)
│
├─ 0. SCRIPT write demos/<slug>/script.md (beats + narration text)
├─ 1. VOICE script → narration.mp3 (narrate) — skip if narration.provider = "none"
├─ 2. ALIGN mp3 → aligned.json (whisper — sentence durations) — skip if no narration
├─ 3. RECORD beats → <slug>-no-sound.mp4 (film-demo, holds sized from step 2)
├─ 4. SYNC mp3 + no-sound → <slug>.mp4 (sync-narration, burned captions) — skip if no narration
└─ 5. QA frames + audio tail + output path → deliver
Voice comes first, always (when narration is enabled). Align the mp3 before recording so each sentence's exact duration sizes the on-screen holds. After that, the video adapts to the voice, not the other way around.
Silent mode: if demo.config.json → narration.provider is "none"
(the default — no API key required), skip stages 1/2/4 entirely: film-demo
records straight to the final output, using its own idle-gap speed-up (no
narration-driven holds to protect).
Artifacts (in demo.config.json → output.dir, default demos/<slug>/):
demos/<slug>/script.md the beat + narration script (source of truth for this run)
demos/<slug>/narration.mp3 narration (never retouched after ✓) — narrated mode only
demos/<slug>/no-sound.mp4 silent recording (keep for re-syncs) — narrated mode only
demos/<slug>/final.mp4 FINAL deliverable
Checklist
Production progress:
- [ ] 0. Confirm demo.config.json exists (else run demo-setup); write demos/<slug>/script.md
- [ ] 1. Generate narration.mp3 (narrate, exact filename) + ffprobe check — skip if silent mode
- [ ] 2. Align mp3 (align-narration.py) → per-sentence durations table — skip if silent mode
- [ ] 3. Write beat plan: sentence ↔ beat ↔ min on-screen hold — skip if silent mode
- [ ] 4. Record silent video with film-demo (narration-aware config below if narrated)
- [ ] 5. film-demo gates: guardrails alerts resolved, inspect frames read, verify pass
- [ ] 6. Copy final.mp4 → no-sound.mp4 (narrated mode only)
- [ ] 7. Sync: contact sheet → targets → cues.txt → plan → validate → render — skip if silent mode
- [ ] 8. QA gates (below) on the final mp4
- [ ] 9. Deliver: absolute path + duration + justified alerts
Stage 0 — Write the script
Write demos/<slug>/script.md with two parallel tracks, beat order = narration
order:
# <Video title>
## Say (skip this section entirely for silent mode)
1. "Here's how it works."
2. "Type your task, hit add, and watch it appear instantly."
3. "That's it — no page reload, no setup."
## Show
1. Open the app, wait for it to settle.
2. Focus the task input.
3. Type "Ship the v1 release".
4. Click Add — expect the new item in the list.
5. Wide — let the viewer see the list update.
6. Focus the new list item, hold ~2s (the payoff).
7. Wide, hold 2.5s, stop.
Keep total narration to ~150 words/minute of planned video length (a 60s video ≈ 150 words). If the user gave you the app's flow but no narration text and narration is enabled, draft the Say lines yourself from what the Show beats actually do — never invent behavior the app doesn't have.
Stage 1 — Voice (narrated mode only)
Follow narrate exactly. Use the current Say text from script.md:
python <narrate-skill-dir>/scripts/generate_narration.py \
--out demos/<slug>/narration.mp3 \
--text-file /tmp/say.txt
Gate: ffprobe duration > 0 and plausible (~150 words/min). Narration costs
API credits — if the mp3 already exists and Say hasn't changed, reuse it; any
Say edit → regenerate.
Stage 2 — Align (before recording, not after)
pip install -r <sync-narration-skill-dir>/scripts/requirements.txt # once
python <sync-narration-skill-dir>/scripts/align-narration.py \
demos/<slug>/narration.mp3 \
--script-file /tmp/say.txt --out /tmp/aligned.json
From sentences[], build the beat plan and put it as a comment block at
the top of run.mjs:
// BEAT PLAN (from aligned.json)
// # sentence (first words…) dur beat min hold
// 1 "Here's how it works…" 2.1s app loads 3s
// 2 "Type your task…" 4.8s type + add 6s
Min hold = sentence duration + ~1.2s. The sync stage can stretch silence between sentences, but it can never make a beat that left the screen too early stay longer — an undersized hold forces narration to spill over the next beat.
Stage 3 — Record silent (film-demo, narration-aware when applicable)
Follow the whole film-demo workflow (beat script, camera plan, guardrails,
inspect, verify). Differences specific to narrated videos:
- Zero on-screen text. No overlays or caption cards in
run.mjs— captions are burned at sync time from the Say text. - Holds are narration-driven. After each beat's action settles,
pause()until the beat's min hold (beat plan above) is met. These pauses are deliberate, so: - Disable the generic idle compression — it would eat exactly those holds:
node <film-demo-skill-dir>/scripts/compose.mjs <renderDir> --speedup idleGapMin=9999,toolWaitMax=2.5,preToolWaitMax=2,tailHold=4
Keep lead/tail trims. Do not log an early open() event before the
first beat — lead trim anchors on the first timeline event; an early
open() blocks trimming dead startup time and forces a huge head-pad at
sync. Preload the app off-camera (page.goto + settle) before
startFilmRecording; the first filmed event must be the first beat.
4. Guardrail justifications shift: inactivity and zoom-too-long
alerts during a planned narration hold are expected — justify them
against the beat plan (name the sentence). cursor-offscreen and
record-alert (error text, click outside zone) are never justifiable,
same as always.
5. Camera ↔ narration: when a clause describes one element, focus() on
it for that hold, then wide(). The pointer must be settled on the
element for the clause's whole window — sync (stage 4) cannot fix a
cursor that was still traveling.
6. Opening + payoff: await settle(page) before the first beat (fully
loaded, no spinner, no leftover state from a previous take). End on the
RESULT: focus() the changed element, hold ≥ 4s, then stop.
7. Fail fast, restart from zero. Use expect: on every click with a
knowable outcome. Any error, repeated click, or unexpected state = kill
the take, fix, fresh render.
8. Duration sanity: silent video ≥ padded-audio estimate (sum of sentence
durations + planned inter-sentence gaps) and ≤ 2min.
For silent mode (no narration), skip 2/3/4 above — just follow film-demo
as-is; its own speed-up defaults already keep pacing tight.
Copy the accepted render's final.mp4 to no-sound.mp4 (narrated mode only).
Stage 4 — Sync + captions (narrated mode only)
Follow sync-narration end to end. Chaining shortcuts already earned:
aligned.jsonfrom stage 2 is reused as-is.segmentTargets: read them off the recording's timeline.json (after compose its timestamps are remapped to final-video time, so each beat's settle moment is exact) plus the camera plan compose prints — still generate the contact sheet to confirm frames match. If the plan builder reports merged sentences (no usable silence between them), pass one target per merged block.cues.txt: split the Say text into caption cues at clause breaks — ≤ 60 chars preferred, 80 hard max.- Keep defaults (
--tail-pad 1.0; pre/post-roll and min-silence are word-safe and silence-aware out of the box). --validate-onlyfirst, always; then render to the requested output path.leadGap/--head-pad= composed timestamp of the first spoken beat, not the raw pre-recording offset. Values > 5s always mean wrong sync (spot-check fails): trim dead opening footage in Stage 3, or extend Say so narration covers long waits.- Spot-check is mandatory:
node <sync-narration-skill-dir>/scripts/spot-check.mjs plan.jsonand read EVERY frame.
Stage 5 — QA gates (all must pass before delivering)
- Final duration 15s-2min; ends on the result screen (payoff readable ≥ 3s), not mid-action
- Narrated mode:
spot-check.mjsrun on the final render and EVERY frame read - Scrub last 1s of audio: final word decays naturally (post-roll not clipped) — narrated mode
- Narrated mode:
plan.jsonleadGap≤ 5s; opening frames show motion before first spoken word -
verify.mjspasses on the silent/final master - No tight
focus()zoom held through a loading state or unscripted UI - No narration while the cursor travels — narrated mode
-
capture-stats.jsonhealthy —compose.mjs/verify.mjs/ sync hard-fail if fps < 25 or p90 > 80ms; else re-record - Final file sits at the exact requested output path
- Silent master +
.mp3kept alongside — narrated mode
What to redo when something is wrong
| Symptom | Redo |
|---|---|
| Wrong words / wrong voice | Stage 1 (only if Say changed or generation was wrong) → then 2, 4 |
| Beat leaves screen before its clause ends | Stage 3 (raise that hold) → 4. Voice is fixed — never speed it up |
| Caption early/late, clipped onset/tail | Stage 4 only (targets, pre/post-roll) — no re-record needed |
| Cursor moving during a clause | Stage 3 (move the action before the hold) → 4 |
| Video too long | Stage 3 (cut a beat — never rush actions, never trim a mapped payoff) |
| Nothing happens / mute audio first 10-20s | Stage 3 (preload off-camera; first event = first beat) → Stage 4 (--head-pad = composed queue time, ≤ 5s) |
| Draft Say too short for the video | Expand Say (stage 0), regenerate voice → align → record/sync |
Deliverable message (required)
## Video ready
**Final:** `<absolute path to final.mp4>`
- Duration: <X>s
- Mode: <narrated (N sentences, voice) | silent>
- Silent master: `<no-sound.mp4>` (narrated mode) · render `<NNN>` · capture <fps>fps
- Guardrail alerts justified: <list or "none">
- Anomalies: <list or "none">
Anomalies are mandatory to report. Anything that did not behave as expected — a feature that errored and needed a retry/workaround, a beat dropped because the UI never reached the scripted state, a flaky endpoint, a take restarted — goes in the final message, even when the delivered video looks fine. Never let the user discover a bug by watching the video.