Demo as code — turn YAML specs into polished product demo videos.
Write a simple YAML file describing what to click, type, and navigate.
demo-machine launches your app, drives a real browser with human-like interactions,
records everything, and renders a production-ready MP4.
A task manager app demo with voice narration — generated entirely from a YAML spec.
https://github.com/45ck/demo-machine/raw/master/examples/todo-app-demo.mp4
Notice the smooth cursor movement to each target, character-by-character typing, voice narration that leads into each action, and polished overlays with fades. This is not a screen recording — it's generated from code.
View the YAML spec that produced this video
meta:
title: "TaskFlow - Task Manager Demo"
resolution:
width: 1920
height: 1080
runner:
command: "node examples/todo-app/serve.mjs"
url: "http://localhost:4567"
timeout: 10000
chapters:
- title: "Welcome to TaskFlow"
steps:
- action: navigate
url: "http://localhost:4567"
narration: "Welcome to TaskFlow, a simple and elegant task manager."
- action: wait
timeout: 1000
- title: "Getting Started"
steps:
- action: click
selector: "#get-started"
narration: "Let's click Get Started to begin managing our tasks."
- action: wait
timeout: 800
- title: "Adding Tasks"
steps:
- action: click
selector: "#task-input"
- action: type
selector: "#task-input"
text: "Design new landing page"
narration: "We'll type in our first task — designing a new landing page."
- action: click
selector: "#add-btn"
- action: wait
timeout: 500
- action: type
selector: "#task-input"
text: "Review pull requests"
narration: "Next, let's add a task to review pull requests."
- action: click
selector: "#add-btn"
- action: wait
timeout: 500
- action: type
selector: "#task-input"
text: "Write unit tests"
narration: "And one more — writing unit tests."
- action: click
selector: "#add-btn"
- action: wait
timeout: 500
- title: "Completing a Task"
steps:
- action: click
selector: ".task-checkbox"
narration: "To mark a task as done, just click the checkbox."
- action: wait
timeout: 800
- title: "Filtering Tasks"
steps:
- action: click
selector: "[data-filter='completed']"
narration: "We can filter to see only completed tasks."
- action: wait
timeout: 800
- action: click
selector: "[data-filter='all']"
narration: "Or switch back to view all tasks at once."
- action: wait
timeout: 500These are compressed GIF previews generated from real rendered MP4s. For the full acceptance matrix, see docs/demo-anything.md.
To regenerate the gallery assets (GIFs + 5 screenshots per demo): pnpm demo:gallery.
For the frame-by-frame review output, see docs/demo-gallery.md.
Tools like Screen Studio and Arcade require manual recording sessions. Every time your UI changes, you re-record. demo-machine takes a different approach:
- Reproducible — same YAML, same video, every time
- Version-controlled — specs live in your repo, reviewable in PRs
- CI-friendly — generate demo videos in your pipeline on every release
- No manual work — no clicking through your app, no editing in a video tool
| Feature | Description |
|---|---|
| Smooth cursor | Cubic-bezier eased movement with click pulse feedback |
| Natural typing | Character-by-character keystroke simulation |
| Configurable pacing | Global + per-step delays for clicks, typing, navigation |
| Polished overlays | Intro/outro cards, chapter titles with fades and backgrounds |
| Auto app lifecycle | Spawns your dev server, healthchecks, tears down after |
| Redaction | Blur sensitive selectors, scan for secret patterns |
| Narration | Local TTS via Kokoro, or cloud via OpenAI/ElevenLabs with VTT/SRT subtitles |
| Dead-time compression | Long pauses automatically sped up |
| Callout zoom | Click targets highlighted with zoom regions |
git clone https://github.com/45ck/demo-machine.git
cd demo-machine
pnpm install
pnpm buildnode dist/cli.js run examples/todo-app.demo.yaml \
--output ./output \
--no-headlessThis will:
- Start the included todo-app dev server
- Launch a browser with smooth cursor and natural typing
- Record the session via Playwright
- Render a polished MP4 with overlays to
./output/output.mp4
# Full pipeline: capture + edit + render
demo-machine run <spec.yaml>
# Validate a spec file without running
demo-machine validate <spec.yaml>
# Capture only (raw video, no post-processing)
demo-machine capture <spec.yaml>
# Re-render from an existing event log
demo-machine edit <events.json>The repo includes multiple example apps and .demo.yaml specs under examples/.
# Validate all example specs
pnpm examples:validate
# Smoke-capture raw videos for all example specs (no narration, no post-processing)
pnpm examples:capture
# Filter to a subset (note the `--` for pnpm passthrough)
pnpm examples:capture -- --filter spa-routercapture and run write these artifacts into --output:
video.webm(raw recording)events.json(event log)metadata.json(capture timing info used for accurate timelines)trace.zip(Playwright trace)
edit expects video.webm to be in the same directory as the events.json you pass. If metadata.json exists, it will be used automatically.
| Flag | Default | Description |
|---|---|---|
-o, --output <dir> |
./output |
Output directory |
--no-narration |
— | Skip TTS narration |
--no-edit |
— | Raw capture only, skip rendering |
--no-headless |
— | Show the browser window |
--renderer <name> |
ffmpeg |
Video renderer |
--tts-provider <name> |
kokoro |
TTS: kokoro (local), openai, elevenlabs, piper |
--verbose |
— | Debug logging |
meta:
title: "My Demo"
runner:
url: "http://localhost:3000"
chapters:
- title: "Getting Started"
steps:
- action: navigate
url: "/"
- action: click
selector: "#btn"navigate.url can be absolute (https://...) or relative (/). Relative URLs are resolved against runner.url.
| Action | Required Fields | Description |
|---|---|---|
navigate |
url |
Go to a URL |
click |
selector or target |
Click an element |
check |
selector or target |
Check a checkbox/toggle |
uncheck |
selector or target |
Uncheck a checkbox/toggle |
type |
selector or target, text |
Type text character-by-character |
select |
selector or target, option |
Select an option in a <select> |
upload |
selector or target, file or files |
Upload files into an <input type="file"> |
hover |
selector or target |
Hover over an element |
scroll |
— | Scroll the page or a container (selector or target, x, y optional) |
wait |
timeout |
Pause for milliseconds |
press |
key |
Press a keyboard key |
back |
— | Go back in browser history |
forward |
— | Go forward in browser history |
assert |
selector or target |
Assert visibility or text content |
screenshot |
— | Take a screenshot (name optional) |
dragAndDrop |
from, to |
Drag from one target to another |
Every action supports an optional narration field for TTS and most support delay to override the default post-action pause.
For click, type, hover, scroll (container scroll), and assert, you can use a structured target instead of a raw CSS selector:
- action: click
target:
by: role
role: button
name: "Next"Supported strategies:
css:{ by: css, selector: ".my-class" }testId:{ by: testId, testId: "save-button" }(usesdata-testid)role:{ by: role, role: "button", name: "Save", exact: true }text:{ by: text, text: "Settings", exact: true }label:{ by: label, text: "Email", exact: true }placeholder:{ by: placeholder, text: "Search", exact: true }altText:{ by: altText, text: "Company logo", exact: true }title:{ by: title, text: "Open menu", exact: true }
This makes specs more resilient across UI refactors (class name changes, DOM reshuffles) and aligns with accessibility.
When a selector/target matches multiple elements, you can pick the Nth match (0-based):
- action: click
selector: "button"
nth: 1Control the feel of the demo globally. All fields are optional with sensible defaults:
pacing:
cursorDurationMs: 600 # cursor travel time
typeDelayMs: 50 # ms between keystrokes
postClickDelayMs: 500 # pause after clicks
postTypeDelayMs: 300 # pause after typing
postNavigateDelayMs: 1000 # pause after navigation
settleDelayMs: 200 # micro-pause after every actionBlur sensitive content and scan for secret patterns:
redaction:
selectors:
- ".user-email"
- "[data-sensitive]"
secrets:
- "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z]{2,}\\b"meta:
branding:
logo: "./assets/logo.png"
colors:
primary: "#3B82F6"
background: "#000000" ┌──────────────┐
│ YAML Spec │
└──────┬───────┘
│
┌──────▼───────┐
│ Spec Loader │ Zod validation + defaults
└──────┬───────┘
│
┌──────▼───────┐
│ Runner │ Spawn dev server, healthcheck
└──────┬───────┘
│
┌──────▼───────┐
│ Playback │ Cursor animation, typing, pacing
│ Engine │ Playwright browser automation
└──────┬───────┘
│
┌──────▼───────┐
│ Capture │ Video recording + event log
└──────┬───────┘
│
┌──────▼───────┐
│ Timeline │ Intro/outro, chapters, callouts
│ Builder │ Dead-time compression
└──────┬───────┘
│
┌──────▼───────┐
│ FFmpeg │ Overlays, fades, final MP4
│ Renderer │
└──────┬───────┘
│
┌──────▼───────┐
│ Narration │ TTS + VTT/SRT subtitles
│ (optional) │
└──────────────┘
src/
cli.ts # CLI entry point
index.ts # Public API exports
spec/ # YAML parsing + Zod validation
runner/ # Dev server lifecycle
playback/ # Cursor, typing, pacing engine
capture/ # Playwright video recording
editor/ # Timeline + ffmpeg renderer
narration/ # TTS providers + subtitles
redaction/ # Blur + secret scanning
utils/ # Logger, process helpers
tests/ # 172 tests across 13 suites
examples/ # Example specs + demo apps
pnpm build # Compile TypeScript
pnpm test # Run 172 tests
pnpm lint # ESLint
pnpm format # Prettier check
pnpm typecheck # tsc --noEmit
pnpm validate # Run everythingSee CONTRIBUTING.md for setup instructions and development workflow.
MIT — use it however you want.














