@mineo71/showtime 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-10-13
4
+
5
+ - Narration: `show.say` on any step is spoken by a TTS engine (`audio.voice`: the macOS/Linux
6
+ system voice, OpenAI, ElevenLabs or any command). The video holds the frame until a line ends.
7
+ Lines are cached; editing them needs no new take.
8
+ - `audio.music`: a looped music bed that ducks under the voice. `audio.sounds`: click and
9
+ keystroke sounds (takes now log keystrokes; older takes spread them over each `type` step).
10
+ - Subtitles from the narration: an `.srt` next to the video, or burned in (`audio.subtitles`).
11
+ - `--aspect 9:16 | 1:1` on `render`, `make` and `frames`: cropped cuts with a camera that follows
12
+ the action, from the same take. Stills of a crop go to `frames-9x16/` or `frames-1x1/`.
13
+ - `--format gif | webp` (or an `-o` with that extension) and `--width`; `--no-audio`. Silent
14
+ outputs skip narration (no voice needed) unless subtitles are burned in.
15
+ - `render --json` adds `subtitles`; the brand file accepts `audio`.
16
+ - `lint`: `narration-stretch` flags lines that hold the frame for long.
17
+
3
18
  ## 0.3.0 — 2026-10-13
4
19
 
5
20
  - `showtime frames` also renders a contact sheet (`frames/sheet.png`) of every step.
package/README.md CHANGED
@@ -8,7 +8,8 @@
8
8
 
9
9
  Your AI agent (Claude Code or Codex) writes a short JSON script. showTime clicks through your web app, films it and edits it
10
10
  the way a motion designer would (auto-zoom, a smooth cursor, click ripples, spotlights, captions,
11
- sped-up loading) and hands you a polished 1080p MP4.<br />
11
+ sped-up loading, narration) and hands you a polished video: a 1080p MP4, a 9:16 cut for social or
12
+ a GIF for your README.<br />
12
13
  No timeline editor. No re-take when a caption changes.
13
14
 
14
15
  [![tests](https://img.shields.io/github/actions/workflow/status/mineo71/showTime/ci.yml?branch=main&style=flat-square&label=tests)](https://github.com/mineo71/showTime/actions/workflows/ci.yml)
@@ -26,7 +27,7 @@ No timeline editor. No re-take when a caption changes.
26
27
  <div align="center">
27
28
  <a href="docs/media/demo.mp4"><img src="docs/media/hero.webp" width="860" alt="A demo of a fictional invoicing app, filmed and edited by showTime: the camera zooms on a chart, a cursor fills in a new invoice, a spotlight lands on the total, and the sent invoice appears in the table" /></a>
28
29
  <br />
29
- <sub>Made by <a href="examples/fennel/demo.json">a 39-line script</a>, a <a href="examples/fennel/showtime.config.json">brand file</a> and one command, with no manual editing. <a href="docs/media/demo.mp4">▶ Full-quality MP4</a></sub>
30
+ <sub>Made by <a href="examples/fennel/demo.json">a 39-line script</a>, a <a href="examples/fennel/showtime.config.json">brand file</a> and one command, with no manual editing. <a href="docs/media/demo.mp4">▶ Full-quality MP4, with sound</a></sub>
30
31
  </div>
31
32
 
32
33
  ---
@@ -124,6 +125,27 @@ again.
124
125
  <sub>Title card with your logo, a browser frame with your URL, a gradient backdrop, rounded corners and a shadow. Each of them is a <code>style</code> or <code>meta</code> field.</sub>
125
126
  </td>
126
127
  </tr>
128
+ <tr>
129
+ <td valign="top">
130
+ <pre lang="json">
131
+ "show": { "say": "Send it before lunch." }
132
+
133
+ "audio": {
134
+ "voice": { "engine": "openai", "name": "nova" },
135
+ "music": { "file": "bed.mp3" },
136
+ "sounds": true,
137
+ "subtitles": "burn"
138
+ }
139
+ </pre>
140
+ <b>🎙️ Narration, music and clicks</b><br />
141
+ <sub>Give a step a line and a voice reads it: the built-in voice of macOS or Linux, OpenAI, ElevenLabs or any TTS command. The video holds the frame until the line ends, music ducks under the voice, and subtitles come as an <code>.srt</code> or burned in.</sub>
142
+ </td>
143
+ <td valign="top">
144
+ <img src="docs/media/formats.jpg" alt="The same moment in 16:9, 9:16 and 1:1" /><br />
145
+ <b>📱 Every format from one take</b><br />
146
+ <sub><code>--aspect 9:16</code> or <code>1:1</code> crops the window and a follow camera keeps the action in view. <code>--format gif</code> or <code>webp</code> for READMEs and docs. Nothing is re-recorded.</sub>
147
+ </td>
148
+ </tr>
127
149
  <tr>
128
150
  <td valign="top">
129
151
  <img src="docs/media/outro.jpg" alt="Outro card with logo, title and URL" /><br />
@@ -206,16 +228,19 @@ flowchart LR
206
228
  R --> E["events.json<br/>targets · clicks · idle"]
207
229
  S --> T["timeline.json<br/>camera · cursor · captions · speed"]
208
230
  E --> T
231
+ T --> A["mix.wav<br/>voice · music · clicks"]
209
232
  T --> M["Remotion"]
233
+ A --> M
210
234
  V --> M
211
- M --> O["demo.mp4"]
235
+ M --> O["demo.mp4 · 9:16 · GIF"]
212
236
  ```
213
237
 
214
238
  The core idea is to **separate capture from direction**. `record` drives a real Chromium through
215
239
  the script and writes two things: a clean video with no cursor, and an event log of where every
216
240
  target was, when each click landed and when the page sat idle. A pure function,
217
241
  `buildTimeline(script, events)`, turns that log into speed segments, camera keyframes, cursor
218
- paths, captions and spotlights. Remotion renders the result frame by frame.
242
+ paths, captions and spotlights, and holds the frame wherever a narration line needs more time.
243
+ The soundtrack is mixed from the same timeline, and Remotion renders the result frame by frame.
219
244
 
220
245
  So presentation is cheap to change and recording stays honest: the app really is clicked, typed
221
246
  into and waited on, and everything cinematic is computed afterwards.
@@ -279,6 +304,7 @@ Targets are Playwright selectors: `role=button[name='Save']`, `text=Save`, `#id`
279
304
  | `spotlight` | boolean | dim everything except the target |
280
305
  | `hold` | ms | extra real time after the step (changes the take) |
281
306
  | `speed` | 0.25–16 | playback speed for this step |
307
+ | `say` | string | narration spoken from the start of the step; the next narrated step waits for it |
282
308
 
283
309
  </details>
284
310
 
@@ -319,6 +345,37 @@ Targets are Playwright selectors: `role=button[name='Save']`, `text=Save`, `#id`
319
345
 
320
346
  </details>
321
347
 
348
+ <details>
349
+ <summary><b>Sound</b> (<code>audio</code>)</summary>
350
+
351
+ ```json
352
+ "audio": {
353
+ "voice": { "engine": "system", "name": "Samantha", "speed": 1 },
354
+ "music": { "file": "music/bed.mp3", "volume": 0.25 },
355
+ "sounds": true,
356
+ "subtitles": "file"
357
+ }
358
+ ```
359
+
360
+ | field | default | |
361
+ |----------------------|------------|-|
362
+ | `voice.engine` | `system` | `system` (macOS `say`, Linux `espeak-ng`), `openai`, `elevenlabs`, or `command` |
363
+ | `voice.name` | engine default | voice name (`system`, `openai`), or voice id (`elevenlabs`, required) |
364
+ | `voice.speed` | `1` | 0.5–2 |
365
+ | `voice.model` | engine default | `gpt-4o-mini-tts` (OpenAI), `eleven_multilingual_v2` (ElevenLabs) |
366
+ | `voice.instructions` | none | OpenAI only: how to speak ("warm, confident") |
367
+ | `voice.command` | none | `command` only: a shell command that reads the text on stdin and writes audio to `{out}` (inserted already quoted, so write it bare; also in `$SHOWTIME_OUT`), e.g. `piper -m en_US.onnx -f {out}` |
368
+ | `music` | none | `{ file, volume? }`: a bed under the whole video, looped and ducked under the voice |
369
+ | `sounds` | `false` | click and keystroke sounds |
370
+ | `subtitles` | `file` | `file`: an `.srt` next to the video; `burn`: also on the video (captions move up); `off` |
371
+
372
+ `openai` and `elevenlabs` read `OPENAI_API_KEY` / `ELEVENLABS_API_KEY` from the environment
373
+ (`OPENAI_BASE_URL` points at any OpenAI-compatible server). Each line is synthesized once and
374
+ cached; edits to `say` or `audio` only need `render`. Silent outputs (GIF, WebP, `--no-audio`) skip
375
+ the narration and its pauses, unless subtitles are burned in.
376
+
377
+ </details>
378
+
322
379
  <details>
323
380
  <summary><b>Cards</b> (<code>meta</code>)</summary>
324
381
 
@@ -350,8 +407,8 @@ command in the script's folder (120 s timeout); `request` is an HTTP call that m
350
407
  <details>
351
408
  <summary><b>Brand file</b> (<code>showtime.config.json</code>)</summary>
352
409
 
353
- Put it next to your scripts or in any folder above them; the nearest one applies. Its `style` and
354
- `meta` are defaults, and a script's own fields win key by key.
410
+ Put it next to your scripts or in any folder above them; the nearest one applies. Its `style`,
411
+ `meta` and `audio` are defaults, and a script's own fields win key by key.
355
412
 
356
413
  ```json
357
414
  {
@@ -367,16 +424,17 @@ Put it next to your scripts or in any folder above them; the nearest one applies
367
424
  | command | what it does |
368
425
  |---|---|
369
426
  | `showtime setup [--agent claude\|codex\|all] [--no-browsers]` | download the browsers, link the skill |
370
- | `showtime make <script> [-o out.mp4]` | check → record → render |
427
+ | `showtime make <script> [render options]` | check → record → render |
371
428
  | `showtime check <script> [--json] [--headed]` | rehearse; a screenshot per step; the first failure with an ARIA snapshot |
372
429
  | `showtime record <script> [--headed] [--keep-frames]` | record a take into `.showtime/<name>/take` |
373
- | `showtime render <script> [-o out.mp4]` | render from the last take, without re-recording |
374
- | `showtime frames <script>` | QA stills (intro, one per step, outro) and a contact sheet of them |
430
+ | `showtime render <script> [-o out] [--aspect 16:9\|9:16\|1:1] [--format mp4\|gif\|webp] [--width px] [--no-audio]` | render from the last take, without re-recording |
431
+ | `showtime frames <script> [--aspect …]` | QA stills (intro, one per step, outro) and a contact sheet of them |
375
432
  | `showtime lint <script>` | warnings about captions, pauses and length; timing checks need a take |
376
433
  | `showtime schema` | print the script's JSON Schema |
377
434
 
378
435
  Every command takes `--json` and then prints exactly one JSON document. The video lands next to
379
- the script (`demo/pitch.json` → `demo/pitch.mp4`). Working files live in
436
+ the script (`demo/pitch.json` → `demo/pitch.mp4`, `demo/pitch-9x16.mp4`, `demo/pitch.gif`), with
437
+ `demo/pitch.srt` when it is narrated. Working files live in
380
438
  `.showtime/<name>/`, which is worth adding to your `.gitignore`.
381
439
 
382
440
  ## Tips for a great demo
@@ -390,14 +448,19 @@ the script (`demo/pitch.json` → `demo/pitch.mp4`). Working files live in
390
448
  for anything personal, and `hooks` to reset the data between `check` and `record`.
391
449
  - **Pin the world.** Mock slow or random APIs with `app.mock` and pin the date with `app.clock`,
392
450
  so every take looks the same.
451
+ - **Narrate the why, not the clicks.** One short sentence per moment. If `showtime lint` says a
452
+ line holds the frame for seconds, shorten it or give the step more to show.
393
453
 
394
454
  ## FAQ
395
455
 
396
456
  **Which apps does it work with?** Any web app Chromium can open: a localhost dev server, staging
397
457
  or production. Logged-in flows work through `app.storageState`.
398
458
 
399
- **Does showTime call an AI model?** No. showTime is deterministic and needs no API keys. The AI
400
- part (writing, fixing and reviewing the script) happens in your agent, guided by the skill.
459
+ **Does showTime call an AI model?** Not for directing: the AI part (writing, fixing and reviewing
460
+ the script) happens in your agent, guided by the skill. Narration is text-to-speech. The built-in
461
+ voice of macOS or Linux needs no key and nothing leaves your machine; with `openai` or
462
+ `elevenlabs`, only the narration text is sent to that provider, with your key from the
463
+ environment.
401
464
 
402
465
  **Why not just screen-record?** You can, once. A screen recording can't be re-edited when the copy
403
466
  changes, the cursor is wherever your hand was, and the spinner plays at full length. With
@@ -408,8 +471,9 @@ needs Playwright and ffmpeg. Windows is untested.
408
471
 
409
472
  ## Roadmap
410
473
 
411
- - 🎙️ Narration: text-to-speech per scene, with steps stretched to fit the audio, and burnt-in subtitles.
412
- - 📱 Vertical 9:16 and square 1:1 cuts with a follow-cam, from the same take.
474
+ - 🧑‍🎨 `showtime studio`: a local page with a live player and a timeline to drag captions, zooms and
475
+ speed, saving back to the JSON.
476
+ - 🎥 `showtime record-human <url>`: click through the app yourself and get a script back.
413
477
  - 🎞️ Deterministic frame-by-frame capture in virtual time, for a perfect 60 fps.
414
478
  - 🧩 An MCP server for Claude Desktop, and a desktop studio for timeline tweaks.
415
479
  - 📲 Mobile (iOS Simulator) and desktop-app drivers that produce the same `events.json`.
@@ -360,6 +360,11 @@
360
360
  "type": "number",
361
361
  "minimum": 0.25,
362
362
  "maximum": 16
363
+ },
364
+ "say": {
365
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
366
+ "type": "string",
367
+ "minLength": 1
363
368
  }
364
369
  },
365
370
  "additionalProperties": false
@@ -420,6 +425,11 @@
420
425
  "type": "number",
421
426
  "minimum": 0.25,
422
427
  "maximum": 16
428
+ },
429
+ "say": {
430
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
431
+ "type": "string",
432
+ "minLength": 1
423
433
  }
424
434
  },
425
435
  "additionalProperties": false
@@ -504,6 +514,11 @@
504
514
  "type": "number",
505
515
  "minimum": 0.25,
506
516
  "maximum": 16
517
+ },
518
+ "say": {
519
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
520
+ "type": "string",
521
+ "minLength": 1
507
522
  }
508
523
  },
509
524
  "additionalProperties": false
@@ -564,6 +579,11 @@
564
579
  "type": "number",
565
580
  "minimum": 0.25,
566
581
  "maximum": 16
582
+ },
583
+ "say": {
584
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
585
+ "type": "string",
586
+ "minLength": 1
567
587
  }
568
588
  },
569
589
  "additionalProperties": false
@@ -624,6 +644,11 @@
624
644
  "type": "number",
625
645
  "minimum": 0.25,
626
646
  "maximum": 16
647
+ },
648
+ "say": {
649
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
650
+ "type": "string",
651
+ "minLength": 1
627
652
  }
628
653
  },
629
654
  "additionalProperties": false
@@ -687,6 +712,11 @@
687
712
  "type": "number",
688
713
  "minimum": 0.25,
689
714
  "maximum": 16
715
+ },
716
+ "say": {
717
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
718
+ "type": "string",
719
+ "minLength": 1
690
720
  }
691
721
  },
692
722
  "additionalProperties": false
@@ -753,6 +783,11 @@
753
783
  "type": "number",
754
784
  "minimum": 0.25,
755
785
  "maximum": 16
786
+ },
787
+ "say": {
788
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
789
+ "type": "string",
790
+ "minLength": 1
756
791
  }
757
792
  },
758
793
  "additionalProperties": false
@@ -817,6 +852,11 @@
817
852
  "type": "number",
818
853
  "minimum": 0.25,
819
854
  "maximum": 16
855
+ },
856
+ "say": {
857
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
858
+ "type": "string",
859
+ "minLength": 1
820
860
  }
821
861
  },
822
862
  "additionalProperties": false
@@ -876,6 +916,11 @@
876
916
  "type": "number",
877
917
  "minimum": 0.25,
878
918
  "maximum": 16
919
+ },
920
+ "say": {
921
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
922
+ "type": "string",
923
+ "minLength": 1
879
924
  }
880
925
  },
881
926
  "additionalProperties": false
@@ -952,6 +997,94 @@
952
997
  },
953
998
  "additionalProperties": false
954
999
  },
1000
+ "audio": {
1001
+ "default": {},
1002
+ "type": "object",
1003
+ "properties": {
1004
+ "voice": {
1005
+ "description": "Text-to-speech for show.say",
1006
+ "default": {},
1007
+ "type": "object",
1008
+ "properties": {
1009
+ "engine": {
1010
+ "default": "system",
1011
+ "description": "system: macOS say / Linux espeak-ng; openai and elevenlabs read OPENAI_API_KEY / ELEVENLABS_API_KEY",
1012
+ "type": "string",
1013
+ "enum": [
1014
+ "system",
1015
+ "openai",
1016
+ "elevenlabs",
1017
+ "command"
1018
+ ]
1019
+ },
1020
+ "name": {
1021
+ "description": "Voice name (system, openai) or voice id (elevenlabs)",
1022
+ "type": "string",
1023
+ "minLength": 1
1024
+ },
1025
+ "speed": {
1026
+ "default": 1,
1027
+ "type": "number",
1028
+ "minimum": 0.5,
1029
+ "maximum": 2
1030
+ },
1031
+ "model": {
1032
+ "description": "openai: gpt-4o-mini-tts by default; elevenlabs: eleven_multilingual_v2",
1033
+ "type": "string",
1034
+ "minLength": 1
1035
+ },
1036
+ "instructions": {
1037
+ "description": "openai only: how to speak, e.g. \"warm and confident\"",
1038
+ "type": "string",
1039
+ "minLength": 1
1040
+ },
1041
+ "command": {
1042
+ "description": "command engine: shell command that reads the text on stdin and writes audio to {out}",
1043
+ "type": "string",
1044
+ "minLength": 1
1045
+ }
1046
+ },
1047
+ "additionalProperties": false
1048
+ },
1049
+ "music": {
1050
+ "description": "Music bed under the whole video; ducks while the voice speaks",
1051
+ "type": "object",
1052
+ "properties": {
1053
+ "file": {
1054
+ "type": "string",
1055
+ "minLength": 1,
1056
+ "description": "Audio file, relative to the file that names it"
1057
+ },
1058
+ "volume": {
1059
+ "default": 0.25,
1060
+ "type": "number",
1061
+ "minimum": 0,
1062
+ "maximum": 1
1063
+ }
1064
+ },
1065
+ "required": [
1066
+ "file"
1067
+ ],
1068
+ "additionalProperties": false
1069
+ },
1070
+ "sounds": {
1071
+ "default": false,
1072
+ "description": "Click and keystroke sounds",
1073
+ "type": "boolean"
1074
+ },
1075
+ "subtitles": {
1076
+ "default": "file",
1077
+ "description": "Narration subtitles: an .srt next to the video (file), also drawn on the video (burn), or none",
1078
+ "type": "string",
1079
+ "enum": [
1080
+ "off",
1081
+ "file",
1082
+ "burn"
1083
+ ]
1084
+ }
1085
+ },
1086
+ "additionalProperties": false
1087
+ },
955
1088
  "scenes": {
956
1089
  "minItems": 1,
957
1090
  "type": "array",
@@ -1015,6 +1148,11 @@
1015
1148
  "type": "number",
1016
1149
  "minimum": 0.25,
1017
1150
  "maximum": 16
1151
+ },
1152
+ "say": {
1153
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1154
+ "type": "string",
1155
+ "minLength": 1
1018
1156
  }
1019
1157
  },
1020
1158
  "additionalProperties": false
@@ -1075,6 +1213,11 @@
1075
1213
  "type": "number",
1076
1214
  "minimum": 0.25,
1077
1215
  "maximum": 16
1216
+ },
1217
+ "say": {
1218
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1219
+ "type": "string",
1220
+ "minLength": 1
1078
1221
  }
1079
1222
  },
1080
1223
  "additionalProperties": false
@@ -1159,6 +1302,11 @@
1159
1302
  "type": "number",
1160
1303
  "minimum": 0.25,
1161
1304
  "maximum": 16
1305
+ },
1306
+ "say": {
1307
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1308
+ "type": "string",
1309
+ "minLength": 1
1162
1310
  }
1163
1311
  },
1164
1312
  "additionalProperties": false
@@ -1219,6 +1367,11 @@
1219
1367
  "type": "number",
1220
1368
  "minimum": 0.25,
1221
1369
  "maximum": 16
1370
+ },
1371
+ "say": {
1372
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1373
+ "type": "string",
1374
+ "minLength": 1
1222
1375
  }
1223
1376
  },
1224
1377
  "additionalProperties": false
@@ -1279,6 +1432,11 @@
1279
1432
  "type": "number",
1280
1433
  "minimum": 0.25,
1281
1434
  "maximum": 16
1435
+ },
1436
+ "say": {
1437
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1438
+ "type": "string",
1439
+ "minLength": 1
1282
1440
  }
1283
1441
  },
1284
1442
  "additionalProperties": false
@@ -1342,6 +1500,11 @@
1342
1500
  "type": "number",
1343
1501
  "minimum": 0.25,
1344
1502
  "maximum": 16
1503
+ },
1504
+ "say": {
1505
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1506
+ "type": "string",
1507
+ "minLength": 1
1345
1508
  }
1346
1509
  },
1347
1510
  "additionalProperties": false
@@ -1408,6 +1571,11 @@
1408
1571
  "type": "number",
1409
1572
  "minimum": 0.25,
1410
1573
  "maximum": 16
1574
+ },
1575
+ "say": {
1576
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1577
+ "type": "string",
1578
+ "minLength": 1
1411
1579
  }
1412
1580
  },
1413
1581
  "additionalProperties": false
@@ -1472,6 +1640,11 @@
1472
1640
  "type": "number",
1473
1641
  "minimum": 0.25,
1474
1642
  "maximum": 16
1643
+ },
1644
+ "say": {
1645
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1646
+ "type": "string",
1647
+ "minLength": 1
1475
1648
  }
1476
1649
  },
1477
1650
  "additionalProperties": false
@@ -1531,6 +1704,11 @@
1531
1704
  "type": "number",
1532
1705
  "minimum": 0.25,
1533
1706
  "maximum": 16
1707
+ },
1708
+ "say": {
1709
+ "description": "Narration spoken from the start of this step; the video waits for it before the next narrated step",
1710
+ "type": "string",
1711
+ "minLength": 1
1534
1712
  }
1535
1713
  },
1536
1714
  "additionalProperties": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mineo71/showtime",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Product demos, directed by AI: Claude writes a JSON script, showTime records your web app and renders a polished MP4",
5
5
  "keywords": [
6
6
  "demo",
package/skill/SKILL.md CHANGED
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: showtime
3
- description: Use when the user wants a product demo video, walkthrough, screencast or feature recording of a web app (for a pitch, talk, README or social post). Write a showTime JSON script, rehearse it, record it and render a polished MP4 with auto-zoom, smooth cursor, captions and sped-up loading via the `showtime` CLI.
3
+ description: Use when the user wants a product demo video, walkthrough, screencast or feature recording of a web app (for a pitch, talk, README or social post). Write a showTime JSON script, rehearse it, record it and render a polished MP4 (or a 9:16 cut, GIF or WebP) with auto-zoom, smooth cursor, captions, narration and sped-up loading via the `showtime` CLI.
4
4
  ---
5
5
 
6
6
  # showTime — scripted demo videos
7
7
 
8
8
  `showtime` turns a JSON script into a polished 1080p MP4. You are the director: you write the
9
9
  script; the CLI clicks through the app, records it and edits it (zoom, cursor, ripples,
10
- spotlights, captions, speed-ups, intro card).
10
+ spotlights, captions, speed-ups, intro card, narration, music, subtitles). One take renders as
11
+ 16:9, 9:16 or 1:1, as MP4, GIF or WebP.
11
12
 
12
13
  ## Setup
13
14
 
@@ -36,12 +37,14 @@ showtime setup # downloads the browsers it records with
36
37
  5. **Lint:** `showtime lint demo/pitch.json` and fix every warning (long captions, pauses that
37
38
  should be waits, zoom or spotlight on steps without a target).
38
39
  6. **Record:** `showtime record demo/pitch.json` (runs in real time).
39
- 7. **Render:** `showtime render demo/pitch.json` → `demo/pitch.mp4`.
40
+ 7. **Render:** `showtime render demo/pitch.json` → `demo/pitch.mp4`. For social posts add
41
+ `--aspect 9:16` (→ `demo/pitch-9x16.mp4`); for a README or docs `--format gif` or `--format webp`
42
+ (`--width` sets the size). Each is a render of the same take, never a new recording.
40
43
  8. **QA:** `showtime frames demo/pitch.json`, then Read the contact sheet it prints first
41
44
  (`sheet.png`: every step in one image); open single stills only to look closer. Check that zoom
42
45
  lands on the right control, captions are legible, and pacing feels right. Run `showtime lint`
43
46
  again: with a take it also flags captions on screen too briefly to read.
44
- 9. **Iterate:** edits to `show`, `style` or `meta` only need `render`. Edits to actions (`do`,
47
+ 9. **Iterate:** edits to `show` (including `say`), `style`, `meta` or `audio` only need `render`. Edits to actions (`do`,
45
48
  targets, text, `hold`, `app`) need `record` again — `render` refuses a stale take.
46
49
 
47
50
  `showtime make <script>` runs check → record → render in one go. Every command takes `--json`
@@ -55,6 +58,29 @@ with `run_in_background: true` and wait for the completion notification. In Code
55
58
  agents, give the command a timeout of at least 10 minutes.
56
59
  A `record` that fails or is stopped halfway leaves the previous take in place.
57
60
 
61
+ ## Narration and sound
62
+
63
+ Give a step `"say": "…"` and a voice reads it from the start of that step. The video holds the
64
+ frame until the line ends before the next narrated step begins, so a line may cover several quick
65
+ steps. Write one short sentence per moment about the benefit, not the clicks. `showtime lint`
66
+ (with a take) flags lines that hold the frame for seconds (`narration-stretch`): shorten them.
67
+
68
+ ```
69
+ "audio": { "voice": { "engine": "system" }, "sounds": true, "subtitles": "burn" }
70
+ ```
71
+
72
+ - `voice.engine`: `system` (macOS `say`, Linux `espeak-ng`; free and offline) is the default.
73
+ Use `openai` or `elevenlabs` only when the user asks for a better voice and the key is in the
74
+ environment (`OPENAI_API_KEY` / `ELEVENLABS_API_KEY`); never write keys into the script. Those
75
+ engines send the narration text to the provider. `command` runs any TTS that reads stdin and
76
+ writes `{out}` (inserted already quoted: write it bare, e.g. `piper -m en_US.onnx -f {out}`).
77
+ - `music: { file, volume? }` adds a looped bed that ducks under the voice; only use files the user
78
+ provides.
79
+ - `subtitles`: `file` (default) writes `<video>.srt`; `burn` also draws them (good for 9:16, which
80
+ is often watched muted); `off`.
81
+ - `--no-audio` renders without the soundtrack. GIF, WebP and `--no-audio` leave narration out
82
+ (no voice needed, no pauses for it) unless subtitles are burned in.
83
+
58
84
  ## Side effects
59
85
 
60
86
  `check` performs every action for real (it is a full run, just fast), and `record` repeats them.
@@ -105,7 +131,10 @@ ask the user for one rather than guessing.
105
131
  `press {key}` · `hover {target}` · `select {target, value}` · `scroll {target} | {by}` ·
106
132
  `wait {for} | {ms}` · `pause {ms}`
107
133
  - **`show` (per step):** `zoom` (number, or `false`), `caption`, `spotlight`, `hold` (ms, real time),
108
- `speed`.
134
+ `speed`, `say` (narration).
135
+ - **`audio`:** `voice` (`{ engine: system | openai | elevenlabs | command, name?, speed?, model?,
136
+ instructions?, command? }`), `music` (`{ file, volume? }`), `sounds` (clicks and keystrokes),
137
+ `subtitles` (`file` | `burn` | `off`).
109
138
  - **`style`:** `background` (any CSS background), `chrome` (`browser` window frame | `none`), `address`
110
139
  (address-bar text), `padding`, `radius`, `shadow`, `font`, `accent`, `autoZoom`, `autoSpeed`, `maxZoom`.
111
140
  - **`app`:** `url`, `viewport`, `deviceScaleFactor`, `storageState` (Playwright storage-state JSON,
@@ -116,8 +145,10 @@ ask the user for one rather than guessing.
116
145
  - **`meta`:** `title`, `subtitle` (intro card), `logo` (SVG/PNG/JPEG/WebP), `outro`
117
146
  (`{ title, subtitle?, url? }`, closing card).
118
147
  - **`hooks`:** `before` / `after` lists of `{ run }` or `{ request }`.
119
- - **Brand file:** `showtime.config.json` in the script's folder or above holds default `style` and
120
- `meta` (logo, outro) for every script; the script's own fields win.
148
+ - **Brand file:** `showtime.config.json` in the script's folder or above holds default `style`,
149
+ `meta` (logo, outro) and `audio` for every script; the script's own fields win.
150
+ - **Render options** (`render`, `make`): `-o <file>`, `--aspect 16:9|9:16|1:1`,
151
+ `--format mp4|gif|webp`, `--width <px>`, `--no-audio`. `frames --aspect` reviews a crop.
121
152
  - Output lands next to the script; working files in `.showtime/<script-name>/`.
122
153
 
123
154
  ## Direction tips
@@ -134,3 +165,5 @@ ask the user for one rather than guessing.
134
165
  - End pitch demos with `meta.outro`: the product name and the URL to visit. Put brand colours, logo
135
166
  and outro in `showtime.config.json` when a project has several demos.
136
167
  - Keep the default 1440×900 viewport at `deviceScaleFactor` 2 for crisp 16:9 zooms.
168
+ - For a 9:16 cut, review it with `showtime frames <script> --aspect 9:16`: the camera follows each
169
+ target, so check that every step's target is the thing worth showing.
@@ -0,0 +1,19 @@
1
+ import { run, runBuffer } from '../ffmpeg';
2
+ import { RATE } from './wav';
3
+
4
+ const LOUDNORM = (lufs: number) => `loudnorm=I=${lufs}:TP=-1.5:LRA=11`;
5
+
6
+ /** A synthesized line as a loudness-normalised mono 48 kHz 16-bit WAV. */
7
+ export async function normalizeVoice(src: string, dest: string): Promise<void> {
8
+ await run('ffmpeg', ['-v', 'error', '-y', '-i', src, '-af', LOUDNORM(-16), '-ac', '1', '-ar', String(RATE), '-c:a', 'pcm_s16le', '-f', 'wav', dest]);
9
+ }
10
+
11
+ /** Any audio file ffmpeg reads, as float channels at 48 kHz (optionally loudness-normalised). */
12
+ export async function decodeAudio(file: string, o: { channels: 1 | 2; lufs?: number }): Promise<Float32Array[]> {
13
+ const af = o.lufs === undefined ? [] : ['-af', LOUDNORM(o.lufs)];
14
+ const raw = await runBuffer('ffmpeg', ['-v', 'error', '-i', file, ...af, '-ac', String(o.channels), '-ar', String(RATE), '-f', 'f32le', '-']);
15
+ const n = Math.floor(raw.length / 4 / o.channels);
16
+ const channels = Array.from({ length: o.channels }, () => new Float32Array(n));
17
+ for (let i = 0; i < n; i++) for (let c = 0; c < o.channels; c++) channels[c][i] = raw.readFloatLE((i * o.channels + c) * 4);
18
+ return channels;
19
+ }