onetwodemo
v0.7.22
Published
Walkthrough demo videos of your app: real clicks, real typing, a voiceover — and a help chat that answers customer questions with the right clip.
Maintainers
Readme
onetwodemo
Walkthrough demo videos of a real web app, with voiceover. Real clicks, real typing, real screens — not animation.
Setup
1. Get a token. Sign in at onetwodemo.com and copy the publishing token from your dashboard. It is what puts finished videos in your library and gives you a link to share with customers.
2. Point your editor at it. In Claude Code or Cursor, add this and restart:
{
"mcpServers": {
"onetwodemo": {
"command": "npx",
"args": ["-y", "onetwodemo", "mcp"],
"env": { "DEMO_GEN_TOKEN": "dg_live_..." }
}
}
}The -y matters. Without it npx stops to ask permission the first time, and a server
that asks a question on startup never finishes starting.
3. Ask for a video.
make a demo video of creating a project, my app is on localhost:3000
It will tell you what it is about to film, on which app, and what it already knows that app can do — and wait for you to agree before a browser opens.
What it needs from you
Your app has to be running. All of it: backend, database, queues, workers. A half-started app does not fail the recording — it produces a polished video of an error page. It checks before it starts, and reads your docker-compose and env files to see what else should be up.
A demo account, if you have one. This does not browse, it presses buttons: it creates things and spends credits in whatever account it signs into. It will ask you to sign in yourself, in a real browser window — it never sees your password, and the session is saved so it only asks once.
Nothing else. ffmpeg is bundled, and a browser downloads on first run (~550MB, shared with any other Playwright project on your machine — and fetched automatically if it is missing, so there is nothing to run by hand). Check it landed:
npx onetwodemo doctorVoice. Set DEMO_GEN_TOKEN and you get the good voice — we pay for it, there is no
key to sign up for. Without a token you get whatever your machine can speak with — say
on a Mac, espeak on Linux — which is free and sounds like 2005.
Try it in 30 seconds
npm run example # starts the test app on :4321
npx onetwodemo record storyboards/calorie-basket.json
open out/calorie-basket.mp4No API keys needed.
How it works
[1] Plan → [2] Record → [3] Assemble1. Plan — an agent opens the running app, does the flow once, and writes down every click it made along with the voiceover line for that moment. The result is a storyboard: plain JSON you can read and edit.
2. Record — a dumb player replays that storyboard. No model, no decisions. Same storyboard always gives the same performance, so you can re-record without paying to plan again.
3. Assemble — each step is cut out, stretched to fit its narration, zoomed toward the thing being clicked, and joined into one mp4.
The rule that makes it work
The video waits for the voice, never the other way round. A step that takes 2 seconds but has 5 seconds of narration holds its last frame for the extra 3. Sync audio to fixed video instead and every demo drifts out of step.
How the camera decides
The zoom is not a fixed amount. It comes from the size of the thing being used, which the recorder measures at the moment it clicks.
- Small target — a button, a menu item — takes a hard push, up to 1.6x.
- Wide target — a full-width text field — gets no zoom at all. Centre on the click point and push in, and the field's left edge, where the typing actually appears, slides off the screen.
The camera also does not return to wide between every click. Consecutive clicks whose targets sit near each other share one zoom and pan across, so two buttons side by side read as one held shot rather than two dives. The run breaks when the next target is far enough away that holding the zoom would mean showing most of the page anyway.
Demos published before the recorder measured sizes only stored a point. Those keep the old fixed 1.15x behaviour rather than losing their zoom.
Commands
npx onetwodemo plan "show logging food and adding it to the basket" --url http://localhost:4321 --repo .
npx onetwodemo record storyboards/calorie-basket.json
npx onetwodemo make "..." --url ... --repo . # plan then record
npx onetwodemo listnpx onetwodemo edit storyboards/calorie-basket.json # timeline editor at localhost:4600Useful flags on record: --speed 1.5, --zoom off, --voice deepgram, --headed,
-o path.mp4, --force (build the video even if a step failed).
The editor
One clip per step, laid out on a timeline, each block sized by its length. Click one to preview it and edit it:
- Narration — retype the line; the clip stretches to fit it
- Picture speed — 0.5× to 3×, only the picture, the voice is never rushed
- Trim — shave milliseconds off either end
- Zoom / Mute / Hide / Reorder
- Plain English — "make this punchier and 1.5× faster" (needs
ANTHROPIC_API_KEY)
Everything here rebuilds in seconds without opening a browser. The recording already exists; narration, speed, trim, order and zoom are all post-processing on it. Only changing which element gets clicked needs a fresh recording.
Edits live in out/edits.json, keyed by step. Delete it to start over.
plan needs ANTHROPIC_API_KEY. record needs nothing.
Storyboard format
A step never stores one selector. It stores an intent sentence plus a ranked list of
ways to find the same element. At playback each is tried in order and the first that
matches exactly one element wins.
{
"action": "click",
"intent": "The green button that saves the new food to today's list",
"anchors": [
{ "kind": "testid", "value": "add-food-btn" },
{ "kind": "role", "role": "button", "name": "Add food" }
],
"narration": "And it's logged. The daily total updates straight away."
}The intent line is what makes repair possible later. A bare selector that breaks is
unfixable; "the button that saves the new food" can be found again in a fresh page.
Actions: goto, click, type, hover, press, wait.
What it does to the app
It clicks the app for real, so real things get saved. Point it at staging with a test login, never production.
It does not touch the source. The repo scan is read-only. The one attribute the
explorer sets (data-demo-ref) lives in its own browser for milliseconds and never
reaches the server.
Leftover demo data is expected for now. Reset the demo account when it gets messy.
Voiceover
You do not need a voice provider, or a key for one. Set DEMO_GEN_TOKEN — the same
one that publishes demos — and the narration is spoken on our account. Pick the voice in
the editor; which service produces it is our problem.
| What you set | What you get |
|---|---|
| DEMO_GEN_TOKEN | the good voice, on us — nothing to sign up for |
| nothing | macOS say / espeak — free, offline, sounds like 2005 |
There is a monthly allowance, roughly two hours of narration per account. Run past it and the voice quietly drops to the built-in one and says so — you never lose a video over it.
Set FISH_AUDIO_API_KEY, DEEPGRAM_API_KEY or ELEVENLABS_API_KEY and that wins over
the hosted voice. Useful for developing on this repo; nobody using the tool needs it.
FISH_AUDIO_VOICE picks a different cloned voice.
MCP
For Claude Code or Cursor:
{
"mcpServers": {
"onetwodemo": {
"command": "npx",
"args": ["-y", "onetwodemo", "mcp"],
"env": { "DEMO_GEN_TOKEN": "dg_live_..." }
}
}
}The server ships instructions — the order to call things in, and what to do when a step fails. The calling agent gets a recipe, not just a pile of tools.
| Tool | Does |
|---|---|
| demo_status | where am I: is the app up, is a login saved, what exists already |
| demo_discover | read-only map: screens, URLs, actions, fields, routes, models, tests, docs |
| demo_prepare | start here — sign-in, setup checks, real values for [brackets], click path |
| demo_login | opens real Chrome so the person signs in once; attachPort for stubborn sites |
| demo_precheck | is each prerequisite already true in the running app |
| demo_open demo_act demo_save | you plan the demo, one verified step at a time |
| demo_act(screenshot: true) | returns a picture of the page too — ~700 tokens, automatic on failure |
| demo_routes | turn /projects/[name] into /projects/Protomatch from the live app |
| demo_cancel | throw away the planning session |
| demo_plan | unattended planning (needs ANTHROPIC_API_KEY) |
| demo_record | perform the storyboard, voice it, build the mp4 |
| demo_clips | list the clips in the last video, with ids and narration |
| demo_edit | change speed, narration, trim, order, cut, drop — then rebuild |
| demo_published | what is on your dashboard, and whether this cut has drifted from it |
| demo_publish | put this cut on your dashboard — only when you ask |
| demo_list | storyboards and videos so far |
Putting a video on your dashboard is something you ask for
"Publish" here means one specific thing: the video goes to your dashboard — your video library at onetwodemo.com, which is also where your customers get shown demos when they ask a question. Nothing else changes, and nothing is made public on the open web.
Until you ask, a finished video stays on your machine. It is not in your library and no customer can be shown it.
That is on purpose: "the file finished rendering" and "my customers are now watching this" are two different decisions. So every time a cut is handed to you — the first one and every re-cut after it — you get asked whether you are happy with it, and whether it should go on your dashboard.
Edits stay on your machine too. Change a video that is already up there and your customers
keep seeing the old one until you say to update it. demo_published tells you when the two
have drifted apart, and sending it again replaces what is on the dashboard rather than
adding a second copy.
Editing by talking
You do not have to open the timeline. demo_clips shows what is there, demo_edit
changes it, and the agent turns plain requests into fields:
| you say | it sends |
|---|---|
| make the typing faster | { id: 'c5', speed: 2 } |
| reword the intro | { id: 'c0', narration: '...' } |
| drop the last bit | { id: 'c8', hidden: true } |
| cut that long clip in half | { id: 'c5', splitAtSeconds: 7 } |
| trim the dead air at the start | { id: 'c2', trimStartMs: 800 } |
Rebuilds in a couple of seconds — the footage already exists, and unchanged narration comes from cache.
House rules
Drop a demo-gen.md in your project and its contents are appended to the server's
instructions on every start:
- Say "Nibble" not "the app".
- Never show the Basket tab; it is not launched yet.
- Keep every video under 40 seconds.That is how a team changes the workflow without touching this package.
Two ways to plan
You drive (no API key). Claude Code or Cursor is the planner. It already read your repo, so it makes better calls than anything built in — and it costs no key:
demo_open → returns the page and everything you can act on
demo_act → one step; records it only if it can be found again later
demo_act → ...
demo_save → writes the storyboard
demo_record → builds the mp4It drives (needs ANTHROPIC_API_KEY). demo_plan explores on its own. For unattended
runs — a scheduled job, a hosted service, an end user clicking "show me how" inside your
product. Pass notes with anything you already know about the app.
scan.js only runs regexes; it cannot tell you what a feature is for. Whoever calls the
tool should read the source and say so — via notes, or just by driving demo_act
themselves. From the CLI: --notes "..." or --notes-file plan.md.
Files
| File | Job |
|---|---|
| src/cli.js | commands |
| src/mcp.js | MCP wrapper |
| src/explore.js | prompt → verified storyboard |
| src/scan.js | read-only repo hints |
| src/player.js | replays a storyboard in a browser |
| src/cursor.js | the fake mouse pointer |
| src/locator.js | anchors → real elements |
| src/voice.js | narration → audio |
| src/assemble.js | cut, stretch, zoom, join |
Known gaps
- No self-repair yet — a broken anchor fails the step instead of asking the model to find it again.
- No rehearsal pass — every attempt records at full speed. Fine until failures get annoying.
- Live streams (WebSocket/SSE), third-party iframes and OAuth popups are untested.
- Captions and intro/outro cards are not built.
- Only tested on macOS.
