A claude skill to create a narrated demo video of a web application
TypeScript
13
49 commits
updated Mar 30, 2026
A CLI toolkit and Claude Code skill for creating narrated demo videos of web applications. You describe what to show, Claude Code drives the browser, and the toolkit renders a polished mp4 with voiceover.
Claude Code is the agent. The toolkit provides browser management, page inspection, segment playback, and video rendering as CLI commands. The skill file (SKILL.md) teaches Claude Code how to use them.
You: "Create a demo showing the new dashboard filters"
→ Claude Code writes the playbook, opens the browser,
authors actions by inspecting the page, tests each
segment, and renders the final mp4.
Add this repo as a skill in your project's .claude/settings.json:
{
"skills": [
"https://github.com/splitbrain/ndemo"
]
}
Clone directly into a Claude Code skills directory:
# Project-level (this project only)
git clone https://github.com/splitbrain/ndemo .claude/skills/ndemo
# Personal (available in all projects)
git clone https://github.com/splitbrain/ndemo ~/.claude/skills/ndemo
This is useful during development — you can edit the skill files and the CLI source directly, and Claude Code picks up changes immediately.
You can also symlink an existing clone:
ln -s /path/to/your/ndemo ~/.claude/skills/ndemo
Either way, the skill file tells Claude Code how to build the toolkit and install Playwright on first use. Everything runs from within the skill's own directory — nothing gets copied into your project.
In Claude Code, just say:
Create a narrated demo of my app at http://localhost:3000
Claude Code will:
Be specific about what the demo should show, what state needs restoring, and how to authenticate:
Create a narrated demo for editing a wiki page. The app runs at http://localhost:8080/wiki. The demo should: (1) show the start page, (2) click edit, (3) type some text, (4) save the page. The demo modifies data/pages/start.txt so save a copy for restoration. Login with admin/admin if needed.
Claude Code will create a playbook directory with this structure:
demo/
edit-page/
edit-page.yaml ← playbook
fixtures/ ← copies of files to restore during setup
audio/ ← TTS files (generated)
video-raw/ ← raw recording (generated)
demo.mp4 ← final output (generated)
The fixtures/ directory holds copies of files that the demo modifies.
Setup steps copy them back before each run so the demo is always
repeatable.
You can also create the playbook yourself and ask Claude Code to fill in the actions:
# demo/edit-page/edit-page.yaml
app:
url: http://localhost:8080/wiki
setup:
- run: cp demo/edit-page/fixtures/start.txt data/pages/start.txt
- type: click
target: { role: link, name: "Login" }
if:
hidden: ".user-info"
segments:
- id: intro
narration: "Welcome to our wiki. Let's edit a page."
intent: "show the start page"
actions:
- type: wait
duration: 2000
- id: open-editor
narration: "Click the edit button to open the editor."
intent: "click the edit button"
actions: []
Fill in the actions for my demo playbook at demo/edit-page/edit-page.yaml
The skill file teaches Claude Code to run these commands automatically, but you can also run them directly:
<skill-directory>/ndemo <command>
| Command | Description |
|---|---|
ndemo open <playbook> | Launch a headed browser daemon and navigate to the app |
ndemo close | Shut down the browser daemon |
ndemo reset | Navigate back to the app URL with a fresh state |
ndemo page-state | Print the current page's accessibility tree |
ndemo page-state --screenshot | Same, plus save a screenshot |
ndemo play <playbook> | Play all segments in the live browser |
ndemo play <playbook> --segment <id> | Play just one segment (rewinds first) |
ndemo play <playbook> --from <id> | Play from a segment to the end |
ndemo play <playbook> --from <id> --to <id> | Play a range of segments |
ndemo play <playbook> --audio | Play with TTS narration (combinable with other flags) |
ndemo render <playbook> | Full pipeline: TTS, headless replay, merge to mp4 |
ndemo render <playbook> --output path.mp4 | Render to a specific output path |
ndemo doctor | Check that all dependencies are installed |
app:
url: https://myapp.dev # required
viewport: # optional, defaults shown
width: 1920
height: 1080
scale: 2 # device scale factor
zoom: 1.25 # CSS zoom
colorScheme: light # light or dark
setup: # optional steps to run on load
- run: cp fixtures/page.txt data/ # shell commands for file ops
- type: click # browser actions
target: { role: button, name: "Login" }
if: # conditional (skip if not met)
visible: ".login-form"
titleCard: # optional, adds a title frame
title: "My Demo"
subtitle: "Optional subtitle" # optional
duration: 3000 # milliseconds (default 3000)
tts: # optional, defaults shown
provider: openai
voice: alloy
speed: 1.0
recording: # optional, defaults shown
outputDir: . # relative to playbook directory
fps: 30
segments:
- id: segment-name # lowercase, hyphens, unique
narration: "What the viewer hears."
intent: "What happens on screen (for Claude Code's reference)."
timing: after # after (default) or parallel
actions:
- type: click
target: { role: button, name: "Settings" }
done:
visible: ".settings-panel"
- type: wait
duration: 2000
| Type | Required fields | Notes |
|---|---|---|
click | target | |
type | target, text | delay: 60-100 for human-like typing |
hover | target | |
scroll | target | Scrolls the element into view |
wait | duration (ms) | Pause so the viewer can see what happened |
press | key | Keyboard key, e.g. Enter, Escape |
select | target, option | Dropdown selection |
Targets tell Playwright how to find an element. Use the output of ndemo page-state to pick the right one:
target: { role: button, name: "Settings" } # accessibility role + name
target: { label: "Email address" } # form label
target: { placeholder: "Search..." } # input placeholder
target: { text: "Learn more" } # visible text
target: { testId: "submit-btn" } # data-testid attribute
target: { selector: "#my-element" } # CSS selector (last resort)
Every action that changes the page should have a done condition so the next action waits for the page to be ready:
done:
visible: ".panel" # element appears
hidden: ".spinner" # element disappears
networkIdle: true # no pending network requests
stable: 500 # DOM unchanged for 500ms
url: "**/settings" # URL matches pattern
text: # element contains text
selector: ".status"
has: "Saved"
attribute: # element has attribute value
selector: html
name: data-theme
value: dark
Claude Code (the agent)
├── reads SKILL.md (skill file) for workflow
├── reads the web app's source for context
├── edits playbook YAML
└── runs ndemo CLI commands
│
├── open ──── launches browser daemon
├── page-state ── reads accessibility tree
├── play ──── executes segments in live browser
├── render ── TTS + headless replay + merge
└── close ─── kills browser daemon
The toolkit deliberately avoids building its own agent loop, conversation manager, retry logic, or element discovery. Claude Code already does all of that — the skill file just teaches it the workflow.
MIT
32 commits
17 commits
TypeScript
88.7%
JavaScript
11.3%
A claude skill to create a narrated demo video of a web application
TypeScript
13
49 commits
updated Mar 30, 2026
A CLI toolkit and Claude Code skill for creating narrated demo videos of web applications. You describe what to show, Claude Code drives the browser, and the toolkit renders a polished mp4 with voiceover.
Claude Code is the agent. The toolkit provides browser management, page inspection, segment playback, and video rendering as CLI commands. The skill file (SKILL.md) teaches Claude Code how to use them.
You: "Create a demo showing the new dashboard filters"
→ Claude Code writes the playbook, opens the browser,
authors actions by inspecting the page, tests each
segment, and renders the final mp4.
Add this repo as a skill in your project's .claude/settings.json:
{
"skills": [
"https://github.com/splitbrain/ndemo"
]
}
Clone directly into a Claude Code skills directory:
# Project-level (this project only)
git clone https://github.com/splitbrain/ndemo .claude/skills/ndemo
# Personal (available in all projects)
git clone https://github.com/splitbrain/ndemo ~/.claude/skills/ndemo
This is useful during development — you can edit the skill files and the CLI source directly, and Claude Code picks up changes immediately.
You can also symlink an existing clone:
ln -s /path/to/your/ndemo ~/.claude/skills/ndemo
Either way, the skill file tells Claude Code how to build the toolkit and install Playwright on first use. Everything runs from within the skill's own directory — nothing gets copied into your project.
In Claude Code, just say:
Create a narrated demo of my app at http://localhost:3000
Claude Code will:
Be specific about what the demo should show, what state needs restoring, and how to authenticate:
Create a narrated demo for editing a wiki page. The app runs at http://localhost:8080/wiki. The demo should: (1) show the start page, (2) click edit, (3) type some text, (4) save the page. The demo modifies data/pages/start.txt so save a copy for restoration. Login with admin/admin if needed.
Claude Code will create a playbook directory with this structure:
demo/
edit-page/
edit-page.yaml ← playbook
fixtures/ ← copies of files to restore during setup
audio/ ← TTS files (generated)
video-raw/ ← raw recording (generated)
demo.mp4 ← final output (generated)
The fixtures/ directory holds copies of files that the demo modifies.
Setup steps copy them back before each run so the demo is always
repeatable.
You can also create the playbook yourself and ask Claude Code to fill in the actions:
# demo/edit-page/edit-page.yaml
app:
url: http://localhost:8080/wiki
setup:
- run: cp demo/edit-page/fixtures/start.txt data/pages/start.txt
- type: click
target: { role: link, name: "Login" }
if:
hidden: ".user-info"
segments:
- id: intro
narration: "Welcome to our wiki. Let's edit a page."
intent: "show the start page"
actions:
- type: wait
duration: 2000
- id: open-editor
narration: "Click the edit button to open the editor."
intent: "click the edit button"
actions: []
Fill in the actions for my demo playbook at demo/edit-page/edit-page.yaml
The skill file teaches Claude Code to run these commands automatically, but you can also run them directly:
<skill-directory>/ndemo <command>
| Command | Description |
|---|---|
ndemo open <playbook> | Launch a headed browser daemon and navigate to the app |
ndemo close | Shut down the browser daemon |
ndemo reset | Navigate back to the app URL with a fresh state |
ndemo page-state | Print the current page's accessibility tree |
ndemo page-state --screenshot | Same, plus save a screenshot |
ndemo play <playbook> | Play all segments in the live browser |
ndemo play <playbook> --segment <id> | Play just one segment (rewinds first) |
ndemo play <playbook> --from <id> | Play from a segment to the end |
ndemo play <playbook> --from <id> --to <id> | Play a range of segments |
ndemo play <playbook> --audio | Play with TTS narration (combinable with other flags) |
ndemo render <playbook> | Full pipeline: TTS, headless replay, merge to mp4 |
ndemo render <playbook> --output path.mp4 | Render to a specific output path |
ndemo doctor | Check that all dependencies are installed |
app:
url: https://myapp.dev # required
viewport: # optional, defaults shown
width: 1920
height: 1080
scale: 2 # device scale factor
zoom: 1.25 # CSS zoom
colorScheme: light # light or dark
setup: # optional steps to run on load
- run: cp fixtures/page.txt data/ # shell commands for file ops
- type: click # browser actions
target: { role: button, name: "Login" }
if: # conditional (skip if not met)
visible: ".login-form"
titleCard: # optional, adds a title frame
title: "My Demo"
subtitle: "Optional subtitle" # optional
duration: 3000 # milliseconds (default 3000)
tts: # optional, defaults shown
provider: openai
voice: alloy
speed: 1.0
recording: # optional, defaults shown
outputDir: . # relative to playbook directory
fps: 30
segments:
- id: segment-name # lowercase, hyphens, unique
narration: "What the viewer hears."
intent: "What happens on screen (for Claude Code's reference)."
timing: after # after (default) or parallel
actions:
- type: click
target: { role: button, name: "Settings" }
done:
visible: ".settings-panel"
- type: wait
duration: 2000
| Type | Required fields | Notes |
|---|---|---|
click | target | |
type | target, text | delay: 60-100 for human-like typing |
hover | target | |
scroll | target | Scrolls the element into view |
wait | duration (ms) | Pause so the viewer can see what happened |
press | key | Keyboard key, e.g. Enter, Escape |
select | target, option | Dropdown selection |
Targets tell Playwright how to find an element. Use the output of ndemo page-state to pick the right one:
target: { role: button, name: "Settings" } # accessibility role + name
target: { label: "Email address" } # form label
target: { placeholder: "Search..." } # input placeholder
target: { text: "Learn more" } # visible text
target: { testId: "submit-btn" } # data-testid attribute
target: { selector: "#my-element" } # CSS selector (last resort)
Every action that changes the page should have a done condition so the next action waits for the page to be ready:
done:
visible: ".panel" # element appears
hidden: ".spinner" # element disappears
networkIdle: true # no pending network requests
stable: 500 # DOM unchanged for 500ms
url: "**/settings" # URL matches pattern
text: # element contains text
selector: ".status"
has: "Saved"
attribute: # element has attribute value
selector: html
name: data-theme
value: dark
Claude Code (the agent)
├── reads SKILL.md (skill file) for workflow
├── reads the web app's source for context
├── edits playbook YAML
└── runs ndemo CLI commands
│
├── open ──── launches browser daemon
├── page-state ── reads accessibility tree
├── play ──── executes segments in live browser
├── render ── TTS + headless replay + merge
└── close ─── kills browser daemon
The toolkit deliberately avoids building its own agent loop, conversation manager, retry logic, or element discovery. Claude Code already does all of that — the skill file just teaches it the workflow.
MIT
32 commits
17 commits
TypeScript
88.7%
JavaScript
11.3%