Recording product demo
End-to-end pipeline for producing a narrated product demo video from any repo with a web UI — the agent discovers and boots the app locally, preps demo data and auth, writes the narration and a scripted browser journey, records a time-locked Playwright take synced to an ElevenLabs voiceover, assembles a 1080p MP4 with branded title/agenda/end cards, and builds a publish kit (faststart MP4, 720p, poster, SRT captions, embed snippet, optional GCS/S3/YouTube upload). Use when the user says 'record a demo video', 'make a product demo', 'demo video for this app/repo', or wants to re-record an existing demo after UI changes.From its SKILL.md
npx -y skills add aictrl-dev/skills --skill recording-product-demoAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
3 things to look at
- reads credentialsReads from 1 credential source: `ELEVENLABS_API_KEY`.
- 1 stars1 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.
- runs commandsInstructs the agent to run 8 commands, including `npm i -D playwright && npx playwright install chromium` and 7 more.
SKILL.md
12.9 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it
Recording Product Demo
Produce a styled, narrated product demo video for any repo with a web UI — entirely from scripts, re-recordable in ~10 minutes of machine time once the narration is locked.
The model: the agent discovers once, then everything is code. On the first run you (the agent) work out how to boot the app, what to show, and what to say — and capture all of it into a committed demo/ directory in the host repo. Re-runs (after any UI change) are fully scripted: boot → record → assemble → publish, no improvisation.
Pipeline Overview
Phase 0 Discover & boot → agent reads the repo, writes demo/boot.sh + demo.config.json, boots, health-checks
Phase 1 Prep → demo data seeded, auth captured (login.cjs), pages probed (scrape-text.cjs)
Phase 2 Script → scene table + narration text (demo/segments.json)
Phase 3 Narration → ONE-SHOT TTS → STT word timestamps → split per scene (timeline.json)
Phase 4 Screen recording → one continuous Playwright take, time-locked to narration (demo/blocks.cjs)
Phase 5 Cards & assembly → branded title/agenda/end cards + ffmpeg assembly (assemble.cjs)
Phase 6 Publish → out/publish kit: faststart MP4, 720p, poster, captions.srt, embed snippet; optional upload
Resources in this skill:
| File | Purpose |
|---|---|
scripts/record-demo.cjs | The recorder FRAMEWORK (time-lock, cursor, glides, anonymisation engine) — loads the journey from demo/blocks.cjs |
scripts/tts-oneshot.cjs | One-shot ElevenLabs TTS + STT word timestamps, from demo/segments.json |
scripts/split-narration.py | Fuzzy-aligned split of the one-shot into per-scene clips; writes timeline.json |
scripts/assemble.cjs | Mux (t0-trim + upscale + narration) → cards concat → verification frame grid, all from metadata |
scripts/publish.cjs | Publish kit (faststart/720p/poster/captions.srt/embed) + optional GCS/S3/YouTube upload |
scripts/login.cjs | One-time interactive login (defeats Google's automation block), saves profile + storage state |
scripts/scrape-text.cjs | Dump rendered page text — find anchor strings, sync live numbers into narration |
templates/ | demo.config.example.json, blocks.example.cjs, segments.example.json, neutral title/agenda/end.html cards |
What the host repo ends up with (committed, except out/):
demo/
demo.config.json # the contract: app/auth/brand/voice/anonymize/record/publish
boot.sh # captured boot recipe — starts app + deps, waits until healthy
segments.json # narration, one entry per scene
blocks.cjs # the Playwright journey, one block per scene
cards/ # title/agenda/end HTML, branded from the templates
out/ # build artifacts — add to .gitignore
Prerequisites (check before starting, tell the user what's missing): ElevenLabs API key (ELEVENLABS_API_KEY — TTS and STT are both load-bearing; the word timestamps drive the scene split), ffmpeg + ffprobe, Node 18+ with Playwright + Chromium (npm i -D playwright && npx playwright install chromium in the host repo), Python 3.
Phase 0 — Discover & Boot (agent work, captured as code)
- Read the repo: README,
package.jsonscripts,docker-compose*.yml,Makefile,Procfile. Determine how to start the app and its dependencies locally, which port it serves, and what visible text proves it's up. - Write
demo/boot.sh: an idempotent script that starts everything (background-safe), then pollsapp.baseUrl + healthPathuntilreadyTextappears (with a timeout that fails loudly). This is the captured boot recipe — re-runs never re-derive it. - Write
demo/demo.config.jsonfromtemplates/demo.config.example.json. Fillapp,brand(name, primary color, andpronunciation— see Phase 2),voice,record. Leaveanonymize.enabled: falseunless the user wants it. - Run
boot.sh, verify health, andnode scripts/scrape-text.cjs <baseUrl>/...over the main surfaces to learn the real on-screen strings. - Already-deployed instance? Set
app.baseUrlto it and skip boot — everything downstream works identically.
Phase 1 — Prep (demo data + auth)
- Demo data: a demo over an empty app is dead on arrival. Use the repo's own seeds/fixtures (
npm run seed,rails db:seed, SQL fixtures) or create realistic content through the app/API. Prefer data that tells one coherent story (a project with history beats ten empty stubs). - Auth:
auth.mode: "none"— app has no login locally (or a dev bypass). Best case; prefer enabling a dev bypass over recording login flows.auth.mode: "storageState"— cookie/localStorage sessions. Capture once:node scripts/login.cjs --url <loginUrl> --expect "<logged-in-only text>".auth.mode: "profile"— SPAs whose auth token lives in IndexedDB (e.g. Firebase) render logged-out from a storageState file; the recorder must reuse the persistent Chrome profile that login.cjs created, which carries IndexedDB too.
- login.cjs gotchas (each cost real debugging):
--expectmust be an authenticated-only string — logged-out marketing pages often contain the same words as app pages, which makes the capture succeed before you log in.- Google sign-in blocks plain Playwright browsers ("This browser or app may not be secure"); the script launches real Chrome with the automation fingerprint disabled, which passes.
- storageState only captures localStorage for origins visited in that session; if the app keeps critical state there (selected org/workspace), inject it into the state file afterwards.
- Always probe before recording:
scrape-text.cjswith the captured auth — confirm the expected logged-in content renders headless.
Phase 2 — Script
- Ask the user: target length (~3 min default), audience, language, which features to show.
- Write
demo/segments.json(copytemplates/segments.example.json): one entry per scene beat,[{"slug": "hook", "text": "..."}, ...]. Budget ~150 words/min — and note pacing differs by TTS model (eleven_v3 runs noticeably slower/more expressive than v2 for the same text). - Narration rules (these prevent re-recording):
- Use the product UI's own labels verbatim — don't paraphrase what's on screen.
- Brand pronunciation: TTS mangles coined names. Generate once, LISTEN, and if mispronounced write the brand phonetically in
segments.json(brand.pronunciationrecords the chosen spelling) while cards keep the styled wordmark. Re-check when switching voice or model. - De-number drift-prone dashboard figures ("more than fifty…") — live data changes between scripting and recording and will contradict the voiceover. Re-scrape on recording day if exact numbers must stay.
- Spell out abbreviations you want spoken ("pull request", not "PR"); letter-acronyms ("API", "AI") read fine.
- Run a copy-review pass (subagent) over the narration before spending TTS credits.
Phase 3 — Narration
node scripts/tts-oneshot.cjs --segments demo/segments.json --config demo/demo.config.json
python3 scripts/split-narration.py --segments demo/segments.json
- Why one-shot: generating clips separately (even with previous/next-text stitching) produces stuttered clip starts. One continuous TTS request has exactly one start; the splitter cuts it at paragraph boundaries using ElevenLabs STT word-level timestamps + fuzzy alignment (STT rewrites brand words, so exact matching fails).
- Verify every clip start by transcription (the splitter prints the command). Boundary snaps can land one word off when the voice barely pauses between paragraphs (40 ms happens) — nudge the cut at the midpoint of the correct gap in
narration-full.stt.json. - v3 short-prompt instability:
eleven_v3can break up/crack on short standalone prompts (card VOs). Fix without dropping to v2: put a throwaway warm-up sentence as segment 0 in the same generation, split it off, discard it. timeline.jsonholds the scene boundaries — the recorder reads it directly.
Phase 4 — Screen Recording
Write demo/blocks.cjs (copy templates/blocks.example.cjs): one block per narration segment, anchored on the real strings you scraped in Phase 0. Then:
node scripts/record-demo.cjs --config demo/demo.config.json # ~real-time: 3-min demo = 3-min run
Hard-won rules baked into the framework — keep them when writing blocks:
- Playwright never upscales video —
recordVideo.sizelarger than the viewport letterboxes the content. Record at viewport 1536×864; the 1080p upscale happens in ffmpeg (assemble.cjs). networkidlenever fires on pages holding an SSE/websocket stream. Usedomcontentloaded+ explicit element waits.- The framework sets
page.setDefaultTimeout(6000)andglideTomisses cost ~2s and a WARN — a missing element must never eat 30s of a time-locked take. - Call
setT0()exactly when narration should start; every block ends withawait until(B[i]). A take withWARN: block overranlines is garbage — tighten and re-run. - Anonymisation (
anonymizein config,--no-anonto disable): rewrites identifying text and swaps operator face avatars/initials live during capture via MutationObserver — the recording is anonymised, the app data untouched. - The take's
t0and boundaries land inout/take-meta.json— nothing is hand-copied downstream.
Phase 5 — Cards & Assembly
- Copy
templates/{title,agenda,end}.htmlintodemo/cards/, set theBRANDblock (color, wordmark, copy). No invented contact details on the end card — ask the user what to print. - Render:
npx playwright screenshot --viewport-size=1920,1080 --wait-for-timeout=1500 file://$PWD/demo/cards/title.html demo/cards/title.png(repeat for agenda/end). - Optional card VOs (with the v3 warm-up trick):
out/voice/welcome.mp3(spoken welcome over the title card — picked up automatically if present) andout/voice/intro.mp3(agenda walkthrough, required unless--no-cards). - Assemble — one command, everything from metadata:
node scripts/assemble.cjs --build demo
# → out/main.mp4, out/final.mp4, out/frames/grid.png
- Verify: eyeball
out/frames/grid.png(every scene on the right page), play the head and tail. Don't chase a glitch at the very start of playback — VLC/GNOME Videos stutter the first ~0.5s on file-open; if it's clean after seeking to 0, the file is fine (confirm withffmpeg -t 6 -i final.mp4 -af astats -f null -— flat factor 0 means no dropouts).
Phase 6 — Publish
node scripts/publish.cjs --build demo
Builds out/publish/: faststart demo.mp4 (upload this to YouTube), demo-720p.mp4 (self-hosting / LinkedIn native upload), poster.jpg, captions.srt (from the STT timestamps — social feeds autoplay muted, captions are non-negotiable), embed.html, and PUBLISH.md with channel-specific guidance (incl.: post LinkedIn video natively, never as a YouTube link). If publish.upload is configured (gcs / s3 / youtube — see PUBLISH.md for the one-time OAuth provisioning), it uploads too.
Pre-flight Checklist
-
demo/boot.shboots from cold and the health check passes - Demo data tells a coherent story; auth probe renders logged-in content headless
- Narration reviewed: UI labels verbatim, brand pronunciation listened-to, drift-prone numbers removed
-
demo/segments.jsonis the single source of narration text - Every narration clip start verified by STT transcription
- Recorder take has ZERO
WARNlines - Frame grid eyeballed: every scene on the right page
-
demo/out/is gitignored; auth state/profile stays in~/.cache(never committed) - Publish kit built; captions attached wherever the video is uploaded
Built by aictrl.dev. This skill teaches the workflow; aictrl operationalizes it — grounded in your backlog, team standards, and codebase knowledge graph. See how →
What ships with it: 13 files
56.1 KB alongside SKILL.md, 8 of them executable
scripts/
- assemble.cjsruns6.7 KB
- login.cjsruns4.0 KB
- publish.cjsruns9.3 KB
- record-demo.cjsruns12.1 KB
- scrape-text.cjsruns2.4 KB
- split-narration.pyruns5.2 KB
- tts-oneshot.cjsruns4.3 KB
templates/
- agenda.html3.9 KB
- blocks.example.cjsruns1.7 KB
- demo.config.example.json1021 B
- end.html2.0 KB
- segments.example.json402 B
- title.html3.0 KB
Gives 0 of the 12 instructions most product growth skills give in ~3.1k tokens
Counted across 694 of the 879 authors here whose files we hold, read 2026-09-06
- Check for product marketing context firstin 49 of 694, across 20 files
- Validate the why before building featuresin 18 of 694, across 4 files
- Respond to every comment in real-timein 17 of 694, across 6 files
- Structure launch marketing across three channel typesin 16 of 694, across 4 files
- Recruit early users one-on-onein 13 of 694, across 2 files
- Ask one question at a timein 13 of 694
- Rank features using ICE scoringin 12 of 694, across 3 files
- Identify primary conversion goalin 11 of 694, across 3 files
- Identify traffic contextin 11 of 694, across 3 files
- Evaluate headline effectivenessin 11 of 694, across 3 files
- Check visual hierarchy and scannabilityin 11 of 694, across 3 files
- Run product diagnosticsin 11 of 694, across 3 files
Said here and by no other author read
- Read the repo to determine how to start the app
- Write an idempotent boot script
- Write demo.config.json from the example template
- Run the boot script and verify health
- Use demo data that tells one coherent story
- Use the product UI's own labels verbatim
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.