Narrated A/B video
More triggers
video walkthrough of the PR, make a demo recording, voiceover demo, record both stacks, explainer video, no drawtext filter, Playwright recordVideo, headless shell missing
One mp4 per claim set: title card, A clip, B clip, evidence card, verdict card, each voiced.
Built for macOS on Apple Silicon (say, the Playwright cache under ~/Library/Caches/ms-playwright, chrome-mac-arm64). Elsewhere, set CHROMIUM_EXE and use --eleven for the voice. Needs Bun, ffmpeg and Python 3.
Do this
S=${SKILL_DIR:-~/.claude/skills/narrated-ab-video}
$S/preflight.sh /tmp/vid # chromium, playwright ffmpeg, ffmpeg, say voice
bun $S/record.ts a.json raw/a-run # one scenario per side (copy examples/scenario.json)
bun $S/record.ts b.json raw/b-run
bun $S/cards.ts cards.json cards # title/evidence/verdict PNGs + banner-A/B.png
python3 $S/build.py plan.json # [--voice Samantha] [--rate 182]
open -a "QuickTime Player" demo.mp4 # opens paused; never play it yourself
Examples in examples/: scenario.json (steps: wait, select, click, clickIfPresent, type, send, waitStable, expand, fetchText, waitText, probeEnabled, reload; see the header of record.ts), cards.json, plan.json. Plan paths are relative to the plan file. Validated end to end on a 4-segment, 40s build from two real clips.
Why it's built this way (friction mined)
- Playwright's pinned headless shell is often not downloaded.
record.tsandcards.tslaunch the newest cachedchromium-*full build (or$CHROMIUM_EXE) instead of failing. - recordVideo needs Playwright's own ffmpeg build. Missing means no webm. preflight reads the revision from
browsers.jsonand runsbunx playwright install ffmpeg. - Homebrew ffmpeg may lack
drawtext. Banners are transparent PNGs fromcards.ts, overlaid in build.py. No font or filter hunting. - Model replies take 10 to 60 seconds of dead air.
sendmarks "sent" andwaitStablemarks "replied". build.py plays that span atspeed(default 10x) and the rest at 1x. - Narration is usually longer than the clip. build.py holds the last frame (
tpad stop_mode=clone) until the voice ends, so a reply stays on screen while it is explained. - Dynamic input (OTP codes, ids from an earlier turn): put
{{fn:NAME}}in atypestep and exportNAME(page, state)fromhooks.tsnext to the scenario.state.lastJsonholds the last POST JSON matchingcaptureJsonFrom. - Selectors: buttons are matched by role and name regex (
"Tools used","^Send$"). CSS is only for inputs and selects. Find names with a quick screenshot before scripting. - Concat needs identical encodes. Every segment is re-encoded with the same codec, rate and audio layout, then joined with
-c copy. - Better voice:
ELEVENLABS_API_KEY=... build.py plan.json --eleven <voice_id>(model eleven_multilingual_v2; clips cached by voice+text hash, so reruns spend no characters). Keep the key in a 0600 file, never in the plan, and delete it after. - Single-side demo (no A/B): record one scenario, give
cards.jsonone banner, and cut the same clip into several plan segments, one claim each, withspansspeeding the waits (e.g. 5x). Same build, no A side. - The user may paste just
<voice_id> <key>. That means rebuild with--eleven <voice_id>. A slower voice stretches the video (72s withsaybecame 92s), since build.py holds the last frame until the voice ends. - Re-standing a torn-down demo before filming: short-lived auth (local JWTs, tunnel URLs, temp cloud resources) is usually dead by the next day. Re-verify with one API run before you record; a 401 "JWT token expired" there would have filmed a turn whose results never arrive.
- Lost raw clip: cut that side's footage from an earlier output mp4 (find its span by sampling the banner corner color), save it as
<dir>/a.webmwith-f mp4and an empty marks.json, and use one explicit span. Drop thebannersince it is baked in. - Audio:
say -owrites files only. Never play the video or any audio without the user's explicit OK. Open it paused and hand it over. - Prove before you film. Run the A/B and assert the result (API or log) first. The video shows a proven result; it is not the test.
- Cards must match the clip size. build.py does not scale. Recording at
"viewport": [1440, 900]needs"size": [1440, 900]in cards.json, or the concat has mixed frame sizes. - Async UIs need marks, not fixed waits.
waitTextmarks when a regex shows up (ortimeout:<mark>),probeEnabledwithwaitMspolls a button and logsprobes[]in marks.json,tryMson type/send/click marksskipped:<step>instead of aborting, so a "stays disabled" side still finishes the clip. - Probe the same element you click. A CSS probe (
button[aria-label=...]) hit a different matching button than the role-namesendand read disabled while the send went through; use"role"inprobeEnabled. - Dev-mode overlays (query devtools, error toasts) end up in the clip. Add
{"clickIfPresent": "^Close .* devtools$"}after the first wait. - Waiting on a reply that never "stabilizes": a live elapsed-time badge changes the body text every second. Check
marks.jsonfor arepliedmark before you build. - Check that the proof is in visible text. Collapsed tool cards may not expand (
expandsilently clicks nothing). Readmarks.jsonbodyfor the claim string. If it's missing, change the prompt or prove it on a card instead. - Prove parity before you film a "base fails" claim. Once a local base failed for a reason (a flag that was on locally, off remotely) that the real environment never hits, and the narration claimed it. Match the remote's flags first, then film.
- Title card: commits tested and flags on each side. One claim per clip segment. End on a verdict card.
Related
narrated-screenshot: a single annotated still instead of a video.pr-ab-evidence: the red/green A/B method whose result this films.