Demo Studio
Turn "make a demo of X" into an mp4. You write a scenario file (the judgment: what to show, in what order, with what narration); the bundled scripts do the labor (drive the browser, record, burn captions, cut GIFs). The scenario is the product — after a UI change, re-recording is free.
Why scripted browser recording instead of screen capture: deterministic (no flaky window focus, fixed viewport, same result every run) and self-documenting — the recorder knows where it clicked and when, so captions land exactly on their step and a cursor is drawn where the action is. Screen capture knows none of that.
Workflow
- Write the scenario (JSON) into the agent workspace. Show it to the operator if the demo is for publication — the scenario review IS the demo review.
- Record:
skill_run_script record.mjs <scenario.json> --out <workspace>/demo-out→raw.webm+manifest.json(per-step timings, click coordinates) + step screenshots. - Post:
skill_run_script post.py <demo-out>→demo.mp4with title card and captions. - GIF (for READMEs/posts):
skill_run_script gif.py <demo-out>/demo.mp4.
All outputs go under the path you pass with --out — use the agent workspace, never the skill
directory (scripts run with the skill directory as cwd; it is not yours to write).
Scenario format
{
"title": "VoleDrop in 30 seconds",
"url": "http://localhost:3000/?token=SECRET",
"viewport": { "width": 1280, "height": 800 },
"colorScheme": "dark",
"pace": 700,
"steps": [
{ "narrate": "One dashboard for every agent" },
{ "click": "text=VoleNet", "narrate": "Chat with a peer node" },
{ "type": ["#vn-input", "hello from the demo"], "narrate": "Messages are end-to-end encrypted" },
{ "press": "Enter" },
{ "waitFor": ".vn-msg" },
{ "wait": 1200 },
{ "scroll": 300 },
{ "screenshot": "final" }
]
}
Verbs (one per step; narrate attaches a caption to any step, or stands alone as a beat):
goto (absolute, or path resolved against url), click, dblclick, hover,
fill (instant) / type (visible keystrokes) — both [selector, text], press (key),
waitFor (selector), wait (ms), scroll (px or a selector to bring into view),
screenshot (name). Selectors are Playwright selectors — text=, css, role=.
pace is the pause between steps (default 700 ms) — demos need air; don't set it below 400
unless the GIF is the only target.
Judgment guide
- Verify selectors against the live page before writing the scenario — never guess at the
DOM. Record a one-step probe (
{ "screenshot": "probe" }) and read the screenshot, or inspect the page first; a scenario of guessed selectors fails on every step and wastes a recording. - Demos of live systems must be side-effect free. Type into inputs for visual effect but do not submit; never click destructive actions. If the page controls real agents (the OpenVole dashboard does — possibly including you), everything you trigger actually happens.
- Narrate outcomes, not mechanics. "Messages are end-to-end encrypted", not "click the input". Three to six captions per demo; a caption every step reads as noise.
- 30–60 seconds. Longer than that is a tutorial, not a demo — split scenarios.
- Never put secrets on screen. Tokenized URLs are fine in the scenario file (it stays in the workspace) but check what the page renders before publishing; use a throwaway token for demos.
- Re-record after UI changes rather than editing old footage — the scenario makes it a one-liner.
- For a Shorts/vertical cut of the same footage, hand
demo.mp4to the resolve-autocut skill.
Requirements
- Playwright:
npm i playwright-corein the agent root (resolution walks up from the skill dir), plus a one-timenpx playwright-core install ffmpeg(~1 MiB — Playwright muxes recordings with its own ffmpeg build; the system ffmpeg does not count). Uses the installed Google Chrome (channel: chrome) — no browser download. Fullplaywrightwith bundled Chromium also works. To force a specific binary (Chromium, Brave, a pinned version), set"chrome": "/path/to/binary"in the scenario file. - ffmpeg/ffprobe on PATH; python3 + Pillow for title cards and caption rendering.
- The recorder injects a visible cursor with a click ripple — real browsers don't show the mouse in recordings; a demo without a cursor is unreadable.
post.py options
--speed 1.5 (time-scale the whole video; captions re-time automatically), --no-title,
--fps 30, --out name.mp4.
gif.py options
--width 960, --fps 12, --out name.gif — two-pass palette, README-friendly sizes.