storyshot 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +339 -2
- package/bin/storyshot.mjs +122 -0
- package/package.json +39 -4
- package/skills/storyshot/SKILL.md +229 -0
- package/src/audio.mjs +185 -0
- package/src/auth.mjs +84 -0
- package/src/build.mjs +17 -0
- package/src/check.mjs +140 -0
- package/src/compose.mjs +328 -0
- package/src/config.mjs +125 -0
- package/src/doctor.mjs +67 -0
- package/src/guide.mjs +12 -0
- package/src/init.mjs +73 -0
- package/src/inspect.mjs +186 -0
- package/src/mcp.mjs +224 -0
- package/src/outputs.mjs +56 -0
- package/src/record.mjs +443 -0
- package/src/setup.mjs +63 -0
- package/src/voice.mjs +112 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 boheling
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,340 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Storyshot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ask your coding agent for a video of your web app, and get a polished 1080p
|
|
4
|
+
MP4: a promo clip, a narrated tutorial, a lesson, or step-by-step GIFs for your
|
|
5
|
+
docs.
|
|
6
|
+
|
|
7
|
+
> *"make a 15 s promo video of https://myapp.com"*
|
|
8
|
+
|
|
9
|
+
Storyshot gives Cursor, Claude Code and other coding agents what they need to
|
|
10
|
+
do this well. A real browser clicks through your site, and the recording gets
|
|
11
|
+
a smooth cursor, automatic zoom, captions, an optional AI voiceover, music
|
|
12
|
+
and an end card. Loading time is squeezed out, so a 30-second AI job takes a
|
|
13
|
+
second or two on screen.
|
|
14
|
+
|
|
15
|
+
The agent plans, writes and fixes the storyboard. Storyshot does the
|
|
16
|
+
recording and editing, and the same storyboard always renders the same video.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
Open your project in Cursor or Claude Code and paste this into the agent chat:
|
|
21
|
+
|
|
22
|
+
> Set up Storyshot in this project: run `npx -y storyshot setup cursor` (use
|
|
23
|
+
> `setup claude` in Claude Code) and do everything it says. Then make a 15 s
|
|
24
|
+
> promo video of https://myapp.com
|
|
25
|
+
|
|
26
|
+
The agent does the rest:
|
|
27
|
+
- `setup` writes the MCP server config and the Storyshot skill, adds
|
|
28
|
+
`.storyshot/` to `.gitignore`, and downloads the browser it needs.
|
|
29
|
+
- If ffmpeg is missing, setup prints the exact install command for your OS
|
|
30
|
+
(`brew install ffmpeg`, `winget install --id Gyan.FFmpeg -e`,
|
|
31
|
+
`sudo apt-get install -y ffmpeg`). The agent runs it after you approve.
|
|
32
|
+
- The agent reads the skill and starts on your video in the same chat, using
|
|
33
|
+
the CLI.
|
|
34
|
+
|
|
35
|
+
Reload Cursor (or restart Claude Code) when convenient. From the next chat on,
|
|
36
|
+
the agent uses the Storyshot MCP tools automatically, and you can just ask:
|
|
37
|
+
|
|
38
|
+
- *"make a 15 s promo video of https://myapp.com"*, or type `/storyshot`
|
|
39
|
+
- *"make a narrated tutorial showing how to export a report"*
|
|
40
|
+
- *"make a lesson video explaining how the AI editor works"*
|
|
41
|
+
- *"make docs GIFs for the onboarding flow"*
|
|
42
|
+
|
|
43
|
+
`npx storyshot doctor` re-checks everything at any time. Storyshot runs on
|
|
44
|
+
macOS, Linux and Windows.
|
|
45
|
+
|
|
46
|
+
## What happens when you ask
|
|
47
|
+
|
|
48
|
+
1. **The agent looks at your site** (about 5 s). It reads the page, and checks
|
|
49
|
+
whether the flow is behind a login and whether you have a voice API key.
|
|
50
|
+
2. **It confirms the plan with you, once, before spending anything:**
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Here's the plan. Reply "ok", or change any line:
|
|
54
|
+
1. Type: pitch: a 15–30 s promo
|
|
55
|
+
2. Length: about 15 s, 6 shots
|
|
56
|
+
3. Flow: hero → Examples → Try It → AI result → pricing → end card
|
|
57
|
+
4. Voice: none (other options: free local English voice; ElevenLabs, key found in .env)
|
|
58
|
+
5. Music: upbeat · Captions: English · Subtitle file (.srt): no
|
|
59
|
+
6. Format: 16:9, 1080p
|
|
60
|
+
7. Login: not needed
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
3. **If you need to log in**, a browser window opens and you log in there.
|
|
64
|
+
The agent never sees or types your password. See
|
|
65
|
+
[Logged-in apps](#logged-in-apps).
|
|
66
|
+
4. **It writes `videos/<site>-<kind>.yaml`** (kind is promo, tutorial, lesson or docs, plus a topic for tutorials, e.g. `videos/acme-tutorial-export-pdf.yaml` → `.mp4`), using only click targets verified on
|
|
67
|
+
your live pages.
|
|
68
|
+
5. **It renders, looks at the result, and fixes it.** Every render returns a
|
|
69
|
+
contact sheet with one frame per step, plus warnings: a step that is rushed
|
|
70
|
+
or slow, narration that doesn't fit, a caption covering what's being
|
|
71
|
+
clicked, a page still loading. The agent fixes these and re-renders. When
|
|
72
|
+
only text or timing changed, it re-renders in seconds without re-recording.
|
|
73
|
+
6. **It hands you the MP4**, a shot list, and what was sped up. Depending on
|
|
74
|
+
the type, you also get `.srt` subtitles, YouTube chapters, or per-step
|
|
75
|
+
GIFs.
|
|
76
|
+
|
|
77
|
+
The storyboard is a plain file in your repo. Commit it, edit it, and ask the
|
|
78
|
+
agent for changes later ("make step 3 slower", "add a Chinese voiceover").
|
|
79
|
+
After your next release, re-render it and you have an up-to-date video.
|
|
80
|
+
|
|
81
|
+
## What you can make
|
|
82
|
+
|
|
83
|
+
| ask for | type (`purpose`) | you get |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| a promo / launch / landing-page clip | `pitch` | 15–30 s, music, captions, tightly cut |
|
|
86
|
+
| a how-to or onboarding video | `tutorial` | narrated, a spotlight on each click, numbered steps, soft music, `.srt` + chapters |
|
|
87
|
+
| a lesson or explainer | `course` | narrated and explains the *why*, no music, `.srt` + chapters |
|
|
88
|
+
| help-center snippets | `docs` | silent, one GIF per step |
|
|
89
|
+
|
|
90
|
+
Any type can also be vertical (9:16) or in another language.
|
|
91
|
+
|
|
92
|
+
## Voice and cost
|
|
93
|
+
|
|
94
|
+
Rendering is free and runs locally. Only the voiceover can cost money:
|
|
95
|
+
|
|
96
|
+
| voice | cost | notes |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| none | free | captions only |
|
|
99
|
+
| `kokoro` | free | local, English only; downloads a ~90 MB model on first use |
|
|
100
|
+
| ElevenLabs | your credits | the most natural voice; many languages including Chinese. Put `ELEVENLABS_API_KEY` in `.env`. |
|
|
101
|
+
| OpenAI | your credits | `OPENAI_API_KEY` |
|
|
102
|
+
|
|
103
|
+
With a paid voice, the agent iterates with the free voice (draft mode) and
|
|
104
|
+
only renders the paid voice once the video looks right. Each line is cached,
|
|
105
|
+
so a change only pays for the lines that changed.
|
|
106
|
+
|
|
107
|
+
## Logged-in apps
|
|
108
|
+
|
|
109
|
+
When the flow is behind a login, the agent says so in its plan. Then:
|
|
110
|
+
|
|
111
|
+
1. A browser window opens (the `login` tool, or `storyshot login <url>`).
|
|
112
|
+
2. You log in there. 2FA and "Sign in with Google" work.
|
|
113
|
+
3. The session is saved to `videos/<site>-login.json` once you're logged in, or
|
|
114
|
+
when you close the window. It includes cookies, localStorage and IndexedDB,
|
|
115
|
+
and the file is added to `.gitignore` because it holds your session.
|
|
116
|
+
4. Every recording starts logged in, so the video never shows a login screen.
|
|
117
|
+
|
|
118
|
+
If the session expires, Storyshot says so ("the saved login has expired, run
|
|
119
|
+
login again") instead of failing in a confusing way. Use a demo account: the
|
|
120
|
+
video shows whatever is on screen.
|
|
121
|
+
|
|
122
|
+
## For agents: tools and playbook
|
|
123
|
+
|
|
124
|
+
The playbook is [`skills/storyshot/SKILL.md`](skills/storyshot/SKILL.md). Agents
|
|
125
|
+
without skill support get the same text from the `storyboard_guide` tool or
|
|
126
|
+
from `storyshot guide`.
|
|
127
|
+
|
|
128
|
+
| MCP tool | what it does |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `storyboard_guide` | The playbook and the full YAML reference. |
|
|
131
|
+
| `inspect_page` | Verified click targets and a screenshot, for a URL or for the state after step N of a storyboard. Reports login walls. |
|
|
132
|
+
| `login` | Opens a browser window where the **user** logs in, and saves the session. |
|
|
133
|
+
| `init_storyboard` | Writes a starter storyboard with verified targets. |
|
|
134
|
+
| `check_storyboard` | Validates in about a second: typos with "did you mean", action and target shapes, `TODO`s, keys, the session file. |
|
|
135
|
+
| `render_video` / `render_status` | Renders in the background and returns the MP4 path, warnings, a per-step speed log and the preview image. If the render isn't done within 45 s, it returns a `job_id`, so client tool timeouts never kill a render. |
|
|
136
|
+
|
|
137
|
+
Other MCP clients: run `npx -y storyshot mcp` (stdio). When run from a git
|
|
138
|
+
checkout, `setup` points to `node /path/to/checkout/bin/storyshot.mjs mcp` instead.
|
|
139
|
+
|
|
140
|
+
When a step fails, the error names the step and action, and says whether the
|
|
141
|
+
target matched nothing or only hidden elements. It also includes a screenshot
|
|
142
|
+
and the clickable targets on the page at that moment, so the agent can fix the
|
|
143
|
+
storyboard in one round.
|
|
144
|
+
|
|
145
|
+
## CLI (scripts, CI, or by hand)
|
|
146
|
+
|
|
147
|
+
Everything the agent does is also a command:
|
|
148
|
+
|
|
149
|
+
| command | what it does |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `storyshot init <url> [--purpose tutorial] [--topic export-pdf] [--storage videos/<site>-login.json]` | Write `videos/<site>-<kind>[-<topic>].yaml` with verified targets; the video gets the same name. |
|
|
152
|
+
| `storyshot login <url> [--save videos/<site>-login.json]` | Log in once in a browser window and save the session. |
|
|
153
|
+
| `storyshot inspect <url> [--storage auth.json]` | List verified targets and save screenshots. |
|
|
154
|
+
| `storyshot inspect videos/acme-promo.yaml --after 3` | The same, for the page after steps 1–3. |
|
|
155
|
+
| `storyshot check videos/acme-promo.yaml` | Validate without opening a browser. |
|
|
156
|
+
| `storyshot render videos/acme-promo.yaml [--draft]` | Record, then compose the video and soundtrack. |
|
|
157
|
+
| `storyshot compose videos/acme-promo.yaml [--draft]` | Rebuild from the last recording, in seconds. |
|
|
158
|
+
| `storyshot record videos/acme-promo.yaml` | Only record. |
|
|
159
|
+
| `storyshot guide` | Print the playbook and the reference. |
|
|
160
|
+
| `storyshot setup cursor\|claude` | Set up the MCP server and the skill in this project, and install or check what Storyshot needs. |
|
|
161
|
+
| `storyshot doctor` | Check Node, ffmpeg, Chromium and voices; install Chromium if it's missing. |
|
|
162
|
+
| `storyshot mcp` | Run the MCP server. |
|
|
163
|
+
|
|
164
|
+
Recordings, previews and voice lines are cached in `.storyshot/` next to the
|
|
165
|
+
YAML. Add it to `.gitignore`. `--draft` swaps a paid voice for the free one and
|
|
166
|
+
writes `<out>.draft.mp4`.
|
|
167
|
+
|
|
168
|
+
To avoid installing anything, use Docker (handy for CI). The image bundles
|
|
169
|
+
Chromium, ffmpeg and CJK fonts:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
docker build -t storyshot .
|
|
173
|
+
docker run --rm -v "$PWD:/work" -v storyshot-cache:/root/.cache --env-file .env storyshot render videos/acme-promo.yaml
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The examples in [`examples/`](examples/) are a 15 s pitch, a narrated tutorial
|
|
177
|
+
and a course lesson for the same app.
|
|
178
|
+
|
|
179
|
+
## Storyboard reference
|
|
180
|
+
|
|
181
|
+
The agent writes this file for you; this reference is for reading or editing it
|
|
182
|
+
by hand.
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
url: https://example.com
|
|
186
|
+
out: acme-promo.mp4 # relative to this file; default: <file name>.mp4
|
|
187
|
+
viewport: { width: 1600, height: 900 } # CSS px the site is laid out at
|
|
188
|
+
output: { width: 1920, height: 1080, fps: 30 } # same aspect ratio; use 1080x1920 with a 900x1600 viewport for vertical
|
|
189
|
+
theme: { accent: "#d9f75b", ink: "#0f172a", font: Inter }
|
|
190
|
+
music: { bpm: 120, volume: 1 } # or { file: track.mp3 }, or false
|
|
191
|
+
sfx: true # click / whoosh / riser sounds
|
|
192
|
+
voice: { provider: elevenlabs, voice: hpp4J3VqNfWAUOO0d1Us } # optional voiceover, see below
|
|
193
|
+
storageState: auth.json # optional Playwright login state
|
|
194
|
+
captionPosition: bottom # or top; can also be set per step. A caption that would cover a step's click target or zoom subject moves to the other side automatically
|
|
195
|
+
purpose: pitch # pitch | tutorial | course | docs (sets the defaults below)
|
|
196
|
+
highlight: false # spotlight targets before click/type (per step or per action too)
|
|
197
|
+
stepNumbers: false # numbered caption badges
|
|
198
|
+
subtitles: false # write <out>.srt (narration, else captions)
|
|
199
|
+
chapters: false # write <out>.chapters.txt for YouTube
|
|
200
|
+
clips: null # gif | webm | mp4: one file per step in <out>-steps/
|
|
201
|
+
maxWait: 1.0 # with duration: auto, the longest a wait is shown
|
|
202
|
+
|
|
203
|
+
setup: # runs before recording starts
|
|
204
|
+
- dismiss: { role: button, name: Accept cookies }
|
|
205
|
+
|
|
206
|
+
steps:
|
|
207
|
+
- caption: Turn PDFs into **editable PowerPoint** # **bold** uses the accent color
|
|
208
|
+
say: Turn any PDF into an editable PowerPoint. # voiceover line (needs `voice`)
|
|
209
|
+
duration: 2.4 # seconds in the final video, or `auto` (fit the narration)
|
|
210
|
+
zoom: { target: h1, scale: 1.2, start: 0.2, end: 2.4, ramp: [1.0, 0.4], offset: { y: 40 } }
|
|
211
|
+
actions:
|
|
212
|
+
- move: { x: 0.5, y: 0.4 } # numbers ≤ 1 are viewport fractions
|
|
213
|
+
- click: { role: button, name: Try it }
|
|
214
|
+
- caption: AI does the work
|
|
215
|
+
duration: 1.3
|
|
216
|
+
sfx: riser # rising sound + chime (or `chime`)
|
|
217
|
+
actions:
|
|
218
|
+
- waitFor: { text: Done, timeout: 120000 } # waiting is compressed automatically
|
|
219
|
+
- hidden: true # cut, not shown in the video
|
|
220
|
+
actions:
|
|
221
|
+
- goto: /
|
|
222
|
+
|
|
223
|
+
end_card:
|
|
224
|
+
logo: { alt: Company logo } # captured from the page, or a path to an image
|
|
225
|
+
title: Acme
|
|
226
|
+
subtitle: Ship **faster**
|
|
227
|
+
cta: Start free at acme.com
|
|
228
|
+
duration: 1.0
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Steps that share a caption keep it on screen across both steps.
|
|
232
|
+
|
|
233
|
+
### Targets
|
|
234
|
+
|
|
235
|
+
A target is either a CSS / Playwright selector string (`h1`, `nav a[href="#pricing"]`)
|
|
236
|
+
or an object with one of these fields:
|
|
237
|
+
|
|
238
|
+
`role` + `name` · `text` · `label` · `placeholder` · `alt` · `testId` · `css`
|
|
239
|
+
|
|
240
|
+
You can add `exact: true`, `last: true` or `nth: 2` to any of them.
|
|
241
|
+
|
|
242
|
+
### Actions
|
|
243
|
+
|
|
244
|
+
| action | example |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `click` | `click: { role: button, name: Convert, ms: 800, offset: { x: 0.3, y: 0.5 } }` |
|
|
247
|
+
| `move` / `hover` | `move: { text: "$0" }` or `move: { x: 0.5, y: 0.4, ms: 1200 }` |
|
|
248
|
+
| `type` | `type: { target: { label: Email }, text: hi@acme.com, delay: 60 }`, or `type: hello` to type into whatever has focus |
|
|
249
|
+
| `drag` | `drag: { from: { css: ".slider" }, by: [{ x: -300 }, { x: 450 }] }` (several moves while held), or `to: <target or point>` |
|
|
250
|
+
| `press` | `press: Enter`, `press: "^"`, `press: { key: Meta+K, show: true }`. When highlighting, shortcuts and special keys show an on-screen badge; plain characters don't. `show: false` hides it. |
|
|
251
|
+
| `pause` | `pause: 700` (ms) |
|
|
252
|
+
| `waitFor` | `waitFor: { role: button, name: Done, timeout: 120000 }` |
|
|
253
|
+
| `waitForUrl` | `waitForUrl: "**/dashboard"` |
|
|
254
|
+
| `networkIdle` | `networkIdle: true` |
|
|
255
|
+
| `goto` | `goto: /pricing` |
|
|
256
|
+
| `scrollTo` | `scrollTo: { css: "#pricing", ms: 1200 }` |
|
|
257
|
+
| `scroll` | `scroll: 600` |
|
|
258
|
+
| `dismiss` | `dismiss: { role: button, name: Close }` (clicks only if visible) |
|
|
259
|
+
|
|
260
|
+
`ms` sets how long the cursor takes to travel to the target (`dragMs` for each
|
|
261
|
+
drag move). `highlight: true/false` on `click`, `type` or `move` overrides the
|
|
262
|
+
step's spotlight setting.
|
|
263
|
+
|
|
264
|
+
After each `click`, `goto`, `waitFor`, `waitForUrl`, `networkIdle` and
|
|
265
|
+
`press: Enter`, Storyshot waits up to 5 s for the images on screen to load and
|
|
266
|
+
decode. That time counts as waiting, so it is compressed in the video. Compose
|
|
267
|
+
warns when a step shows near-blank frames, such as an empty page with a
|
|
268
|
+
loading spinner.
|
|
269
|
+
|
|
270
|
+
### `duration: auto`
|
|
271
|
+
|
|
272
|
+
The cursor moves in real time. Waits are shown for at most `maxWait` seconds.
|
|
273
|
+
If the step's narration needs more time, the wait is first shown longer
|
|
274
|
+
(slower), and then the last frame is held until the line finishes. Nothing is
|
|
275
|
+
ever rushed, and the voice is never cut off. Use it for tutorials; for tightly
|
|
276
|
+
cut promos, use fixed durations.
|
|
277
|
+
|
|
278
|
+
Targets only ever match **visible** elements, so hidden copies (closed menus,
|
|
279
|
+
`<option>`s, mobile duplicates) are ignored. Elements below the fold are
|
|
280
|
+
scrolled into view automatically, including in apps that scroll an inner
|
|
281
|
+
container instead of the window. `dismiss` is a no-op when the element isn't
|
|
282
|
+
there, so it's safe for banners that only sometimes appear.
|
|
283
|
+
|
|
284
|
+
In-page anchor links (`href="#..."`) scroll smoothly instead of jumping.
|
|
285
|
+
|
|
286
|
+
### Zoom
|
|
287
|
+
|
|
288
|
+
| field | meaning |
|
|
289
|
+
|---|---|
|
|
290
|
+
| `target` | What to center on: a target, or a point `{ x: 0.5, y: 0.3 }`. If a step action used the same target, its position at that moment is used (so you can zoom on a button that disappears after the click). Otherwise it is measured at the start of the step when the zoom begins in the first half, or at the end of the step otherwise. |
|
|
291
|
+
| `scale` | Zoom factor (default 1.3). 1.2–1.5 works well. |
|
|
292
|
+
| `start`, `end` | Seconds from the start of the step. The camera eases in from 1× at `start` and is back at 1× at `end` (default: the step's end). `end` may run past the step, e.g. to hold the zoom through a click. |
|
|
293
|
+
| `ramp` | Ease-in / ease-out seconds: `0.5` or `[in, out]`. |
|
|
294
|
+
| `offset` | Moves the camera center from the target's center, in viewport CSS px. `{ y: -120 }` centers 120 px above the target. |
|
|
295
|
+
|
|
296
|
+
The camera never shows anything outside the frame. When a target sits near an
|
|
297
|
+
edge, the zoomed view is clamped to that edge, so the target is off-center
|
|
298
|
+
rather than cut off.
|
|
299
|
+
|
|
300
|
+
### Voiceover
|
|
301
|
+
|
|
302
|
+
Add a `voice` block, then give steps a `say:` line (and optionally
|
|
303
|
+
`end_card.say`). Each line starts as its step begins (`sayAt` shifts it) and may
|
|
304
|
+
run until the next line. A line that is up to 20% too long is sped up slightly.
|
|
305
|
+
A line longer than that is queued after the previous line, and you get a
|
|
306
|
+
warning telling you to shorten it. Music ducks under the voice, and the final
|
|
307
|
+
mix is normalized to -14 LUFS.
|
|
308
|
+
|
|
309
|
+
| provider | key (env or `.env`) | notes |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| `kokoro` (default) | none | Local and free; English voices only (`af_heart`, `am_michael`, `bf_emma`, …). Needs `npm i kokoro-js`; the first run downloads an ~90 MB model. |
|
|
312
|
+
| `elevenlabs` | `ELEVENLABS_API_KEY` | Most natural, many languages including Chinese. `voice` is a voice ID; `model` defaults to `eleven_multilingual_v2`. The key needs Text to Speech access, plus Voices → Read to list voices. |
|
|
313
|
+
| `openai` | `OPENAI_API_KEY` | `voice: alloy`, `model: gpt-4o-mini-tts`; optional `instructions` for tone. |
|
|
314
|
+
| `say` | none | macOS built-in voices, robotic; fine for drafts. |
|
|
315
|
+
|
|
316
|
+
Other fields: `speed` (default 1), `volume` (default 1), and `pronounce`, a
|
|
317
|
+
map of word → spoken form used only for TTS, e.g.
|
|
318
|
+
`pronounce: { pxGenius: P X Genius, PPTX: P P T X }`. Subtitles keep the real
|
|
319
|
+
spelling. Synthesized lines
|
|
320
|
+
are cached in `.storyshot/tts-cache/`, so `compose` only pays for lines you
|
|
321
|
+
changed. Write names the way they should be pronounced (`say: P X Genius.`),
|
|
322
|
+
and spell out prices (`eleven ninety-nine`) in `pronounce` or in the line itself. At normal pace that comes to
|
|
323
|
+
about 2.5 words per second, or roughly 35 words for a 15 s video.
|
|
324
|
+
|
|
325
|
+
### End card
|
|
326
|
+
|
|
327
|
+
Every field is optional; leave out `end_card` entirely for no end card.
|
|
328
|
+
|
|
329
|
+
| field | meaning |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `logo` | A target on the page (captured as an image, e.g. `{ alt: Acme logo }`) or a path to an image file. If the logo image already contains the wordmark, leave out `title`. |
|
|
332
|
+
| `title` | Large product name next to the logo. |
|
|
333
|
+
| `subtitle` | One line; `**bold**` gets an accent highlight. |
|
|
334
|
+
| `cta` | Dark call-to-action pill, e.g. `Start free at acme.com`. |
|
|
335
|
+
| `duration` | Seconds (default 1.0). |
|
|
336
|
+
| `say` | Voiceover line played over the end card. |
|
|
337
|
+
|
|
338
|
+
## License
|
|
339
|
+
|
|
340
|
+
MIT
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { loadConfig, loadEnv } from '../src/config.mjs';
|
|
4
|
+
import { inspect } from '../src/inspect.mjs';
|
|
5
|
+
import { build } from '../src/build.mjs';
|
|
6
|
+
import { check, formatCheck } from '../src/check.mjs';
|
|
7
|
+
|
|
8
|
+
const USAGE = `storyshot — turn a YAML storyboard into a product demo, tutorial or course video
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
storyshot init <url> [--purpose pitch|tutorial|course|docs] [--topic export-pdf] [--storage <site>-login.json]
|
|
12
|
+
write a starter storyboard using verified targets from the page,
|
|
13
|
+
named by type: videos/<site>-promo|tutorial|lesson|docs[-<topic>].yaml
|
|
14
|
+
storyshot login <url> [--save videos/<site>-login.json]
|
|
15
|
+
open a browser window to log in once; saves the session for
|
|
16
|
+
\`storageState:\` (and adds it to .gitignore)
|
|
17
|
+
storyshot inspect <url> [--storage <site>-login.json]
|
|
18
|
+
list a page's headings, sections and verified click targets
|
|
19
|
+
storyshot inspect <storyboard.yaml> [--after N]
|
|
20
|
+
same, for the page as it is after the storyboard's steps 1..N
|
|
21
|
+
storyshot check <storyboard.yaml> validate the storyboard in a second (no browser)
|
|
22
|
+
storyshot render <storyboard.yaml> record the site, then compose video + soundtrack
|
|
23
|
+
storyshot compose <storyboard.yaml> re-compose from the last recording
|
|
24
|
+
(after changing captions, durations, zoom, music, voice, end card)
|
|
25
|
+
storyshot record <storyboard.yaml> only (re-)record the browser session
|
|
26
|
+
storyshot guide print the agent playbook + storyboard reference
|
|
27
|
+
storyshot setup cursor|claude add the MCP server + playbook to this project for Cursor / Claude Code,
|
|
28
|
+
and install / check everything Storyshot needs
|
|
29
|
+
storyshot doctor check (and install what it can of) Chromium, ffmpeg, voices
|
|
30
|
+
storyshot mcp run as an MCP server (stdio)
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--draft (render/compose) use the free local voice instead of a paid TTS provider and write
|
|
34
|
+
<out>.draft.mp4; when it looks right, run \`compose\` without --draft (reuses the recording)
|
|
35
|
+
--viewport 1600x900 (inspect, init)
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const [cmd, arg] = args;
|
|
40
|
+
const flag = (name) => { const i = args.indexOf(`--${name}`); return i > 0 ? args[i + 1] : undefined; };
|
|
41
|
+
const NEEDS_ARG = ['login', 'init', 'inspect', 'check', 'render', 'compose', 'record', 'setup'];
|
|
42
|
+
if (![...NEEDS_ARG, 'mcp', 'guide', 'doctor'].includes(cmd) || (NEEDS_ARG.includes(cmd) && !arg)) {
|
|
43
|
+
process.stdout.write(USAGE);
|
|
44
|
+
process.exit(['--help', '-h', 'help'].includes(cmd) ? 0 : 1);
|
|
45
|
+
}
|
|
46
|
+
const viewport = () => { const [w, h] = (flag('viewport') ?? '1600x900').split('x').map(Number); return { width: w, height: h }; };
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (cmd === 'mcp') {
|
|
50
|
+
await import('../src/mcp.mjs');
|
|
51
|
+
} else if (cmd === 'guide') {
|
|
52
|
+
const { guide } = await import('../src/guide.mjs');
|
|
53
|
+
console.log(guide());
|
|
54
|
+
} else if (cmd === 'doctor') {
|
|
55
|
+
const { doctor } = await import('../src/doctor.mjs');
|
|
56
|
+
loadEnv(process.cwd());
|
|
57
|
+
const d = await doctor();
|
|
58
|
+
console.log(d.report);
|
|
59
|
+
if (d.todo.length) { console.log(`\nStill to do — run:\n${d.todo.map((t) => ` ${t}`).join('\n')}`); process.exit(1); }
|
|
60
|
+
} else if (cmd === 'setup') {
|
|
61
|
+
const { setup, cliCommand } = await import('../src/setup.mjs');
|
|
62
|
+
const { doctor } = await import('../src/doctor.mjs');
|
|
63
|
+
const files = setup(arg);
|
|
64
|
+
for (const f of files) console.log(`✔ wrote ${f}`);
|
|
65
|
+
loadEnv(process.cwd());
|
|
66
|
+
console.log('\nChecking what Storyshot needs:');
|
|
67
|
+
const d = await doctor();
|
|
68
|
+
console.log(d.report.replace(/^/gm, ' '));
|
|
69
|
+
const skill = files.find((f) => f.endsWith('SKILL.md'));
|
|
70
|
+
console.log([
|
|
71
|
+
'',
|
|
72
|
+
...(d.todo.length ? ['STILL TO DO (agent: run these yourself, then `' + cliCommand() + ' doctor`):', ...d.todo.map((t) => ` ${t}`), ''] : []),
|
|
73
|
+
'NEXT',
|
|
74
|
+
` Agent: read ${skill} now and follow it for the user's request. Until the storyshot MCP tools`,
|
|
75
|
+
` show up in this session, use the CLI instead: ${cliCommand()} <command>`,
|
|
76
|
+
arg === 'cursor'
|
|
77
|
+
? ' User: reload Cursor (Cmd/Ctrl+Shift+P → "Reload Window") so the storyshot MCP tools load; future chats use them automatically.'
|
|
78
|
+
: ' User: restart Claude Code in this project and approve the storyshot MCP server; future sessions use it automatically.',
|
|
79
|
+
].join('\n'));
|
|
80
|
+
} else if (cmd === 'login') {
|
|
81
|
+
const { login, authFileFor } = await import('../src/auth.mjs');
|
|
82
|
+
const r = await login(arg, { file: flag('save') ?? authFileFor(arg), viewport: viewport() });
|
|
83
|
+
console.log(`✔ saved the login session to ${r.file} (${r.how}, ${r.cookies} cookies)${r.gitignore ? `\n added it to ${r.gitignore}` : ''}` +
|
|
84
|
+
`\n use it: \`storageState: ${r.file.split(/[\\/]/).pop()}\` in the storyboard (path relative to the yaml), or \`--storage ${r.file}\` for inspect/init`);
|
|
85
|
+
} else if (cmd === 'init') {
|
|
86
|
+
const { initStoryboard } = await import('../src/init.mjs');
|
|
87
|
+
const r = await initStoryboard(arg, { purpose: flag('purpose') ?? 'pitch', topic: flag('topic'), file: flag('file'), storageState: flag('storage'), viewport: viewport() });
|
|
88
|
+
console.log(`✔ wrote ${r.file}\n screenshot: ${r.screenshot}\n next: edit it, then \`storyshot check ${r.file}\``);
|
|
89
|
+
} else if (cmd === 'inspect') {
|
|
90
|
+
await (await import('../src/doctor.mjs')).ensureBrowser();
|
|
91
|
+
if (/\.ya?ml$/.test(arg)) {
|
|
92
|
+
const { record } = await import('../src/record.mjs');
|
|
93
|
+
const cfg = loadConfig(arg);
|
|
94
|
+
const after = flag('after');
|
|
95
|
+
console.log((await record(cfg, { log: () => {}, inspectAfter: after != null ? Number(after) : cfg.steps.length })).text);
|
|
96
|
+
} else {
|
|
97
|
+
console.log((await inspect(arg, { viewport: viewport(), storageState: flag('storage') })).text);
|
|
98
|
+
}
|
|
99
|
+
} else if (cmd === 'check') {
|
|
100
|
+
const r = check(arg, { draft: args.includes('--draft') });
|
|
101
|
+
console.log(formatCheck(r));
|
|
102
|
+
process.exit(r.ok ? 0 : 1);
|
|
103
|
+
} else {
|
|
104
|
+
const draft = args.includes('--draft');
|
|
105
|
+
const c = check(arg, { draft });
|
|
106
|
+
if (!c.ok) { console.error(formatCheck(c)); process.exit(1); }
|
|
107
|
+
try { execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' }); } catch {
|
|
108
|
+
throw new Error('storyshot needs ffmpeg on your PATH (macOS: brew install ffmpeg · Ubuntu: apt install ffmpeg · Windows: winget install ffmpeg) — or use the Docker image');
|
|
109
|
+
}
|
|
110
|
+
const r = await build(loadConfig(arg, { draft }), cmd);
|
|
111
|
+
if (r.out) {
|
|
112
|
+
console.log(`\n✔ ${r.out} (${r.duration.toFixed(1)}s, built in ${r.seconds}s)`);
|
|
113
|
+
console.log(` preview: ${r.preview}`);
|
|
114
|
+
for (const f of r.extras ?? []) if (!/-steps[\\/]/.test(f)) console.log(` + ${f}`);
|
|
115
|
+
for (const w of r.warnings) console.log(` ⚠ ${w}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`\n✖ ${e.message}`);
|
|
120
|
+
if (/Executable doesn't exist/.test(e.message)) console.error(' run: npx playwright install chromium');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyshot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn a short YAML storyboard into a polished product demo video of your web app — smooth cursor, auto zoom, captions, music.",
|
|
5
|
+
"author": "boheling <boheling@gmail.com>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/boheling/storyshot.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/boheling/storyshot#readme",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"storyshot": "bin/storyshot.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"src",
|
|
18
|
+
"skills",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"demo",
|
|
26
|
+
"video",
|
|
27
|
+
"playwright",
|
|
28
|
+
"screen-recording",
|
|
29
|
+
"product-demo"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
34
|
+
"playwright": "^1.64.0",
|
|
35
|
+
"yaml": "^2.9.1",
|
|
36
|
+
"zod": "^4.6.5"
|
|
37
|
+
},
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"kokoro-js": "^1.2.1"
|
|
40
|
+
}
|
|
41
|
+
}
|