Make a narrated demo video
You write a scenes file: each scene is a piece of narration plus the clicks that happen while it plays. The scripts turn the narration into speech, drive the app in a headless Chromium at a fixed 1600×900 while capturing the screen, and build an MP4 with the voice-over and burned-in captions. A visible cursor, click ripples, highlight rings and title cards are added on top of the page.
All commands below use PY, which means python on Windows and python3 on macOS, and DV, which means
"${CLAUDE_SKILL_DIR}/scripts/demo_video.py". Every command prints plain status lines (SCENE …, RECORDED …,
CHECK OK, ERROR …); read them rather than guessing.
1. Set up (once per machine)
PY "${CLAUDE_SKILL_DIR}/scripts/setup_env.py"
It creates a private environment in ~/.claude-demo-video with Playwright, Chromium and edge-tts, then checks ffmpeg.
It ends with READY, or with exactly what is missing. ffmpeg is a system tool: ask the user before installing it
(winget install --id Gyan.FFmpeg -e on Windows, brew install ffmpeg on macOS). The scripts also look in the usual install
folders (WinGet, Chocolatey, Scoop, Homebrew). If ffmpeg is somewhere else, set DEMO_VIDEO_FFMPEG_DIR to its folder
for every command.
2. Agree the video with the user before writing anything
Ask, or confirm from context:
- Who will watch it and why (a reviewer approving a feature, a client, new staff). That sets the tone and what to explain.
- What to show, in what order. Propose a scene list with one line each and get a yes. Aim for 3 to 8 minutes.
- Voice. The default is a natural English neural voice (edge-tts). Tell the user the narration text is sent to
Microsoft's speech service to generate the audio. It should contain feature explanations only, never data. The
alternative is the offline system voice (
--offline): nothing leaves the machine, but it sounds robotic. - Data. Record only demo data: fictional people,
@example.comemails, clearly fake projects. Never record real client or employee records, and never point the recorder at production.
3. Prepare the app
- The app must be running and reachable from this machine, usually a local build.
- If screens need data, write a seed script and a matching cleanup script for the demo data, and run the seed. Tag
demo rows (for example IDs starting
DEMO-) so cleanup can't touch anything else. - Make sure outgoing email and notifications are off, or go only to fake addresses, because the walkthrough clicks real buttons (submit, approve…).
- Scenes that change data (approve, delete) can only be recorded once per seed. Re-seed before every retake.
4. Write the scenes file
Create it in the user's project, for example demo-videos/<name>.py, never inside the skill folder. Start from
${CLAUDE_SKILL_DIR}/reference/scenes-template.py. For a full real example, see
${CLAUDE_SKILL_DIR}/reference/zentrio-timesheet-example.py, which made a 6-minute approval video.
CONFIG = {'url': 'https://localhost:7101/MyTimesheet', 'ready': '#tsCards .tsw-card', 'sign_in': True}
async def week(r):
await r.click('.tsw-row:has-text("Approval screen")')
await r.spot('.tsw-panel', 3)
SCENES = [
('week', "Click a task row and the log panel opens right under it.", week),
]
CONFIG['url']is the first page.readyis a selector that is only visible once that page is loaded and signed in. It's how the recorder knows sign-in worked.- Narration: one to three short spoken sentences per scene. Explain why something matters, not just what is on screen. Write numbers and symbols the way they should be said ("two point three zero"). Don't put data in narration.
- Actions run while their narration plays. Keep them shorter than the narration; the recorder waits for the narration to finish before starting the next scene.
Scene helpers (r is the recorder; anything else is available as r.page, the Playwright page):
| Helper | Does |
|---|---|
await r.goto('/path', wait_for=sel) | open a page (relative to CONFIG['url']) |
await r.click(target, dx=.5, dy=.5) | move the visible cursor there and click; dx/dy pick the point inside the element |
await r.move_to(target) | move the cursor without clicking |
await r.type(target, 'text') | click a field and type into it visibly |
await r.select(target, value) | choose a dropdown option |
await r.press('ControlOrMeta+K') | keyboard shortcut (Ctrl on Windows, Cmd on macOS) |
await r.spot(target, seconds, pad) | orange highlight ring |
await r.card(kicker, title, lines, bullets) / await r.uncard() | full-screen title card (intro, summary, outro) |
await r.confirm_click(target, show=4) | for buttons that call confirm(): shows a look-alike dialog, then accepts |
await r.download(target) | click a download and return the saved file's path |
await r.show_file(path) | show a local file, for example an HTML preview you made of a downloaded report |
await r.scroll_top(), await r.scroll_x(selector, px) | smooth scrolling |
await r.clear_toasts(selector) | remove notification toasts that cover buttons |
await r.wait(seconds) | pause |
target is a CSS or Playwright selector string ('button:has-text("Approve")') or a Locator
(r.page.locator('tr', has_text='Aarav').first).
5. Make the narration, record, and review
PY DV voice demo-videos/<name>.py # audio per scene; prints each scene's length
PY DV record demo-videos/<name>.py # drives the app and captures frames
PY DV stills demo-videos/<name>.py # two stills per scene in <name>-video/stills/
Sign-in. With 'sign_in': True, if the app isn't signed in, a browser window opens and the recorder prints
SIGN_IN_NEEDED. Tell the user to sign in in that window. Never type credentials or ask for passwords. The session
is saved in <name>-video/.profile so retakes don't need another sign-in.
Review every still before building, by reading the images:
- right page, panel open, cursor where the narration says;
- nothing covering the action (toasts, cookie banners, a profile menu);
- the picture isn't stretched or cut off;
- no real personal data on screen.
A SCENE_ERROR line or a wrong still means fixing that scene function and recording again (re-seed first if the
scenes changed data). reference/lessons.md lists the failures seen so far and their fixes. Read it when something
looks off.
6. Build, check and hand over
PY DV build demo-videos/<name>.py [--out "path/Name.mp4"]
PY DV check "path/Name.mp4" --size 1600x900
run does voice, record, stills, build and check in one go, once the scenes are known to work. Tell the user the
video's path and length, and what it shows. Don't upload or share it anywhere unless they ask.
7. Clean up
PY DV clean demo-videos/<name>.pydeletes the saved sign-in. Always do this when finished, because it holds a real session.--allalso removes frames, stills and audio.- Run the demo-data cleanup script, and stop any app processes you started.
Platform notes
- macOS: use
python3. Captions are drawn by Chromium and laid over the video, so they look the same everywhere and don't need an ffmpeg built with libass (Homebrew's isn't). Recording is headless, so Retina screens don't change the video size. --headedrecords in a visible window. Only use it when an app refuses headless browsers, and ask the user not to touch or resize the window while it records.DEMO_VIDEO_HOMEmoves the private environment;DEMO_VIDEO_FFMPEG_DIRpoints at an ffmpeg folder.