demo-reel 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/commands/demo-script.md +200 -0
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +8 -0
- package/README.md +208 -85
- package/bin/demo-reel.mjs +98 -0
- package/dist/audio-processor.d.ts +14 -1
- package/dist/audio-processor.d.ts.map +1 -1
- package/dist/audio-processor.js +73 -32
- package/dist/audio-processor.js.map +1 -1
- package/dist/auth.d.ts +6 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +36 -25
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts +26 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +267 -67
- package/dist/cli.js.map +1 -1
- package/dist/commands/index.d.ts +24 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +35 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.d.ts +9 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +26 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/run/all.d.ts +18 -0
- package/dist/commands/run/all.d.ts.map +1 -0
- package/dist/commands/run/all.js +46 -0
- package/dist/commands/run/all.js.map +1 -0
- package/dist/commands/run/default.d.ts +18 -0
- package/dist/commands/run/default.d.ts.map +1 -0
- package/dist/commands/run/default.js +47 -0
- package/dist/commands/run/default.js.map +1 -0
- package/dist/commands/run/single.d.ts +20 -0
- package/dist/commands/run/single.d.ts.map +1 -0
- package/dist/commands/run/single.js +45 -0
- package/dist/commands/run/single.js.map +1 -0
- package/dist/commands/script/build.d.ts +15 -0
- package/dist/commands/script/build.d.ts.map +1 -0
- package/dist/commands/script/build.js +23 -0
- package/dist/commands/script/build.js.map +1 -0
- package/dist/commands/script/fix.d.ts +12 -0
- package/dist/commands/script/fix.d.ts.map +1 -0
- package/dist/commands/script/fix.js +20 -0
- package/dist/commands/script/fix.js.map +1 -0
- package/dist/commands/script/generate.d.ts +14 -0
- package/dist/commands/script/generate.d.ts.map +1 -0
- package/dist/commands/script/generate.js +27 -0
- package/dist/commands/script/generate.js.map +1 -0
- package/dist/commands/script/pipeline.d.ts +21 -0
- package/dist/commands/script/pipeline.d.ts.map +1 -0
- package/dist/commands/script/pipeline.js +34 -0
- package/dist/commands/script/pipeline.js.map +1 -0
- package/dist/commands/script/router.d.ts +30 -0
- package/dist/commands/script/router.d.ts.map +1 -0
- package/dist/commands/script/router.js +128 -0
- package/dist/commands/script/router.js.map +1 -0
- package/dist/commands/script/validate.d.ts +12 -0
- package/dist/commands/script/validate.d.ts.map +1 -0
- package/dist/commands/script/validate.js +21 -0
- package/dist/commands/script/validate.js.map +1 -0
- package/dist/commands/script/voice.d.ts +15 -0
- package/dist/commands/script/voice.d.ts.map +1 -0
- package/dist/commands/script/voice.js +26 -0
- package/dist/commands/script/voice.js.map +1 -0
- package/dist/commands/types.d.ts +32 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/config-loader.d.ts +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +25 -41
- package/dist/config-loader.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +232 -3
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +2 -0
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/interfaces.js +2 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/narration-manifest.d.ts +31 -0
- package/dist/narration-manifest.d.ts.map +1 -0
- package/dist/narration-manifest.js +32 -0
- package/dist/narration-manifest.js.map +1 -0
- package/dist/presets.d.ts +11 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +106 -0
- package/dist/presets.js.map +1 -0
- package/dist/random.d.ts +3 -0
- package/dist/random.d.ts.map +1 -0
- package/dist/random.js +26 -0
- package/dist/random.js.map +1 -0
- package/dist/runner.d.ts +43 -4
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +396 -116
- package/dist/runner.js.map +1 -1
- package/dist/schemas.d.ts +5155 -4522
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +316 -160
- package/dist/schemas.js.map +1 -1
- package/dist/script/assembler.d.ts +26 -0
- package/dist/script/assembler.d.ts.map +1 -0
- package/dist/script/assembler.js +173 -0
- package/dist/script/assembler.js.map +1 -0
- package/dist/script/cli.d.ts +48 -0
- package/dist/script/cli.d.ts.map +1 -0
- package/dist/script/cli.js +168 -0
- package/dist/script/cli.js.map +1 -0
- package/dist/script/crawl-cli.d.ts +3 -0
- package/dist/script/crawl-cli.d.ts.map +1 -0
- package/dist/script/crawl-cli.js +34 -0
- package/dist/script/crawl-cli.js.map +1 -0
- package/dist/script/crawler.d.ts +31 -0
- package/dist/script/crawler.d.ts.map +1 -0
- package/dist/script/crawler.js +193 -0
- package/dist/script/crawler.js.map +1 -0
- package/dist/script/explore.d.ts +54 -0
- package/dist/script/explore.d.ts.map +1 -0
- package/dist/script/explore.js +230 -0
- package/dist/script/explore.js.map +1 -0
- package/dist/script/generator.d.ts +40 -0
- package/dist/script/generator.d.ts.map +1 -0
- package/dist/script/generator.js +209 -0
- package/dist/script/generator.js.map +1 -0
- package/dist/script/index.d.ts +8 -0
- package/dist/script/index.d.ts.map +1 -0
- package/dist/script/index.js +7 -0
- package/dist/script/index.js.map +1 -0
- package/dist/script/timing.d.ts +9 -0
- package/dist/script/timing.d.ts.map +1 -0
- package/dist/script/timing.js +140 -0
- package/dist/script/timing.js.map +1 -0
- package/dist/script/tts.d.ts +49 -0
- package/dist/script/tts.d.ts.map +1 -0
- package/dist/script/tts.js +461 -0
- package/dist/script/tts.js.map +1 -0
- package/dist/script/types.d.ts +1203 -0
- package/dist/script/types.d.ts.map +1 -0
- package/dist/script/types.js +84 -0
- package/dist/script/types.js.map +1 -0
- package/dist/script/voice-cli.d.ts +3 -0
- package/dist/script/voice-cli.d.ts.map +1 -0
- package/dist/script/voice-cli.js +149 -0
- package/dist/script/voice-cli.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/video-handler.d.ts +45 -5
- package/dist/video-handler.d.ts.map +1 -1
- package/dist/video-handler.js +324 -57
- package/dist/video-handler.js.map +1 -1
- package/dist/voice-config.d.ts +95 -0
- package/dist/voice-config.d.ts.map +1 -0
- package/dist/voice-config.js +109 -0
- package/dist/voice-config.js.map +1 -0
- package/package.json +67 -35
- package/src/audio-processor.ts +263 -0
- package/src/auth.ts +392 -0
- package/src/cli.ts +373 -0
- package/src/commands/index.ts +23 -0
- package/src/commands/init.ts +39 -0
- package/src/commands/registry.ts +34 -0
- package/src/commands/run/all.ts +77 -0
- package/src/commands/run/default.ts +78 -0
- package/src/commands/run/single.ts +75 -0
- package/src/commands/script/build.ts +37 -0
- package/src/commands/script/fix.ts +31 -0
- package/src/commands/script/generate.ts +43 -0
- package/src/commands/script/pipeline.ts +58 -0
- package/src/commands/script/router.ts +173 -0
- package/src/commands/script/validate.ts +32 -0
- package/src/commands/script/voice.ts +44 -0
- package/src/commands/types.ts +35 -0
- package/src/config-loader.ts +138 -0
- package/src/index.ts +292 -0
- package/src/interfaces.ts +1 -0
- package/src/narration-manifest.ts +41 -0
- package/src/presets.ts +115 -0
- package/src/random.ts +31 -0
- package/src/runner.ts +1117 -0
- package/src/schemas.ts +518 -0
- package/src/script/assembler.ts +199 -0
- package/src/script/cli.ts +246 -0
- package/src/script/crawl-cli.ts +36 -0
- package/src/script/crawler.ts +237 -0
- package/src/script/explore.ts +312 -0
- package/src/script/generator.ts +256 -0
- package/src/script/index.ts +23 -0
- package/src/script/timing.ts +166 -0
- package/src/script/tts.ts +580 -0
- package/src/script/types.ts +109 -0
- package/src/script/voice-cli.ts +180 -0
- package/src/types.ts +25 -0
- package/src/video-handler.ts +648 -0
- package/src/voice-config.ts +138 -0
- package/templates/dashboard-demo.demo.ts +155 -71
- package/templates/demo-reel.config.ts +39 -43
- package/templates/scenario.demo.ts +26 -28
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
You are helping the user create a demo video script for their web application. This is a collaborative process — you figure out the flow together, then build the script scene by scene.
|
|
2
|
+
|
|
3
|
+
## Modular Video Series Pattern
|
|
4
|
+
|
|
5
|
+
Demo videos are designed as **modular, standalone segments** that also work together as a guided journey. Each video:
|
|
6
|
+
|
|
7
|
+
1. **Is fully independent** — has its own setup steps that create required state from scratch (login, create tenant, create template, etc.)
|
|
8
|
+
2. **Opens with brief context** (~3-5 seconds narration) — "We hebben een template aangemaakt — laten we varianten toevoegen" so standalone viewers aren't lost
|
|
9
|
+
3. **Covers one focused topic** — 30-90 seconds, one clear goal
|
|
10
|
+
4. **Ends on the result** — the viewer sees the accomplished outcome
|
|
11
|
+
|
|
12
|
+
### Example series structure:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
01-create-template.demo.ts
|
|
16
|
+
setup: login → create tenant
|
|
17
|
+
recorded: navigate to templates → create template → open editor
|
|
18
|
+
|
|
19
|
+
02-add-variants.demo.ts
|
|
20
|
+
setup: login → create tenant → create template (fast, off-screen)
|
|
21
|
+
recorded: open template → add variants → show variant list
|
|
22
|
+
|
|
23
|
+
03-editor-walkthrough.demo.ts
|
|
24
|
+
setup: login → create tenant → create template (fast, off-screen)
|
|
25
|
+
recorded: open editor → drag blocks → edit text → preview
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Step 1: Understand the Goal
|
|
29
|
+
|
|
30
|
+
Ask clarifying questions to understand:
|
|
31
|
+
|
|
32
|
+
- **Who is the audience?** New users? Existing customers? Potential buyers?
|
|
33
|
+
- **What's the starting point?** Where in the app does this flow begin?
|
|
34
|
+
- **What's the end state?** What should the viewer see at the end?
|
|
35
|
+
- **Any specifics?** Test data (emails, names, project names), things to emphasize, things to skip
|
|
36
|
+
- **Tone and language?** Casual/formal, which language for narration and content
|
|
37
|
+
- **Part of a series?** Does this build on previous demos? What state already exists?
|
|
38
|
+
|
|
39
|
+
If the user provided $ARGUMENTS, treat it as the goal description and ask follow-up questions.
|
|
40
|
+
|
|
41
|
+
## Step 2: Explore the App
|
|
42
|
+
|
|
43
|
+
Before writing any script, explore the app. Use the explore command:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm demo-reel explore <url>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For authenticated pages, write a quick inline exploration script that logs in, clicks through to the target page, and extracts elements. Always **click through the UI** rather than navigating by URL — SPAs often break with direct URL navigation.
|
|
50
|
+
|
|
51
|
+
Summarize what you find for the user — page structure, available actions, selectors.
|
|
52
|
+
|
|
53
|
+
## Step 3: Identify Setup vs. Recorded Steps
|
|
54
|
+
|
|
55
|
+
Some steps need to happen before recording starts. These are **setup** and **auth**: they run but aren't captured in the video (they run in a separate browser).
|
|
56
|
+
|
|
57
|
+
Ask the user: "Is there anything we need to do before the recording starts?"
|
|
58
|
+
|
|
59
|
+
Common setup steps:
|
|
60
|
+
|
|
61
|
+
- Login flow → use the `auth` block with `loginSteps`, `validate`, `storage`
|
|
62
|
+
- Create a fresh tenant/workspace → keeps demos reproducible and independent
|
|
63
|
+
- Create prerequisite data (templates, users, etc.) that this video builds upon
|
|
64
|
+
- Navigate past landing pages to the actual starting point
|
|
65
|
+
|
|
66
|
+
Also identify **cleanup** steps that should run after recording (e.g., delete tenant).
|
|
67
|
+
|
|
68
|
+
## Step 4: Plan the Recorded Flow
|
|
69
|
+
|
|
70
|
+
Before crawling anything, sketch the high-level flow together. Present it and ask: "Does this flow make sense?"
|
|
71
|
+
|
|
72
|
+
## Step 5: Build Scenes (Page by Page)
|
|
73
|
+
|
|
74
|
+
Work through the flow one page at a time.
|
|
75
|
+
|
|
76
|
+
**a) Crawl it** — explore the page, extract interactive elements. Always click links in the SPA.
|
|
77
|
+
|
|
78
|
+
**b) Draft scene(s):**
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Scene 2: "Laten we een nieuw template aanmaken."
|
|
82
|
+
→ hover [href: "/tenants/demo/templates"]
|
|
83
|
+
→ click [href: "/tenants/demo/templates"]
|
|
84
|
+
→ wait 1500ms
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
CRITICAL: Only use selectors from the crawl output. Never invent selectors. Always hover before clicking for natural cursor movement.
|
|
88
|
+
|
|
89
|
+
**c) Get feedback** — "How's this? Want to adjust the narration or steps?"
|
|
90
|
+
|
|
91
|
+
**d) Move to the next page** — "What happens after this?"
|
|
92
|
+
|
|
93
|
+
## Step 6: Review the Full Script
|
|
94
|
+
|
|
95
|
+
Show the complete script. The user can reorder, cut, merge, tighten narration, adjust pacing.
|
|
96
|
+
|
|
97
|
+
## Step 7: Save
|
|
98
|
+
|
|
99
|
+
Ask for a name. Write the `.demo.ts` config:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { demo } from 'demo-reel';
|
|
103
|
+
|
|
104
|
+
export default demo({
|
|
105
|
+
video: { resolution: "FHD" },
|
|
106
|
+
cursor: "dot",
|
|
107
|
+
motion: "smooth",
|
|
108
|
+
typing: "humanlike",
|
|
109
|
+
timing: "normal",
|
|
110
|
+
outputFormat: "mp4",
|
|
111
|
+
name: "create-template",
|
|
112
|
+
outputDir: "./output",
|
|
113
|
+
|
|
114
|
+
voice: {
|
|
115
|
+
provider: "elevenlabs",
|
|
116
|
+
voice: "CwhRBWXzGAHq8TQ4Fs17",
|
|
117
|
+
pronunciation: { "template": "template" },
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
auth: { /* loginSteps, validate, storage */ },
|
|
121
|
+
setup: [ /* steps to run before recording */ ],
|
|
122
|
+
cleanup: [ /* steps to run after recording */ ],
|
|
123
|
+
|
|
124
|
+
scenes: [
|
|
125
|
+
{ narration: "...", stepIndex: 0, isIntro: true },
|
|
126
|
+
{ narration: "...", stepIndex: 3 },
|
|
127
|
+
],
|
|
128
|
+
|
|
129
|
+
steps: [
|
|
130
|
+
// Scene 1: ...
|
|
131
|
+
{ action: "hover", selector: { ... }, delayAfterMs: 800 },
|
|
132
|
+
{ action: "click", selector: { ... }, delayAfterMs: 1500 },
|
|
133
|
+
// Scene 2: ...
|
|
134
|
+
{ action: "type", selector: { ... }, text: "...", delayAfterMs: 300 },
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Key points:
|
|
140
|
+
|
|
141
|
+
- Use `demo()` (or `defineConfig()`) from `'demo-reel'`
|
|
142
|
+
- `setup` = steps before recording (off-screen), `cleanup` = steps after recording
|
|
143
|
+
- `voice` config inline — voiceover is auto-generated during recording
|
|
144
|
+
- `voice` values are provider-specific:
|
|
145
|
+
- `piper`: `nl_NL-mls-medium`, `en_US-amy-medium`
|
|
146
|
+
- `openai`: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`
|
|
147
|
+
- `elevenlabs`: `21m00Tcm4TlvDq8ikWAM`, `5zhopMftSdRGaPYVcwKK`
|
|
148
|
+
- for a custom Piper model, use `voicePath` instead of `voice`
|
|
149
|
+
- `scenes` map narration text to step indices for subtitles + metadata
|
|
150
|
+
- Always `hover` before `click` for natural cursor movement
|
|
151
|
+
|
|
152
|
+
## Step 8: Record
|
|
153
|
+
|
|
154
|
+
One command does everything (compile, voice, record, subtitles, cleanup):
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pnpm demo-reel <name> --verbose
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This compiles the .demo.ts, generates voiceover (if voice config + narration present), runs the recording in Docker, and outputs video + subtitles + metadata.
|
|
161
|
+
|
|
162
|
+
For local debugging with a visible browser:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pnpm demo-reel-local <name> --headed --verbose
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If it fails, check `output/debug/step-N-failure.png` for a screenshot of the page at the failed step.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Writing Guidelines
|
|
173
|
+
|
|
174
|
+
**Narration:**
|
|
175
|
+
|
|
176
|
+
- Goal-oriented — explain what we're trying to accomplish, not just what we're clicking
|
|
177
|
+
- Concise — 1-3 sentences per scene, 30-90 seconds total
|
|
178
|
+
- Conversational — "Let's", "Notice how", "Here's where we" (or Dutch equivalents)
|
|
179
|
+
- No filler — cut "As you can see" and "Now we're going to"
|
|
180
|
+
- Value-focused — "This saves you from having to..." not just "Click here"
|
|
181
|
+
- For series: open with brief context so standalone viewers aren't lost
|
|
182
|
+
|
|
183
|
+
**Pacing:**
|
|
184
|
+
|
|
185
|
+
- `delayAfterMs: 500-1500` after visual changes
|
|
186
|
+
- `wait: 1500-2500` after page navigations
|
|
187
|
+
- `waitFor` after clicks that trigger loading
|
|
188
|
+
- Longer pauses at "aha" moments
|
|
189
|
+
- Quick pace through routine steps (form filling)
|
|
190
|
+
- Always `hover` before `click` — shows cursor movement
|
|
191
|
+
|
|
192
|
+
**Selectors:** `data-testid` > `id` > `href` > unique `class` > `custom`
|
|
193
|
+
|
|
194
|
+
**Navigation:** Always click elements in the UI. Never use `goto` for internal SPA pages — only for the initial page load after setup.
|
|
195
|
+
|
|
196
|
+
**Scene structure:**
|
|
197
|
+
|
|
198
|
+
- Each scene = one logical beat, not one page
|
|
199
|
+
- First scene: brief context intro for standalone viewers
|
|
200
|
+
- Last scene: linger on the result
|
package/README.md
CHANGED
|
@@ -1,133 +1,254 @@
|
|
|
1
1
|
# Demo Reel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Create professional demo videos from web apps. Code your demos in TypeScript, record via Docker, with automatic voiceover and subtitles.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Quick Start
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **CI/CD Ready**: Generate videos in automated pipelines
|
|
13
|
-
- **Human-Like**: Natural cursor movements and typing
|
|
14
|
-
- **Audio Support**: Mix narration and background music
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -D demo-reel
|
|
11
|
+
```
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
```typescript
|
|
14
|
+
// demos/signup.demo.ts
|
|
15
|
+
import { generate } from "demo-reel";
|
|
16
|
+
|
|
17
|
+
await generate(
|
|
18
|
+
{
|
|
19
|
+
name: "signup",
|
|
20
|
+
outputDir: "./output",
|
|
21
|
+
video: { resolution: "FHD" },
|
|
22
|
+
cursor: "dot",
|
|
23
|
+
motion: "smooth",
|
|
24
|
+
typing: "humanlike",
|
|
25
|
+
timing: "normal",
|
|
26
|
+
outputFormat: "mp4",
|
|
27
|
+
|
|
28
|
+
voice: {
|
|
29
|
+
provider: "elevenlabs", // or "piper" (local/free) or "openai"
|
|
30
|
+
voice: "5zhopMftSdRGaPYVcwKK",
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
auth: {
|
|
34
|
+
loginSteps: [
|
|
35
|
+
{ action: "goto", url: "https://myapp.com/login" },
|
|
36
|
+
{ action: "type", selector: { strategy: "id", value: "email" }, text: "demo@example.com" },
|
|
37
|
+
{ action: "type", selector: { strategy: "id", value: "password" }, text: "password" },
|
|
38
|
+
{ action: "click", selector: { strategy: "class", value: "btn-primary" } },
|
|
39
|
+
],
|
|
40
|
+
validate: {
|
|
41
|
+
protectedUrl: "https://myapp.com/dashboard",
|
|
42
|
+
successIndicator: { strategy: "custom", value: "h1:has-text('Dashboard')" },
|
|
43
|
+
},
|
|
44
|
+
storage: { name: "demo-session", types: ["cookies"] },
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
setup: [
|
|
48
|
+
// Runs before recording (off-screen) — create test data, navigate
|
|
49
|
+
{ action: "goto", url: "https://myapp.com/dashboard" },
|
|
50
|
+
],
|
|
51
|
+
|
|
52
|
+
cleanup: [
|
|
53
|
+
// Runs after recording — delete test data
|
|
54
|
+
],
|
|
55
|
+
|
|
56
|
+
scenes: [
|
|
57
|
+
{ narration: "Welcome to our app. Let's create a new project.", stepIndex: 0, isIntro: true },
|
|
58
|
+
{ narration: "Fill in the details and click Create.", stepIndex: 2 },
|
|
59
|
+
],
|
|
60
|
+
|
|
61
|
+
steps: [
|
|
62
|
+
// Scene 1
|
|
63
|
+
{
|
|
64
|
+
action: "hover",
|
|
65
|
+
selector: { strategy: "testId", value: "new-project" },
|
|
66
|
+
delayAfterMs: 800,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
action: "click",
|
|
70
|
+
selector: { strategy: "testId", value: "new-project" },
|
|
71
|
+
delayAfterMs: 1500,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Scene 2
|
|
75
|
+
{
|
|
76
|
+
action: "type",
|
|
77
|
+
selector: { strategy: "id", value: "name" },
|
|
78
|
+
text: "My Project",
|
|
79
|
+
delayAfterMs: 500,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
action: "hover",
|
|
83
|
+
selector: { strategy: "custom", value: "button[type='submit']" },
|
|
84
|
+
delayAfterMs: 600,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
action: "click",
|
|
88
|
+
selector: { strategy: "custom", value: "button[type='submit']" },
|
|
89
|
+
delayAfterMs: 2000,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{ verbose: true },
|
|
94
|
+
);
|
|
95
|
+
```
|
|
17
96
|
|
|
18
97
|
```bash
|
|
19
|
-
|
|
98
|
+
npx tsx demos/signup.demo.ts
|
|
20
99
|
```
|
|
21
100
|
|
|
22
|
-
|
|
101
|
+
Output: `output/signup.mp4` + `.srt` + `.vtt` + `.meta.json`
|
|
23
102
|
|
|
24
|
-
|
|
103
|
+
## How It Works
|
|
25
104
|
|
|
26
|
-
|
|
105
|
+
1. **You write a `.demo.ts`** — TypeScript config with steps, scenes, and narration
|
|
106
|
+
2. **`generate()` handles everything** — compiles config, generates voiceover (via Docker), records the video (via Docker), outputs subtitles and metadata
|
|
107
|
+
3. **Docker runs the heavy stuff** — Chromium, FFmpeg, Piper TTS are in the Docker image, not on your machine
|
|
27
108
|
|
|
28
|
-
|
|
29
|
-
import { defineConfig } from 'demo-reel';
|
|
30
|
-
|
|
31
|
-
export default defineConfig({
|
|
32
|
-
viewport: { width: 1920, height: 1080 },
|
|
33
|
-
video: { enabled: true, size: { width: 1920, height: 1080 } },
|
|
34
|
-
name: 'my-demo',
|
|
35
|
-
steps: [
|
|
36
|
-
{ action: 'goto', url: 'https://example.com' },
|
|
37
|
-
{ action: 'wait', ms: 2000 },
|
|
38
|
-
],
|
|
39
|
-
});
|
|
40
|
-
```
|
|
109
|
+
### Requirements
|
|
41
110
|
|
|
42
|
-
|
|
111
|
+
- **Docker** — for recording and voice generation
|
|
112
|
+
- **Node.js 18+** — for running your demo scripts
|
|
113
|
+
- **API keys** (optional) — `ELEVENLABS_KEY` or `OPENAI_API_KEY` for cloud TTS
|
|
114
|
+
|
|
115
|
+
## Claude Code Integration
|
|
116
|
+
|
|
117
|
+
Build demo scripts interactively with Claude Code:
|
|
43
118
|
|
|
44
119
|
```bash
|
|
45
|
-
|
|
120
|
+
pnpm demo-reel setup # shows how to install the /demo-script plugin
|
|
46
121
|
```
|
|
47
122
|
|
|
48
|
-
|
|
123
|
+
Then use `/demo-script` in Claude Code:
|
|
49
124
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
# Run default config
|
|
54
|
-
npx demo-reel
|
|
125
|
+
```
|
|
126
|
+
/demo-script https://myapp.com show the signup flow
|
|
127
|
+
```
|
|
55
128
|
|
|
56
|
-
|
|
57
|
-
npx demo-reel onboarding
|
|
129
|
+
Claude crawls your app, builds the script with you scene by scene, and generates the `.demo.ts`.
|
|
58
130
|
|
|
59
|
-
|
|
60
|
-
npx demo-reel --all
|
|
131
|
+
## Configuration
|
|
61
132
|
|
|
62
|
-
|
|
63
|
-
npx demo-reel --dry-run
|
|
133
|
+
### Voice / TTS
|
|
64
134
|
|
|
65
|
-
|
|
66
|
-
|
|
135
|
+
```typescript
|
|
136
|
+
voice: {
|
|
137
|
+
provider: "elevenlabs", // "piper" | "openai" | "elevenlabs"
|
|
138
|
+
voice: "5zhopMftSdRGaPYVcwKK", // provider-specific autocomplete
|
|
139
|
+
speed: 1.0,
|
|
140
|
+
pronunciation: { // word replacements before TTS
|
|
141
|
+
"template": "template", // prevent Dutch pronunciation of English words
|
|
142
|
+
},
|
|
143
|
+
},
|
|
67
144
|
```
|
|
68
145
|
|
|
69
|
-
|
|
146
|
+
Built-in voice values:
|
|
70
147
|
|
|
71
|
-
|
|
148
|
+
- `piper`: `"nl_NL-mls-medium"`, `"en_US-amy-medium"`
|
|
149
|
+
- `openai`: `"alloy"`, `"echo"`, `"fable"`, `"onyx"`, `"nova"`, `"shimmer"`
|
|
150
|
+
- `elevenlabs`: `"21m00Tcm4TlvDq8ikWAM"`, `"5zhopMftSdRGaPYVcwKK"`, `CwhRBWXzGAHq8TQ4Fs17`
|
|
72
151
|
|
|
73
|
-
|
|
74
|
-
1. `demo-reel.config.ts` (default)
|
|
75
|
-
2. `<scenario>.demo.ts` (when specifying scenario name)
|
|
76
|
-
3. All `*.demo.ts` files (with `--all` flag)
|
|
152
|
+
For a custom Piper `.onnx` model, use `voicePath` instead of `voice`:
|
|
77
153
|
|
|
78
|
-
|
|
154
|
+
```typescript
|
|
155
|
+
voice: {
|
|
156
|
+
provider: "piper",
|
|
157
|
+
voicePath: "/models/custom-voice.onnx",
|
|
158
|
+
speed: 1.0,
|
|
159
|
+
}
|
|
160
|
+
```
|
|
79
161
|
|
|
80
|
-
- `
|
|
81
|
-
- `click` - Click an element
|
|
82
|
-
- `hover` - Hover over element
|
|
83
|
-
- `type` - Type text into input
|
|
84
|
-
- `press` - Press a key
|
|
85
|
-
- `scroll` - Scroll element
|
|
86
|
-
- `wait` - Wait for duration
|
|
87
|
-
- `waitFor` - Wait for condition
|
|
162
|
+
Voiceover is auto-generated when `scenes` have `narration` text and `voice` is configured. Cached by content hash — only regenerates when narration changes.
|
|
88
163
|
|
|
89
|
-
###
|
|
164
|
+
### Setup & Cleanup
|
|
90
165
|
|
|
91
166
|
```typescript
|
|
92
|
-
|
|
93
|
-
|
|
167
|
+
setup: [
|
|
168
|
+
// Runs in a separate browser BEFORE recording (not visible in video)
|
|
169
|
+
{ action: "goto", url: "https://myapp.com/" },
|
|
170
|
+
{ action: "click", selector: { strategy: "id", value: "create-workspace" } },
|
|
171
|
+
],
|
|
172
|
+
|
|
173
|
+
cleanup: [
|
|
174
|
+
// Runs AFTER recording (even on failure) — delete test data
|
|
175
|
+
{ action: "goto", url: "https://myapp.com/admin" },
|
|
176
|
+
{ action: "click", selector: { strategy: "custom", value: "button.delete" } },
|
|
177
|
+
],
|
|
178
|
+
```
|
|
94
179
|
|
|
95
|
-
|
|
96
|
-
{ strategy: 'id', value: 'username' }
|
|
180
|
+
Setup and cleanup run in tolerant mode — failed steps are skipped.
|
|
97
181
|
|
|
98
|
-
|
|
99
|
-
{ strategy: 'class', value: 'btn-primary' }
|
|
182
|
+
### Scenes & Subtitles
|
|
100
183
|
|
|
101
|
-
|
|
102
|
-
|
|
184
|
+
```typescript
|
|
185
|
+
scenes: [
|
|
186
|
+
{ narration: "Welcome to our app.", stepIndex: 0, isIntro: true },
|
|
187
|
+
{ narration: "Let's create something.", stepIndex: 3 },
|
|
188
|
+
],
|
|
103
189
|
```
|
|
104
190
|
|
|
105
|
-
|
|
191
|
+
- `narration` — voiceover text (also used for subtitles)
|
|
192
|
+
- `stepIndex` — which step starts this scene
|
|
193
|
+
- `isIntro` — marks the intro scene (used by presentation systems to skip context)
|
|
106
194
|
|
|
107
|
-
|
|
195
|
+
Generates `.srt`, `.vtt` (subtitles) and `.meta.json` (scene timestamps for interactive players).
|
|
108
196
|
|
|
109
|
-
|
|
197
|
+
### Steps
|
|
110
198
|
|
|
111
|
-
|
|
199
|
+
| Action | Description |
|
|
200
|
+
| --------- | -------------------------------------------------------------------- |
|
|
201
|
+
| `goto` | Navigate to URL |
|
|
202
|
+
| `click` | Click an element |
|
|
203
|
+
| `hover` | Hover over element |
|
|
204
|
+
| `type` | Type text into input |
|
|
205
|
+
| `press` | Press a key |
|
|
206
|
+
| `scroll` | Scroll element |
|
|
207
|
+
| `select` | Select dropdown option(s) |
|
|
208
|
+
| `check` | Check/uncheck checkbox |
|
|
209
|
+
| `upload` | Upload files |
|
|
210
|
+
| `drag` | Drag and drop |
|
|
211
|
+
| `wait` | Wait for duration |
|
|
212
|
+
| `waitFor` | Wait for condition (selector, URL, load state, network, JS function) |
|
|
112
213
|
|
|
113
|
-
|
|
214
|
+
### Selectors
|
|
114
215
|
|
|
115
|
-
|
|
216
|
+
```typescript
|
|
217
|
+
{ strategy: "testId", value: "submit-button" } // data-testid
|
|
218
|
+
{ strategy: "id", value: "username" } // id (no #)
|
|
219
|
+
{ strategy: "class", value: "btn-primary" } // class (no .)
|
|
220
|
+
{ strategy: "href", value: "/dashboard" } // link href
|
|
221
|
+
{ strategy: "custom", value: "button:has-text('Save')" } // any CSS selector
|
|
222
|
+
{ strategy: "class", value: "card", index: 2 } // nth match
|
|
223
|
+
```
|
|
116
224
|
|
|
117
|
-
|
|
225
|
+
### Presets
|
|
118
226
|
|
|
119
227
|
```typescript
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
228
|
+
cursor: "dot" | "arrow" | "none";
|
|
229
|
+
motion: "smooth" | "snappy" | "instant";
|
|
230
|
+
typing: "humanlike" | "fast" | "instant";
|
|
231
|
+
timing: "normal" | "fast" | "instant";
|
|
232
|
+
video: {
|
|
233
|
+
resolution: "HD" | "FHD" | "2K" | "4K";
|
|
125
234
|
}
|
|
126
235
|
```
|
|
127
236
|
|
|
128
|
-
##
|
|
237
|
+
## Modular Video Series
|
|
238
|
+
|
|
239
|
+
Demo videos are designed as standalone segments that also work as a series:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
demos/
|
|
243
|
+
└── my-app/
|
|
244
|
+
├── 01-signup/signup.demo.ts # setup: create workspace
|
|
245
|
+
├── 02-create-project/project.demo.ts # setup: create workspace + template
|
|
246
|
+
└── 03-editor/editor.demo.ts # setup: create workspace + template + project
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Each video has its own `setup` that recreates the required state. Later videos have heavier setup. Every video is independently recordable.
|
|
129
250
|
|
|
130
|
-
|
|
251
|
+
## CI/CD
|
|
131
252
|
|
|
132
253
|
```yaml
|
|
133
254
|
name: Generate Demo Videos
|
|
@@ -138,13 +259,15 @@ jobs:
|
|
|
138
259
|
runs-on: ubuntu-latest
|
|
139
260
|
steps:
|
|
140
261
|
- uses: actions/checkout@v4
|
|
141
|
-
-
|
|
142
|
-
- run:
|
|
143
|
-
- run: npx demo
|
|
262
|
+
- uses: pnpm/action-setup@v4
|
|
263
|
+
- run: pnpm install
|
|
264
|
+
- run: npx tsx demos/01-signup/signup.demo.ts
|
|
265
|
+
env:
|
|
266
|
+
ELEVENLABS_KEY: ${{ secrets.ELEVENLABS_KEY }}
|
|
144
267
|
- uses: actions/upload-artifact@v4
|
|
145
268
|
with:
|
|
146
269
|
name: demo-videos
|
|
147
|
-
path: ./
|
|
270
|
+
path: ./output/*.mp4
|
|
148
271
|
```
|
|
149
272
|
|
|
150
273
|
## License
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawn } from "child_process";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const SKILL_URL =
|
|
7
|
+
"https://raw.githubusercontent.com/whit3st/demo-reel/main/.claude-plugin/commands/demo-script.md";
|
|
8
|
+
const DEFAULT_IMAGE = "ghcr.io/whit3st/demo-reel:latest";
|
|
9
|
+
const LOCAL_IMAGE = "demo-reel:latest";
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
if (args[0] === "setup") {
|
|
14
|
+
console.log(`demo-reel setup
|
|
15
|
+
|
|
16
|
+
Install the /demo-script Claude Code plugin to build demo scripts interactively.
|
|
17
|
+
|
|
18
|
+
Option 1 — Claude Code plugin (recommended):
|
|
19
|
+
/plugin marketplace add whit3st/demo-reel
|
|
20
|
+
/plugin install demo-reel@whit3st-demo-reel
|
|
21
|
+
|
|
22
|
+
Option 2 — Manual copy:
|
|
23
|
+
mkdir -p .claude/commands
|
|
24
|
+
curl -sL ${SKILL_URL} -o .claude/commands/demo-script.md
|
|
25
|
+
|
|
26
|
+
Then use /demo-script in Claude Code to create demo videos collaboratively.`);
|
|
27
|
+
} else if (args[0] === "explore") {
|
|
28
|
+
const image = getImage();
|
|
29
|
+
const dockerArgs = [
|
|
30
|
+
"run",
|
|
31
|
+
"--rm",
|
|
32
|
+
"-v",
|
|
33
|
+
`${process.cwd()}:/work:z`,
|
|
34
|
+
"-w",
|
|
35
|
+
"/work",
|
|
36
|
+
"--entrypoint",
|
|
37
|
+
"node",
|
|
38
|
+
image,
|
|
39
|
+
"/app/dist/script/crawl-cli.js",
|
|
40
|
+
...args.slice(1),
|
|
41
|
+
];
|
|
42
|
+
const proc = spawn("docker", dockerArgs, { stdio: "inherit" });
|
|
43
|
+
forwardSignals(proc);
|
|
44
|
+
proc.on("close", (code) => process.exit(code ?? 1));
|
|
45
|
+
} else if (args.length > 0) {
|
|
46
|
+
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/cli.js");
|
|
47
|
+
const proc = spawn(process.execPath, ["--import", "tsx/esm", cliPath, ...args], {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
});
|
|
50
|
+
forwardSignals(proc);
|
|
51
|
+
proc.on("close", (code) => process.exit(code ?? 1));
|
|
52
|
+
} else {
|
|
53
|
+
console.log(`demo-reel — Create demo videos from web apps
|
|
54
|
+
|
|
55
|
+
Usage in your code:
|
|
56
|
+
import { generate } from "demo-reel";
|
|
57
|
+
await generate({ steps: [...], scenes: [...] }, { verbose: true });
|
|
58
|
+
|
|
59
|
+
CLI commands:
|
|
60
|
+
demo-reel setup Install the Claude Code plugin
|
|
61
|
+
demo-reel explore <url> Crawl a page and show selectors`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getImage() {
|
|
65
|
+
try {
|
|
66
|
+
execSync(`docker image inspect ${LOCAL_IMAGE}`, { stdio: "ignore" });
|
|
67
|
+
return LOCAL_IMAGE;
|
|
68
|
+
} catch {
|
|
69
|
+
/* no local image */
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
execSync(`docker image inspect ${DEFAULT_IMAGE}`, { stdio: "ignore" });
|
|
73
|
+
return DEFAULT_IMAGE;
|
|
74
|
+
} catch {
|
|
75
|
+
console.log(`Pulling ${DEFAULT_IMAGE}...`);
|
|
76
|
+
execSync(`docker pull ${DEFAULT_IMAGE}`, { stdio: "inherit" });
|
|
77
|
+
return DEFAULT_IMAGE;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function forwardSignals(child) {
|
|
82
|
+
const signals = ["SIGINT", "SIGTERM"];
|
|
83
|
+
const forward = (signal) => {
|
|
84
|
+
if (!child.killed) {
|
|
85
|
+
child.kill(signal);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (const signal of signals) {
|
|
90
|
+
process.on(signal, () => forward(signal));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
process.on("exit", () => {
|
|
94
|
+
if (!child.killed) {
|
|
95
|
+
child.kill("SIGTERM");
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|