walkthrough-record
Runs the bundled Playwright recorder: a scenario YAML in, a cinematic webm master
out, plus mp4 (H.264, faststart) and gif (two-pass palette) exports. The recorder
drives a real Chromium, injects an overlay engine (eased bezier cursor, click
ripples, spotlight ring, privacy masks, explanation cards, camera zoom) and
captures with Playwright's page.screencast at crisp 2× resolution. An optional
framing pass then insets the capture into a wallpaper canvas — rounded corners,
soft shadow, optional macOS window header.
It fixes the failure of hand-driven capture: shaky cursor, missed beats, unreproducible takes, soft text, and 20 MB GIFs.
When NOT to use
- Writing or refining the scenario YAML itself (beats, card copy, zoom placement,
themes) →
walkthrough-storyboard. - OS-level screen capture of arbitrary desktop apps or live presentations.
- Playwright test authoring, screenshots, or generic ffmpeg conversions.
Workflow
- Setup once per machine. Run
bash "${CLAUDE_SKILL_DIR}/scripts/setup.sh". It verifies Node ≥ 20 and ffmpeg, installs pinned deps (playwright 1.61.1, yaml 2.8.0) next to the scripts, and installs Chromium. It exits non-zero with the exact missing prerequisite — surface that message, don't improvise installs. - Get a scenario. Use the one the user provides, or storyboard one first
(that's the sibling skill's job). No scenario and they just want to see it work?
Record the bundled demo:
"${CLAUDE_SKILL_DIR}/assets/demo/orbit-tour.yaml". - Record.
Flags:node "${CLAUDE_SKILL_DIR}/scripts/record.mjs" scenario.yaml --out ./walkthrough-out--validate-only(schema check, guaranteed no browser),--webm-only(skip exports while iterating),--headed(watch live),--out DIR. The recorder validates the scenario BEFORE opening a browser and exits 1 listing every schema violation — relay those verbatim; fix the YAML, never the validator. Authenticated targets: astorageStatescenario performs real clicks with a real session — confirm the origin is a staging/test account with the user before recording anything production. - Verify before declaring success. Check the logged
mp4 check:line (width,height,frames | seconds), then extract 3–4 spot frames and look at them:
Confirm: cursor visible and smooth, cards readable and inside the frame, zooms centered on their targets, no masked content leaking.ffmpeg -y -i out/<name>.mp4 -vf "select='not(mod(n\,400))'" -vsync vfr /tmp/frame%d.png - Frame it (optional, off unless the scenario asks).
output.frameinsets the capture into a wallpaper canvas. It runs at export time — the webm master stays full-bleed, so a frame can be added, restyled or dropped by re-exporting.output: frame: background: auto # auto | studio | midnight | spotlight | paper | any CSS colour/gradient chrome: false # macOS window header (12 pt lights, 28 pt bar) — default off title: "Orbit" # header title, chrome only pad: 0.062 # canvas padding, fraction of canvas width radius: 13 # corner radius in CSS px (10 = real macOS, the default when chrome is on)output.frame: truetakes every default.background: autosamples the recorded app's brand colour and light/dark-ness and derives a matching wallpaper — the right choice when you don't know the app in advance. Pick a named background when the video has to match a brand you already know. - Tune exports when asked for platform targets (README, YouTube, socials):
budgets and recipes live in
references/export-profiles.md. GIF too big? Trim to one beat (gif: {start, duration}), then narrow width, then lower fps — re-export from the existing webm, don't re-record (framed runs keep their*.frame-plate.png/*.frame-mask.pngnext to the outputs precisely so that re-export still works; the recipe is inreferences/export-profiles.md). - Failures (selector not found, wrong zoom area, blank video, sticky-header
artifacts): diagnose with
references/troubleshooting.md. Never editscripts/to work around one app; fix the scenario or document the limitation.
Output spec
<out>/<name>.webm(master, always full-bleed — the frame is never baked into it),<name>.mp4(H.264 CRF 18, yuv420p, faststart, CFR atoutput.fps),<name>.gif(palettegen/paletteuse two-pass). Each file is written atomically (never truncated); a failure mid-run can still leave the earlier completed files of that run — same-named outputs are overwritten.- Framed runs additionally keep
<name>.frame-plate.png(canvas-sized: wallpaper, shadow, rim, header) and<name>.frame-mask.png(the rounded-corner alpha) — the re-export inputs, not junk to clean up. - A verification note: probe line + which frames you inspected.
Gotchas
- Camera zoom is a CSS transform on
<body>— by design (GPU-smooth easing). Apps whose fixed/sticky chrome or JS reads viewport rects during a zoom can misbehave; the fix is scenario-side (zoom-free beat or different target), see troubleshooting. Compositor-side zoom is roadmap, not shipped — don't promise it. - 2× crispness uses
html {zoom}with a doubled viewport, so media queries see base×2 width. Desktop-first apps render identically; apps with max-width breakpoints in between needoutput: {scale: 1}. - Masks are opaque and fail-closed: solid covers (not blurs — blur leaks shape/length), re-resolved every frame; a mask selector matching nothing aborts the run; navigations paint a curtain until masks re-apply. Don't "fix" an abort by deleting the mask — find the right selector. Masks can't reach inside cross-origin iframes.
wait: {until: networkidle}hangs on apps with persistent websockets — preferwait: {for: selector}.- Exports overwrite same-named outputs (iterative renders are the norm); use
distinct
--outdirs to keep takes. - Recording is realtime: keep the machine unloaded during capture; verify pacing with the ffprobe commands in troubleshooting if output looks stuttery.
- A frame costs ~20 % of the app's linear resolution (2560 → 2054 px at the
default padding). The master is 2× raster, so the app still lands at 1.6× and
text stays sharp — but don't stack a frame on
output: {scale: 1}. background: autosamples once, at the first overlay injection. A tour that toggles the app to dark mode halfway keeps the wallpaper it started with; the rim and shadow still separate the window. Pin a named background if that bothers you.- Framed GIFs change palette defaults to 256 colours +
sierra2_4a: gradient wallpapers band visibly under the unframed 128-colour bayer default. Diffusion at 128 colours is both uglier and bigger — don't "optimise" it back down.
Files
scripts/record.mjs— recorder CLI (validates, records, exports; exit ≠ 0 on any failure)scripts/overlay.js— injected overlay engine (cursor/cards/ring/masks/camera)scripts/frame.mjs— export-time framing (wallpaper/shadow/corners/macOS header);node frame.mjs --self-testscripts/setup.sh— one-time dependency setup,scripts/themes/— theme presetsassets/demo/— bundled demo app (orbit.html) + demo scenario (orbit-tour.yaml)references/export-profiles.md— mp4/gif budgets per destination (README, YouTube, Vimeo, in-app)references/troubleshooting.md— failure→fix table, zoom compat notes, pacing audit commands