Turn a narrated script into a finished, voiced screen-demo video — automatically.
You write a YAML script (what's said + what happens on screen). demoreel:
- 🎙️ generates the voiceover with ElevenLabs,
- 🖥️ plays the demo in a self-playing page and records it with Playwright (headless, deterministic — runs anywhere, even CI),
- 🎬 syncs the narration to the on-screen action and encodes a clean MP4 with ffmpeg.
No screen-capture, no manual recording, no clicking. Same script → same video, every time.
git clone <your-repo> demoreel && cd demoreel
npm install # also downloads a headless Chromium
cp .env.example .env # then paste your ElevenLabs API keyRequires Node 18+ and ffmpeg on your PATH.
npx demoreel build examples/sql-join/script.yaml
# -> out/How-to-Join-Two-Tables-in-SQL/How-to-Join-Two-Tables-in-SQL.mp4That's it. The example renders a complete ~2-minute narrated SQL JOIN tutorial.
title: How to Join Two Tables in SQL
voice: alice # alice | matilda | sarah | river | george | <elevenlabs-voice-id>
player: code-editor
size: [1920, 1080]
header: { title: Query Editor, subtitle: demo.db, badge: SQLite connected }
schema: # left-hand panel
- name: customers
columns: [{ name: customer_id, type: INTEGER, key: true }, { name: name, type: VARCHAR }]
- name: orders
columns: [{ name: order_id, type: INTEGER }, { name: customer_id, type: INTEGER, key: true }, { name: amount, type: DECIMAL }]
scenes:
- say: "In this demo I will show you how a JOIN works..." # narration (verbatim TTS)
do: # on-screen actions for this scene
- { highlight: schema }
- say: "I type SELECT star FROM customers..."
do:
- { highlight: editor }
- { type: "SELECT * FROM customers;" }
- say: "Now I run it..."
do:
- run: { columns: [name, amount], rows: [["Maria", "250.00"]] }
- { highlight: results }Each scene = one narration line + a list of actions. The narration's length sets how long the scene holds on screen, so the voice and the visuals always stay in sync.
| action | effect |
|---|---|
{ type: "text" } |
type text into the editor (animated) |
{ backspace: N } |
delete N characters |
{ setText: "text" } |
clear and retype |
{ run: { columns, rows } } |
flash Run, show a results grid |
{ highlight: editor | schema | results | { table: name } } |
move the spotlight box to that area |
{ keys: on | off } |
pulse the key columns in the schema |
{ wait: seconds } |
pause within the scene |
alice (UK educator), matilda, sarah, river (US), george (UK) — or pass any ElevenLabs voice ID.
script.yaml
│ scene.say ──► ElevenLabs ──► voiceover clips + durations
│ scene.do ──► player.html (self-playing, timed to those durations)
│ │ Playwright records it to video (headless)
└──────────────────────┴──► ffmpeg: sync narration + encode ──► final.mp4
Because the page is self-playing and timed to the voiceover, the result is deterministic: re-running the same script produces the same video. Recording is done by Playwright's video capture, so there's no OS screen-recorder dependency — it works on macOS, Linux, and in CI.
code-editor(included) — SQL / code demos with a schema panel, animated typing, run + results grid, syncing highlights. Great for SQL, query languages, config files, API payloads.
More players (terminal, web-app driver via Playwright, slide/markdown) are intended to plug in
the same way — each is just an HTML template plus a list of supported do actions.
- ✅ Code / data demos (SQL, HTML/CSS, terminal, API) and real web apps (drive a live site with Playwright) are fully supported and portable.
- ⛔ Native desktop apps (Excel, Word, native CRM clients) are out of scope — automating arbitrary desktop GUIs needs an AI-agent layer that can't be bundled in a library. Use a web equivalent, or record those segments separately and drop them in.
MIT