# Demo Studio

> Record polished demo videos of webapps — scripted Playwright scenarios with a visible cursor, caption overlays, title cards, and GIF export. Deterministic and re-runnable after every UI change.

- Skill: `openvole/demo-studio` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add openvole/demo-studio`
- Raw SKILL.md: https://api.skillmd.com/api/skills/openvole/demo-studio/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: openvole (https://skillmd.com/u/openvole)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/openvole/demo-studio

---


# 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

1. **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.
2. **Record**: `skill_run_script record.mjs <scenario.json> --out <workspace>/demo-out`
   → `raw.webm` + `manifest.json` (per-step timings, click coordinates) + step screenshots.
3. **Post**: `skill_run_script post.py <demo-out>` → `demo.mp4` with title card and captions.
4. **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

```json
{
  "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.mp4` to the **resolve-autocut** skill.

## Requirements

- **Playwright**: `npm i playwright-core` in the agent root (resolution walks up from the skill
  dir), plus a one-time `npx 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. Full `playwright` with 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.

