First, check if a repo-scoped version exists in the current project:
- If
.claude/skills/demo-video/SKILL.md exists (Glob) → read and follow it instead of this file.
- If
.claude/commands/demo-video.md exists (Glob) → read and follow it instead (legacy /jacked-setup output).
Otherwise follow the engine below.
/demo-video is /qa-video's polished cousin. Both record the browser, but the intent is
opposite: /qa-video captures a QA session as bug/regression evidence for developers;
/demo-video produces a clean, narrated teaching video for end users — staged data,
deliberate pacing, captions/voiceover, and a reusable narration script committed to the repo
so the video can be regenerated whenever the UI changes. Reach for this when the goal is to
explain a feature, not to prove a bug.
Tip: MCP-based browser tools (Chrome DevTools MCP, Playwright MCP) require no bash approval and work instantly. For voiceover + muxing you'll want ffmpeg; macOS say provides offline narration with zero external accounts.
You are producing a clean, narrated walkthrough video that teaches a feature for end-user
documentation — not bug evidence (that's /qa-video). The output is something you'd embed in
docs, a README, a release note, or onboarding: staged data, deliberate pacing, captions and/or
voiceover, and a narration script committed to the repo so the whole thing regenerates when
the UI changes.
Step 0: Scope the demo
- What to demo: if
$ARGUMENTS names a feature/flow ("the new export wizard", "login →
first dashboard"), use it. Otherwise infer from what changed (git diff/recent commits) or
ask the user what they want taught.
- Audience & goal: a new user learning this capability. One video = one coherent story
(don't cram the whole app into one clip; suggest separate clips per feature).
- Where it lands: ask or default to
docs/videos/ (the MP4 + captions) plus the narration
script alongside it. Note the embed target (README, docs site, release note).
Step 1: Write the narration script — the SOURCE OF TRUTH
Everything is driven by a narration script you write FIRST and commit. It's an ordered list of
segments, each with an id, the text to be spoken/captioned, and a type:
narrate — talk over the current screen (no interaction; e.g. an intro or a "notice that…").
action — the browser actually does something (click, type, navigate) while narrating.
highlight — call attention to a specific element (zoom/outline) while narrating.
Save it as docs/videos/<slug>.narration.json (committed — diff it like code, regenerate on
release):
[
{ "id": "intro", "type": "narrate", "text": "Welcome. This shows how to export a report in three steps." },
{ "id": "open-reports", "type": "action", "text": "From the dashboard, open the Reports tab.", "do": "click Reports nav" },
{ "id": "pick-range", "type": "highlight", "text": "Pick a date range — the last 30 days is the default.", "focus": "the date-range picker" },
{ "id": "export", "type": "action", "text": "Click Export, choose CSV, and your download starts.", "do": "click Export, choose CSV" },
{ "id": "outro", "type": "narrate", "text": "That's it — exports run in the background and email you when ready." }
]
Keep each segment to one idea and ~1–2 sentences. Order them as the real flow. This script is
what makes the video reproducible: edit the text or steps, re-run, get a fresh video.
Step 2: Detect the browser tool + recording path
Detect exactly as /qa-video Step 0–1 (read ~/.claude/skills/qa-video/SKILL.md if needed):
- Path P — Playwright MCP native video (preferred: true motion + chapter markers; needs
--caps=devtools). On Playwright, also start a trace (Path T) — it's free alongside.
- Path F — frame-stitch fallback for any other tool (Chrome DevTools MCP / agent-browser)
→ assemble frames into MP4 (ffmpeg) or animated GIF (ImageMagick, zero-install).
Announce the chosen path. For a demo (vs QA evidence), Playwright native video is strongly
preferred — smoother motion reads better for teaching.
Step 3: Stage a clean set — this is a demo, not a test
A teaching video must look intentional — and the demo performs actions (clicks, exports,
saves), so it must run on an ISOLATED instance, never production:
- Record against an isolated instance. Best available, in order: (1) a PR / preview /
ephemeral deploy if one exists (the PR's "View deployment" link, a Vercel/Netlify/Cloudflare
preview, a Railway/Heroku review app); (2) spin it up locally — dev server + a local DB
with seed/sample data (
docker compose up, manage.py runserver, npm run dev/pnpm dev,
.env.local), pointed at localhost; (3) a disposable staging the user confirms. Keep the
data clean and staged — this is a polished demo, not a stress test.
- Confirm non-prod before any write — fail closed. Before the first save/export/submit, you
are READ-ONLY until you confirm ALL of: (a) the host is local or the EXACT preview URL (never
the production domain); (b) the running PROCESS is on a local/throwaway DB — read it from the
live process (
ps eww <pid>, /proc/<pid>/environ) or an app endpoint, NOT a dotfile (a
preview/remote URL alone does NOT prove the DB); (c) email/payment/webhook integrations are
sandboxed or disabled (a local DB won't stop a real send/charge); (d) you started it — the
server is one YOU spun up this session or the verified preview env, not one you merely found
listening. If you can't prove ALL of it, do NOT perform writes — narrate a read-only walkthrough
instead. When unsure, it's production. A URL passed in or auto-detected clears only the Host
check, never the rest.
This gate governs EVERY write in the skill — logging in as the demo persona (next bullet),
every action/highlight segment that submits/saves/exports (Step 4), and any re-run. On an
unproven/production target: navigate + observe only — no login, no writes.
- Log in as the demo persona with representative, clean data (not empty, not debug
junk, no real PII — use seed/sample data; if the screen would show real customer data, switch
to a demo account or sanitize).
- Set a clean, doc-friendly viewport — 1280×720 or 1920×1080 (16:9 embeds well).
- Remove noise: dismiss cookie banners, close debug overlays/devtools panels, hide any
"localhost"/staging banners, silence notifications.
- Pre-navigate to the true starting point so segment 1 opens on the intended screen.
Step 4: Record, timed to the narration
Start recording BEFORE the first navigation (trace/video only capture what happens after start).
Then walk the segments deliberately — a teaching pace, slower than a QA run:
- Start the video (and trace on Playwright). Path F: capture frames per sub-step into
docs/videos/<slug>/frame_####.png.
- For each segment, in order:
narrate → hold on the current screen for the segment's beat (≥ the spoken duration; see
Step 5). No interaction.
action → perform the interaction with visible, unhurried motion; insert a ~0.8–1.5s
settle after each click/type so the viewer can follow. Use browser_snapshot to locate
elements by role/label.
highlight → draw attention to the focus element: scroll it into view and, if supported,
zoom or outline it (Playwright: evaluate a temporary outline/box-shadow on the element,
or use a known highlight API; otherwise center + a brief pause). Remove the outline after.
- Stop the video (and trace). Record the saved paths.
Step 5: Voiceover (optional but recommended) + caption timing
The trick that syncs everything: generate per-segment audio, MEASURE each clip's duration, and
time the browser holds to those durations.
- Offline default — macOS
say: per segment, say -v Samantha -o seg-<id>.aiff "<text>",
then read its duration (ffprobe -i seg-<id>.aiff -show_entries format=duration -of csv=p=0)
into a durations.json. Drive each segment's on-screen hold for at least that long.
- Higher quality (only if the user has it): an external TTS (e.g. ElevenLabs) — same
segment→audio→duration flow. Don't assume an API key; ask, and fall back to
say.
- Captions: when voiceover WAS generated, derive caption timings from
durations.json
(the measured per-segment lengths) so the text tracks the audio exactly. Only on the
no-voiceover path use a fixed reading-speed estimate (~15 chars/sec). Either way, write a
.vtt/.srt from the segment text and ship the sidecar or burn it in (Step 6 subtitles=).
Step 6: Assemble the final video
- Path F frames → video:
ffmpeg -y -framerate <fps> -pattern_type glob -i 'frame_*.png' -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv420p" -c:v libx264 -movflags +faststart out.mp4 (or ImageMagick GIF if no ffmpeg).
- Build one narration track from the per-segment clips (in
narration.json order) with a
short silence pad between segments, then mux it onto the silent recording:# 0.4s silence pad, then a concat list: seg-intro.aiff, gap.aiff, seg-open-reports.aiff, …
ffmpeg -y -f lavfi -t 0.4 -i anullsrc=r=44100:cl=stereo gap.aiff
ffmpeg -y -f concat -safe 0 -i list.txt -c:a aac -b:a 192k narration.aac # list.txt = file 'seg-*.aiff' / file 'gap.aiff' lines
# mux audio onto the silent video (<silent-video> = the .webm from Path P, or out.mp4 from Path F)
ffmpeg -y -i <silent-video> -i narration.aac -c:v libx264 -c:a aac -b:a 192k -shortest demo.mp4
-shortest trims to the shorter track so a slight timing drift never leaves frozen frames or
trailing silence.
- Captions: ship
demo.vtt next to demo.mp4 (docs players show it), or burn in with
-vf "subtitles=demo.vtt" if the embed target can't show a sidecar.
- Polish (optional): a 1–2s title card intro/outro (ffmpeg or a stitched frame). Keep it
short — teaching, not advertising.
Step 7: Deliver into the docs
- Write
demo.mp4 (+ demo.vtt + the .narration.json) to the chosen docs location.
- Provide the embed snippet for the target (Markdown image/link, an HTML
<video controls>
with the .vtt track, or the docs-site shortcode).
- State that the video is regenerable: the narration script is committed, so on a UI change
you re-run
/demo-video to produce an updated clip — no manual re-recording.
Step 8: Verify the artifact is real
Don't report success on a file existing — prove it:
ffprobe demo.mp4 → duration > 0, a video stream AND (if voiceover) an audio stream,
sane resolution. (magick identify for a GIF: frame count > 1.)
- File size is non-trivial; spot-check that the captions/narration text matches what's on screen
at that timestamp.
If duration is 0, audio is missing when it should be present, or the codec is junk → it failed;
fix and re-run, don't hand over a broken clip.
This command teaches; for bug/regression evidence use /qa-video; for a fast issue list
use /qa; for a pixel-and-function audit use /aesthetic-dogfood-audit.
1---2name: demo-video3description: Use to produce a polished, narrated product DEMO / walkthrough / how-to video for documentation and education — teaching a feature or capability to end users, to embed in docs, a README, a release note, or onboarding. NOT for bug/regression evidence (that's /qa-video). Triggers include "make a demo video", "record a walkthrough", "feature walkthrough for the docs", "how-to video", "show this feature off", "onboarding video", "educational clip for new users".4---56First, check if a repo-scoped version exists in the current project:71. If `.claude/skills/demo-video/SKILL.md` exists (Glob) → read and follow it instead of this file.82. If `.claude/commands/demo-video.md` exists (Glob) → read and follow it instead (legacy `/jacked-setup` output).9Otherwise follow the engine below.1011`/demo-video` is `/qa-video`'s polished cousin. Both record the browser, but the intent is12opposite: `/qa-video` captures a QA session as **bug/regression evidence** for developers;13`/demo-video` produces a **clean, narrated teaching video** for end users — staged data,14deliberate pacing, captions/voiceover, and a reusable narration script committed to the repo15so the video can be regenerated whenever the UI changes. Reach for this when the goal is to16*explain a feature*, not to *prove a bug*.1718<!-- ENGINE -->1920> **Tip:** MCP-based browser tools (Chrome DevTools MCP, Playwright MCP) require no bash approval and work instantly. For voiceover + muxing you'll want `ffmpeg`; macOS `say` provides offline narration with zero external accounts.2122You are producing a **clean, narrated walkthrough video that teaches a feature** for end-user23documentation — not bug evidence (that's `/qa-video`). The output is something you'd embed in24docs, a README, a release note, or onboarding: staged data, deliberate pacing, captions and/or25voiceover, and a **narration script committed to the repo** so the whole thing regenerates when26the UI changes.2728## Step 0: Scope the demo2930- **What to demo:** if `$ARGUMENTS` names a feature/flow ("the new export wizard", "login →31 first dashboard"), use it. Otherwise infer from what changed (`git diff`/recent commits) or32 ask the user what they want taught.33- **Audience & goal:** a new user learning this capability. One video = one coherent story34 (don't cram the whole app into one clip; suggest separate clips per feature).35- **Where it lands:** ask or default to `docs/videos/` (the MP4 + captions) plus the narration36 script alongside it. Note the embed target (README, docs site, release note).3738## Step 1: Write the narration script — the SOURCE OF TRUTH3940Everything is driven by a narration script you write FIRST and commit. It's an ordered list of41**segments**, each with an `id`, the `text` to be spoken/captioned, and a `type`:42- `narrate` — talk over the current screen (no interaction; e.g. an intro or a "notice that…").43- `action` — the browser actually does something (click, type, navigate) while narrating.44- `highlight` — call attention to a specific element (zoom/outline) while narrating.4546Save it as `docs/videos/<slug>.narration.json` (committed — diff it like code, regenerate on47release):48```json49[50 { "id": "intro", "type": "narrate", "text": "Welcome. This shows how to export a report in three steps." },51 { "id": "open-reports", "type": "action", "text": "From the dashboard, open the Reports tab.", "do": "click Reports nav" },52 { "id": "pick-range", "type": "highlight", "text": "Pick a date range — the last 30 days is the default.", "focus": "the date-range picker" },53 { "id": "export", "type": "action", "text": "Click Export, choose CSV, and your download starts.", "do": "click Export, choose CSV" },54 { "id": "outro", "type": "narrate", "text": "That's it — exports run in the background and email you when ready." }55]56```57Keep each segment to one idea and ~1–2 sentences. Order them as the real flow. This script is58what makes the video reproducible: edit the text or steps, re-run, get a fresh video.5960## Step 2: Detect the browser tool + recording path6162Detect exactly as `/qa-video` Step 0–1 (read `~/.claude/skills/qa-video/SKILL.md` if needed):63- **Path P — Playwright MCP native video** (preferred: true motion + chapter markers; needs64 `--caps=devtools`). On Playwright, also start a **trace** (Path T) — it's free alongside.65- **Path F — frame-stitch fallback** for any other tool (Chrome DevTools MCP / agent-browser)66 → assemble frames into MP4 (ffmpeg) or animated GIF (ImageMagick, zero-install).67Announce the chosen path. For a *demo* (vs QA evidence), Playwright native video is strongly68preferred — smoother motion reads better for teaching.6970## Step 3: Stage a clean set — this is a demo, not a test7172A teaching video must look intentional — and the demo *performs actions* (clicks, exports,73saves), so it must run on an ISOLATED instance, never production:74- **Record against an isolated instance.** Best available, in order: (1) a **PR / preview /75 ephemeral deploy** if one exists (the PR's "View deployment" link, a Vercel/Netlify/Cloudflare76 preview, a Railway/Heroku review app); (2) **spin it up locally** — dev server + a local DB77 with seed/sample data (`docker compose up`, `manage.py runserver`, `npm run dev`/`pnpm dev`,78 `.env.local`), pointed at `localhost`; (3) a disposable staging the user confirms. Keep the79 data **clean and staged** — this is a polished demo, not a stress test.80- **Confirm non-prod before any write — fail closed.** Before the first save/export/submit, you81 are READ-ONLY until you confirm ALL of: (a) the host is local or the EXACT preview URL (never82 the production domain); (b) the running PROCESS is on a local/throwaway DB — read it from the83 live process (`ps eww <pid>`, `/proc/<pid>/environ`) or an app endpoint, NOT a dotfile (a84 preview/remote URL alone does NOT prove the DB); (c) email/payment/webhook integrations are85 sandboxed or disabled (a local DB won't stop a real send/charge); (d) **you started it** — the86 server is one YOU spun up this session or the verified preview env, not one you merely found87 listening. If you can't prove ALL of it, do NOT perform writes — narrate a read-only walkthrough88 instead. When unsure, it's production. A URL passed in or auto-detected clears only the Host89 check, never the rest.90 **This gate governs EVERY write in the skill** — logging in as the demo persona (next bullet),91 every `action`/`highlight` segment that submits/saves/exports (Step 4), and any re-run. On an92 unproven/production target: navigate + observe only — no login, no writes.93- Log in as the **demo persona** with **representative, clean data** (not empty, not debug94 junk, no real PII — use seed/sample data; if the screen would show real customer data, switch95 to a demo account or sanitize).96- Set a **clean, doc-friendly viewport** — 1280×720 or 1920×1080 (16:9 embeds well).97- **Remove noise:** dismiss cookie banners, close debug overlays/devtools panels, hide any98 "localhost"/staging banners, silence notifications.99- Pre-navigate to the true starting point so segment 1 opens on the intended screen.100101## Step 4: Record, timed to the narration102103Start recording BEFORE the first navigation (trace/video only capture what happens after start).104Then walk the segments **deliberately** — a teaching pace, slower than a QA run:1051. Start the video (and trace on Playwright). Path F: capture frames per sub-step into106 `docs/videos/<slug>/frame_####.png`.1072. For each segment, in order:108 - `narrate` → hold on the current screen for the segment's beat (≥ the spoken duration; see109 Step 5). No interaction.110 - `action` → perform the interaction with visible, unhurried motion; insert a ~0.8–1.5s111 settle after each click/type so the viewer can follow. Use `browser_snapshot` to locate112 elements by role/label.113 - `highlight` → draw attention to the focus element: scroll it into view and, if supported,114 zoom or outline it (Playwright: `evaluate` a temporary outline/box-shadow on the element,115 or use a known highlight API; otherwise center + a brief pause). Remove the outline after.1163. Stop the video (and trace). Record the saved paths.117118## Step 5: Voiceover (optional but recommended) + caption timing119120The trick that syncs everything: generate per-segment audio, MEASURE each clip's duration, and121time the browser holds to those durations.122123- **Offline default — macOS `say`:** per segment, `say -v Samantha -o seg-<id>.aiff "<text>"`,124 then read its duration (`ffprobe -i seg-<id>.aiff -show_entries format=duration -of csv=p=0`)125 into a `durations.json`. Drive each segment's on-screen hold for at least that long.126- **Higher quality (only if the user has it):** an external TTS (e.g. ElevenLabs) — same127 segment→audio→duration flow. Don't assume an API key; ask, and fall back to `say`.128- **Captions:** when voiceover WAS generated, derive caption timings from `durations.json`129 (the measured per-segment lengths) so the text tracks the audio exactly. Only on the130 no-voiceover path use a fixed reading-speed estimate (~15 chars/sec). Either way, write a131 `.vtt`/`.srt` from the segment text and ship the sidecar or burn it in (Step 6 `subtitles=`).132133## Step 6: Assemble the final video134135- **Path F frames → video:** `ffmpeg -y -framerate <fps> -pattern_type glob -i 'frame_*.png' -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv420p" -c:v libx264 -movflags +faststart out.mp4` (or ImageMagick GIF if no ffmpeg).136- **Build one narration track** from the per-segment clips (in `narration.json` order) with a137 short silence pad between segments, then mux it onto the silent recording:138 ```bash139 # 0.4s silence pad, then a concat list: seg-intro.aiff, gap.aiff, seg-open-reports.aiff, …140 ffmpeg -y -f lavfi -t 0.4 -i anullsrc=r=44100:cl=stereo gap.aiff141 ffmpeg -y -f concat -safe 0 -i list.txt -c:a aac -b:a 192k narration.aac # list.txt = file 'seg-*.aiff' / file 'gap.aiff' lines142 # mux audio onto the silent video (<silent-video> = the .webm from Path P, or out.mp4 from Path F)143 ffmpeg -y -i <silent-video> -i narration.aac -c:v libx264 -c:a aac -b:a 192k -shortest demo.mp4144 ```145 `-shortest` trims to the shorter track so a slight timing drift never leaves frozen frames or146 trailing silence.147- **Captions:** ship `demo.vtt` next to `demo.mp4` (docs players show it), or burn in with148 `-vf "subtitles=demo.vtt"` if the embed target can't show a sidecar.149- **Polish (optional):** a 1–2s title card intro/outro (ffmpeg or a stitched frame). Keep it150 short — teaching, not advertising.151152## Step 7: Deliver into the docs153154- Write `demo.mp4` (+ `demo.vtt` + the `.narration.json`) to the chosen docs location.155- Provide the **embed snippet** for the target (Markdown image/link, an HTML `<video controls>`156 with the `.vtt` track, or the docs-site shortcode).157- State that the video is **regenerable**: the narration script is committed, so on a UI change158 you re-run `/demo-video` to produce an updated clip — no manual re-recording.159160## Step 8: Verify the artifact is real161162Don't report success on a file existing — prove it:163- `ffprobe demo.mp4` → duration > 0, a **video stream AND (if voiceover) an audio stream**,164 sane resolution. (`magick identify` for a GIF: frame count > 1.)165- File size is non-trivial; spot-check that the captions/narration text matches what's on screen166 at that timestamp.167168If duration is 0, audio is missing when it should be present, or the codec is junk → it failed;169fix and re-run, don't hand over a broken clip.170171---172173This command **teaches**; for bug/regression evidence use `/qa-video`; for a fast issue list174use `/qa`; for a pixel-and-function audit use `/aesthetic-dogfood-audit`.