reelson 0.7.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +15 -0
  4. package/README.md +180 -0
  5. package/bin/reelson.js +649 -0
  6. package/bin/reelson.ts +676 -0
  7. package/bin/run.js +5 -0
  8. package/demo.config.example.json +51 -0
  9. package/docs/demo.webp +0 -0
  10. package/docs/prompting.md +72 -0
  11. package/docs/style-guide.md +117 -0
  12. package/examples/demo.config.json +18 -0
  13. package/examples/todo-add-item/scenario.ts +45 -0
  14. package/examples/todo-add-item/video.json +35 -0
  15. package/music/README.md +7 -0
  16. package/package.json +68 -0
  17. package/skills/reelson-compose/SKILL.md +226 -0
  18. package/skills/reelson-compose/schemas/video.schema.json +172 -0
  19. package/skills/reelson-compose/scripts/build.js +424 -0
  20. package/skills/reelson-compose/scripts/build.ts +532 -0
  21. package/skills/reelson-compose/scripts/captions.js +24 -0
  22. package/skills/reelson-compose/scripts/captions.ts +40 -0
  23. package/skills/reelson-compose/scripts/check.js +80 -0
  24. package/skills/reelson-compose/scripts/check.ts +93 -0
  25. package/skills/reelson-compose/scripts/composition.js +163 -0
  26. package/skills/reelson-compose/scripts/composition.ts +241 -0
  27. package/skills/reelson-compose/scripts/hyperframes.js +138 -0
  28. package/skills/reelson-compose/scripts/hyperframes.ts +157 -0
  29. package/skills/reelson-compose/scripts/portrait.js +150 -0
  30. package/skills/reelson-compose/scripts/portrait.ts +218 -0
  31. package/skills/reelson-compose/scripts/project.js +238 -0
  32. package/skills/reelson-compose/scripts/project.ts +324 -0
  33. package/skills/reelson-compose/scripts/studio.js +330 -0
  34. package/skills/reelson-compose/scripts/studio.ts +394 -0
  35. package/skills/reelson-compose/scripts/timeline.js +271 -0
  36. package/skills/reelson-compose/scripts/timeline.ts +514 -0
  37. package/skills/reelson-compose/scripts/tts.js +303 -0
  38. package/skills/reelson-compose/scripts/tts.ts +371 -0
  39. package/skills/reelson-compose/scripts/verify.js +190 -0
  40. package/skills/reelson-compose/scripts/verify.ts +209 -0
  41. package/skills/reelson-compose/scripts/voice.js +178 -0
  42. package/skills/reelson-compose/scripts/voice.ts +224 -0
  43. package/skills/reelson-compose/scripts/zooms.js +259 -0
  44. package/skills/reelson-compose/scripts/zooms.ts +333 -0
  45. package/skills/reelson-compose/sections/README.md +5 -0
  46. package/skills/reelson-compose/sections/intro/minimal/section.css +27 -0
  47. package/skills/reelson-compose/sections/intro/minimal/section.html +12 -0
  48. package/skills/reelson-compose/sections/intro/minimal/section.js +16 -0
  49. package/skills/reelson-compose/sections/intro/minimal/section.json +9 -0
  50. package/skills/reelson-compose/sections/intro/poster/section.css +27 -0
  51. package/skills/reelson-compose/sections/intro/poster/section.html +14 -0
  52. package/skills/reelson-compose/sections/intro/poster/section.js +28 -0
  53. package/skills/reelson-compose/sections/intro/poster/section.json +9 -0
  54. package/skills/reelson-compose/sections/intro/split/section.css +30 -0
  55. package/skills/reelson-compose/sections/intro/split/section.html +12 -0
  56. package/skills/reelson-compose/sections/intro/split/section.js +19 -0
  57. package/skills/reelson-compose/sections/intro/split/section.json +9 -0
  58. package/skills/reelson-compose/sections/outro/compact/section.css +23 -0
  59. package/skills/reelson-compose/sections/outro/compact/section.html +10 -0
  60. package/skills/reelson-compose/sections/outro/compact/section.js +11 -0
  61. package/skills/reelson-compose/sections/outro/compact/section.json +8 -0
  62. package/skills/reelson-compose/sections/outro/endcard/section.css +25 -0
  63. package/skills/reelson-compose/sections/outro/endcard/section.html +13 -0
  64. package/skills/reelson-compose/sections/outro/endcard/section.js +13 -0
  65. package/skills/reelson-compose/sections/outro/endcard/section.json +8 -0
  66. package/skills/reelson-compose/sections/outro/wordmark/section.css +11 -0
  67. package/skills/reelson-compose/sections/outro/wordmark/section.html +5 -0
  68. package/skills/reelson-compose/sections/outro/wordmark/section.js +12 -0
  69. package/skills/reelson-compose/sections/outro/wordmark/section.json +8 -0
  70. package/skills/reelson-compose/sections/recap/compact/section.css +26 -0
  71. package/skills/reelson-compose/sections/recap/compact/section.html +8 -0
  72. package/skills/reelson-compose/sections/recap/compact/section.js +23 -0
  73. package/skills/reelson-compose/sections/recap/compact/section.json +11 -0
  74. package/skills/reelson-compose/sections/recap/steps/section.css +36 -0
  75. package/skills/reelson-compose/sections/recap/steps/section.html +8 -0
  76. package/skills/reelson-compose/sections/recap/steps/section.js +27 -0
  77. package/skills/reelson-compose/sections/recap/steps/section.json +11 -0
  78. package/skills/reelson-compose/studio/index.html +81 -0
  79. package/skills/reelson-compose/studio/studio.css +142 -0
  80. package/skills/reelson-compose/studio/studio.js +1031 -0
  81. package/skills/reelson-compose/templates/README.md +193 -0
  82. package/skills/reelson-compose/templates/classic/assets/vendor/Inter-OFL.txt +93 -0
  83. package/skills/reelson-compose/templates/classic/assets/vendor/gsap.min.js +11 -0
  84. package/skills/reelson-compose/templates/classic/assets/vendor/inter-latin-ext-wght-normal.woff2 +0 -0
  85. package/skills/reelson-compose/templates/classic/assets/vendor/inter-latin-wght-normal.woff2 +0 -0
  86. package/skills/reelson-compose/templates/classic/stage.html +447 -0
  87. package/skills/reelson-compose/templates/classic/template.json +5 -0
  88. package/skills/reelson-record/SKILL.md +220 -0
  89. package/skills/reelson-record/schemas/demo.config.schema.json +269 -0
  90. package/skills/reelson-record/scripts/capture.js +142 -0
  91. package/skills/reelson-record/scripts/capture.ts +190 -0
  92. package/skills/reelson-record/scripts/config.js +148 -0
  93. package/skills/reelson-record/scripts/config.ts +264 -0
  94. package/skills/reelson-record/scripts/cursor-overlay.js +111 -0
  95. package/skills/reelson-record/scripts/cursor-overlay.ts +122 -0
  96. package/skills/reelson-record/scripts/doctor.js +128 -0
  97. package/skills/reelson-record/scripts/doctor.ts +147 -0
  98. package/skills/reelson-record/scripts/languages.js +29 -0
  99. package/skills/reelson-record/scripts/languages.ts +41 -0
  100. package/skills/reelson-record/scripts/record.js +365 -0
  101. package/skills/reelson-record/scripts/record.ts +451 -0
  102. package/skills/reelson-record/scripts/scenario.js +356 -0
  103. package/skills/reelson-record/scripts/scenario.ts +570 -0
  104. package/skills/reelson-record/scripts/validate.js +143 -0
  105. package/skills/reelson-record/scripts/validate.ts +179 -0
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "./skills/reelson-record/schemas/demo.config.schema.json",
3
+ "$comment": "Copy to <project>/demo.config.json (or run `reelson init`). Every field is optional; defaults in skills/reelson-record/scripts/config.ts.",
4
+ "videosDir": "docs/videos",
5
+ "language": "en",
6
+ "locale": "en-US",
7
+ "brand": {
8
+ "name": "YOUR BRAND",
9
+ "tagline": "PRODUCT",
10
+ "eyebrow": "Your Brand",
11
+ "color": "#dc2626",
12
+ "colorSoft": "#f87171",
13
+ "logo": null
14
+ },
15
+ "template": "classic",
16
+ "sections": {
17
+ "intro": "poster",
18
+ "recap": "steps",
19
+ "outro": "wordmark"
20
+ },
21
+ "strings": {
22
+ "recapTitle": "In short",
23
+ "stepsLabel": {
24
+ "one": "step",
25
+ "other": "steps"
26
+ },
27
+ "secondsLabel": {
28
+ "one": "second",
29
+ "other": "seconds"
30
+ }
31
+ },
32
+ "music": {
33
+ "file": null,
34
+ "lufs": -28,
35
+ "lufsUnderNarration": -34
36
+ },
37
+ "record": {
38
+ "viewport": {
39
+ "width": 1440,
40
+ "height": 900
41
+ },
42
+ "deviceScaleFactor": 2,
43
+ "extraHTTPHeaders": {
44
+ "X-Demo-Recording": "1"
45
+ },
46
+ "hideSelectors": [],
47
+ "personaDomain": "example.com",
48
+ "cursor": "layer",
49
+ "capture": "screencast"
50
+ }
51
+ }
package/docs/demo.webp ADDED
Binary file
@@ -0,0 +1,72 @@
1
+ # How to ask for a video
2
+
3
+ The prompt is the spec. A complete prompt has six parts; the more you give, the fewer guesses
4
+ Claude makes and the fewer re-renders you need.
5
+
6
+ ```
7
+ Make a demo video of <FEATURE>.
8
+
9
+ Where: <URL / area + account> e.g. the admin area on https://app.test as the admin user
10
+ Steps: 1. ... 2. ... 3. ... what the viewer should see happen, in order
11
+ Callouts: <one short imperative per step> in the UI's language, matching its labels
12
+ Cards: title "...", subtitle "..." recap title, intro/recap/outro sections optional
13
+ Emphasis: zoom on <what> during step <n> only where the UI is small
14
+ Output: mp4 (default) / also gif slug: <kebab-case>
15
+ ```
16
+
17
+ Claude writes the scenario, records, verifies frames, builds the composition, words the
18
+ callouts in video.json, checks, renders, and reports the file path with a few extracted frames.
19
+
20
+ ## Example prompts
21
+
22
+ **A first video for a feature**
23
+
24
+ > Make a demo video of searching a customer. Admin area on https://app.test as the admin.
25
+ > Steps: open Customers, type the first customer's first name in the table search, hover the
26
+ > matching row. Callouts: "Open Customers", "Type the name in Search". Title "Find a customer",
27
+ > subtitle "Search any customer from the list". Zoom on the search box while typing.
28
+ > Slug customers-search.
29
+
30
+ **A create flow with a modal**
31
+
32
+ > Demo video: creating a customer. Customers → New customer, fill name/email/phone with a
33
+ > persona, save, show the success notification and the new row. One callout per step, zoom on
34
+ > the form while filling. Title "Add a customer". Trim the login away.
35
+
36
+ **A two-actor flow**
37
+
38
+ > Demo of a document approval: a manager sends a document from the admin area, then the
39
+ > employee accepts it in their personal area. One scenario; switch accounts inside
40
+ > `demo.transition()` with a "Manager → Employee" card. ~40 s max.
41
+
42
+ **Through an external payment page**
43
+
44
+ > Demo of the shop checkout paid by card, no login: product → add to cart → checkout → card
45
+ > payment on the gateway's test page with its test card → cut the 3-D Secure wait → success
46
+ > page. Accept the cookie banner first. Zoom on the payment-method choice and the card form.
47
+
48
+ **Re-record after a UI change**
49
+
50
+ > The customers table got new columns. Re-record <videosDir>/customers-search with the
51
+ > existing scenario, re-render with the current video.json, and show me
52
+ > frames at each callout.
53
+
54
+ **Tweak an existing video (no re-record)**
55
+
56
+ > In <videosDir>/customers-search, change callout 2 to "Search by name or email", make the zoom
57
+ > tighter (scale 2.0) and 1 s shorter, then re-render.
58
+
59
+ **Different output**
60
+
61
+ > Render <videosDir>/customers-search also as a 15 fps GIF for the README.
62
+
63
+ **Another intro or outro**
64
+
65
+ > Render <videosDir>/customers-search with the `minimal` intro, no recap and the `endcard`
66
+ > outro, next to the current one so I can compare.
67
+
68
+ **Narrated recording (manual, OpenScreen)**
69
+
70
+ > I recorded <videosDir>/onboarding-tour/recording.mp4 myself with OpenScreen, with voiceover.
71
+ > Build the composition around it: title "Platform tour", callouts at 0:04 "Main menu",
72
+ > 0:12 "Company settings", 0:21 "Invite colleagues". Keep the narration.
@@ -0,0 +1,117 @@
1
+ # Style guide
2
+
3
+ The house style for every video made with the kit. The scripts, the `classic` template and
4
+ every kit section already implement it; this page exists so a fresh session knows *why* and doesn't undo it.
5
+ A project can tighten these rules in its own docs (e.g. its payment test cards or seeded
6
+ accounts), but should not loosen them without a reason.
7
+
8
+ ## Standing instructions
9
+
10
+ 1. **Structure**: intro → framed recording with numbered callouts and zooms → recap (optional)
11
+ → outro. Intro, recap and outro are sections picked per project or per video; the classic
12
+ set is the poster cover, a recap with the title left and ONE column of numbered steps right,
13
+ and a wordmark card (letters growing in from farther out, line wiping from the left). The
14
+ video **ends on the brand**, never on an empty frame: the last frame is what players show
15
+ when playback stops. `wordmark` and `compact` settle at 15 % opacity; `endcard` stays fully
16
+ readable. No URLs on any card.
17
+ 2. **Intro = poster**: **frame 0 shows the brand and the title legibly** — chat apps, GitHub
18
+ and Finder use it as the thumbnail — never hidden or blurred. The classic `poster`: wordmark,
19
+ accent line, tagline, then title, subtitle and the "N steps · S seconds" chip, centred; at
20
+ t=0 everything is big (130 %) and 60 % transparent. It "breathes" down into place (sine.inOut,
21
+ letters one after another from the centre), holds ~1.8 s, and at 3.0 s lifts out through
22
+ the top while the recording rides in from below on the same upward motion. Other intros
23
+ keep the poster rule and hand over with `stage.enter()`.
24
+ 3. **Background**: deep navy gradient with four soft glows (brand colour, blue, violet, teal)
25
+ drifting and breathing on 4–6.5 s loops — slow but perceptible — plus a faint dot grid.
26
+ 4. **Cursor**: large black macOS-style arrow with a white outline, press squash, and a
27
+ brand-coloured double ring on click. It never jumps on click, and it keeps its size
28
+ during zooms (the video draws it as a layer from the recorder's log).
29
+ 5. **Motion**: gently curved, eased mouse paths with a little tremor, landing slightly
30
+ off-centre; typing with uneven per-key delays. Human, never slow. The recording opens with
31
+ the cursor resting in the middle third of the screen, never jumping in from a corner.
32
+ 6. **People**: realistic names and emails in the UI's language from `demo.persona()`. Never
33
+ `test@`, `e2e-123` or seeded-looking data typed on camera.
34
+ 7. **Payments / external services**: use the provider's test mode and test data, the page in
35
+ the UI's language, and remove waits (3-D Secure, redirects) with `demo.cut()`.
36
+ 8. **Audio**: one music bed per project (`music.file`) under every video at −28 LUFS with
37
+ fades; −34 LUFS under narration. Don't pick a different track per video.
38
+ 9. **Quality**: capture at 2x (2880x1800 for 1440x900, CRF 15) so text stays sharp in the
39
+ 1080p frame and under zooms; render with `--video-frame-format jpg -q delivery`; keep
40
+ `media.autoProxy: false` and no `will-change` on `#frame`.
41
+ 10. **Language**: the product UI's language. Callouts are imperative steps (≤ 6 words)
42
+ because they also become the recap. **At most 10 steps** per video.
43
+ 11. **Nothing pre-filled on camera**: the recorder sends `X-Demo-Recording: 1`; the app should
44
+ skip local-only prefills when it sees it. Checkboxes and consents are ticked on camera.
45
+ 12. **Verify with frames**, not logs: frame 0 (poster), settled intro, the hand-over to the
46
+ recording, a callout, a zoom, the recap, the last frame of a real render.
47
+ 13. **Zooms ride along with the cursor and finish before the click**: zoom in while the cursor
48
+ glides to the first click it frames (starting ≤ 0.35 s before the glide), fully in 0.1 s
49
+ before that click; zoom out while it glides to the next target, done before that click.
50
+ A zoom holds its own clicks (plus a click in view right after them, < 1 s), not everything
51
+ still on screen; when the next glide is > 3 s away it lingers 1.2 s and leaves. No easing during a click. No extra pauses in the scenario to make room
52
+ for zooms — shorten the ease (≥ 0.4 s) instead. A zoom anchored with `clicks` in video.json
53
+ is timed this way automatically; `reelson check` enforces it for every zoom.
54
+
55
+ ## What makes a video good
56
+
57
+ **Plan the steps before the pixels.** One feature, 3–6 visible state changes, each a sentence a
58
+ reader could follow without the video. If you can't write the callouts, the video isn't ready.
59
+
60
+ **Recording**
61
+
62
+ - `demo.marker()` right after the UI reaches the state, not before the click. The marker ends
63
+ its step: the callout starts as the step begins (the first glide or click after the previous
64
+ marker), stays through it and a moment on its result, so the viewer reads "Type the name"
65
+ while the name is typed.
66
+ - Pause 1–1.5 s after anything the viewer must read (a modal, a notification, results). The
67
+ default settle after a click (0.7 s) is enough for menus only.
68
+ - `demo.click` / `demo.type` / `demo.moveTo`, never raw `page.click` / `fill()`.
69
+ - Small `demo.scroll()` steps over big jumps; or navigate straight to the section.
70
+ - Read values from the page instead of hard-coding seeded data.
71
+ - Wrap slow waits in `demo.cut()`; a short glimpse is kept so the cut doesn't feel like a glitch.
72
+ - Trim the login off (video.json `"trim": { "start": "auto" }`, the default) unless the video
73
+ is about logging in. Tie trims to markers or clicks, never to plain seconds: a re-record
74
+ moves the footage.
75
+ - Keep the 1440x900 viewport; wider makes text tiny inside the 1920x1080 frame.
76
+
77
+ **Composition**
78
+
79
+ - Callouts: imperative, in the UI's language, ≤ 6 words ("Click Save", not "Now the user
80
+ clicks the save button"). One on screen at a time, ≥ 2 s each.
81
+ - Zoom only where the UI is small (a field, a toggle, a badge), scale 1.5–2.0, timed by the
82
+ cursor (rule 13).
83
+ - Title = the task ("Find a customer"), subtitle = the benefit or context. Keep the intro at
84
+ its section's default (3 s for `poster`); longer intros get skipped.
85
+ - Recap rows must read as steps on their own ("Choose Card"), not commentary ("Here is the form").
86
+ - Length: 15–30 s is the sweet spot. Over 45 s, split into two videos.
87
+
88
+ **Verify the artifact, not the log**
89
+
90
+ 1. `reelson check <slug>` says "ready to render" (schemas, zoom timing, `hyperframes check`).
91
+ 2. Extract and look at frames: t=0, settled intro, each callout, the zoom, the recap, the end.
92
+ 3. `ffprobe` duration matches the timeline the build printed.
93
+ 4. Watch it once at 1x. If a step is unreadable, pause longer in the scenario, not in the
94
+ composition.
95
+
96
+ ## Iterating
97
+
98
+ | Want to change | Do |
99
+ |---------------------------|-----------------------------------------------------------------------------|
100
+ | Callout text, zoom, trims | Edit `video.json`, `reelson render <slug>` (it rebuilds). |
101
+ | Pacing, missing step | Edit `scenario.ts`, `reelson record`, add the new marker to video.json, render. |
102
+ | UI changed | `reelson record` then `reelson render`: callouts follow their markers, zooms their click numbers (re-check if the clicks changed). |
103
+ | Look of intro/recap/outro | Pick another section (`reelson templates`), or make one (`templates/README.md`) — never per-video edits. |
104
+
105
+ ## Troubleshooting
106
+
107
+ - **Scenario timed out on a locator**: open `recording.failed.mp4`, re-run with `--headed`.
108
+ - **`video_nested_in_timed_element` lint error**: a timed `<video>` inside a timed wrapper.
109
+ - **A callout flashes at t=0 or a zoom starts zoomed in**: an "out" tween without
110
+ `immediateRender: false`.
111
+ - **Blurry UI text (worst in zooms)**: capture must be 2x (`ffprobe recording.mp4` →
112
+ 2880x1800), `hyperframes.json` `autoProxy: false`, no `will-change` on `#frame`.
113
+ - **Callout clipped during a zoom**: it ended up inside `#frame`; callouts live in `#callouts`.
114
+ - **`Cannot find module '@playwright/test'`** or **`reelson: command not found`**: run
115
+ `npm install -g reelson && reelson install` (or the kit checkout's `install.sh`).
116
+ - **`… is invalid: … unknown key — did you mean …`**: a typo in demo.config.json or video.json;
117
+ the message names the path. Editors autocomplete both through their `$schema`.
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "../skills/reelson-record/schemas/demo.config.schema.json",
3
+ "$comment": "Config for the bundled examples. A real project keeps its own demo.config.json at its root (copy demo.config.example.json).",
4
+ "videosDir": ".",
5
+ "language": "en",
6
+ "locale": "en-US",
7
+ "brand": {
8
+ "name": "REELSON",
9
+ "tagline": "EXAMPLE",
10
+ "eyebrow": "Reelson",
11
+ "color": "#6366f1",
12
+ "colorSoft": "#a5b4fc"
13
+ },
14
+ "template": "classic",
15
+ "music": {
16
+ "file": null
17
+ }
18
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Example demo: adding and completing items in the public TodoMVC app.
3
+ * Runs anywhere with internet access, so it doubles as the kit's smoke test.
4
+ *
5
+ * node skills/reelson-record/scripts/record.ts examples/todo-add-item/scenario.ts
6
+ * node skills/reelson-compose/scripts/scaffold.ts examples/todo-add-item \
7
+ * --title "Plan your day" --subtitle "Add tasks and tick them off"
8
+ *
9
+ * Every `demo.marker()` becomes a timed callout placeholder in the composition.
10
+ */
11
+ import type { Scenario } from '../../skills/reelson-record/scripts/scenario.ts'
12
+
13
+ export default {
14
+ name: 'todo-add-item',
15
+ baseURL: 'https://demo.playwright.dev',
16
+ async run(demo) {
17
+ await demo.goto('/todomvc/#/')
18
+ const input = demo.page.getByPlaceholder('What needs to be done?')
19
+
20
+ await demo.type(input, 'Book the team offsite')
21
+ await demo.page.keyboard.press('Enter')
22
+ await demo.pause(600)
23
+ demo.marker('Type a task and press Enter')
24
+
25
+ await demo.type(input, 'Send the weekly report')
26
+ await demo.page.keyboard.press('Enter')
27
+ await demo.pause(900)
28
+ demo.marker('Add as many as you need')
29
+
30
+ await demo.click(
31
+ demo.page
32
+ .getByTestId('todo-item')
33
+ .filter({ hasText: 'Send the weekly report' })
34
+ .getByRole('checkbox'),
35
+ )
36
+ await demo.pause(900)
37
+ demo.marker('Tick a task when it is done')
38
+
39
+ await demo.click(demo.page.getByRole('link', { name: 'Active' }))
40
+ await demo.pause(1200)
41
+ demo.marker('Filter what is left')
42
+ // Leave the last callout time to be read before the recording ends.
43
+ await demo.pause(2000)
44
+ },
45
+ } satisfies Scenario
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "../../skills/reelson-compose/schemas/video.schema.json",
3
+ "title": "Plan your day",
4
+ "subtitle": "Add tasks and tick them off",
5
+ "trim": {
6
+ "start": "auto"
7
+ },
8
+ "callouts": [
9
+ {
10
+ "marker": "Type a task and press Enter",
11
+ "text": "Type a task and press Enter"
12
+ },
13
+ {
14
+ "marker": "Add as many as you need",
15
+ "text": "Add as many as you need"
16
+ },
17
+ {
18
+ "marker": "Tick a task when it is done",
19
+ "text": "Tick a task when it is done"
20
+ },
21
+ {
22
+ "marker": "Filter what is left",
23
+ "text": "Filter what is left"
24
+ }
25
+ ],
26
+ "zooms": [
27
+ {
28
+ "clicks": [
29
+ 3,
30
+ 4
31
+ ],
32
+ "scale": 1.7
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,7 @@
1
+ # Music
2
+
3
+ Licensed tracks are not committed (stock-music licences are usually registered per project).
4
+ Keep each project's bed inside that project (e.g. `docs/videos/_music/<track>.mp3`, with its
5
+ licence certificate) and point `music.file` in its `demo.config.json` at it.
6
+
7
+ Files you drop here for local experiments are git-ignored.
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "reelson",
3
+ "version": "0.7.0",
4
+ "type": "module",
5
+ "description": "Claude Code skills + CLI that turn a scripted Playwright walkthrough into a branded HyperFrames demo video.",
6
+ "keywords": [
7
+ "demo-video",
8
+ "screencast",
9
+ "playwright",
10
+ "hyperframes",
11
+ "claude-code",
12
+ "skills",
13
+ "product-demo",
14
+ "video"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Ion Caliman <icaliman92@gmail.com>",
18
+ "homepage": "https://github.com/reelson/reelson#readme",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/reelson/reelson.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/reelson/reelson/issues"
25
+ },
26
+ "bin": {
27
+ "reelson": "bin/run.js"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "skills/",
32
+ "docs/",
33
+ "examples/demo.config.json",
34
+ "examples/todo-add-item/scenario.ts",
35
+ "examples/todo-add-item/video.json",
36
+ "demo.config.example.json",
37
+ "CHANGELOG.md",
38
+ "NOTICE.md",
39
+ "music/README.md"
40
+ ],
41
+ "engines": {
42
+ "node": ">=22.18"
43
+ },
44
+ "scripts": {
45
+ "setup": "npm install && npx playwright install chromium",
46
+ "test": "node --test --test-reporter=spec \"test/**/*.test.ts\"",
47
+ "test:update-golden": "UPDATE_GOLDEN=1 node --test \"test/**/*.test.ts\"",
48
+ "typecheck": "tsc --noEmit",
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "clean": "find bin skills -name '*.js' -path '*/scripts/*' -delete; rm -f bin/reelson.js",
51
+ "prepack": "npm run clean && npm run build",
52
+ "postpack": "npm run clean",
53
+ "prepublishOnly": "npm run ci",
54
+ "ci": "npm run typecheck && npm test",
55
+ "example:verify": "cd examples && node ../bin/reelson.ts verify todo-add-item",
56
+ "example:record": "cd examples && node ../bin/reelson.ts record todo-add-item --all-takes",
57
+ "example:build": "cd examples && node ../bin/reelson.ts build todo-add-item",
58
+ "example:check": "cd examples && node ../bin/reelson.ts check todo-add-item",
59
+ "example:render": "cd examples && node ../bin/reelson.ts render todo-add-item --all-formats"
60
+ },
61
+ "dependencies": {
62
+ "@playwright/test": "^1.55.0"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^22.18.0",
66
+ "typescript": "^5.9.0"
67
+ }
68
+ }
@@ -0,0 +1,226 @@
1
+ ---
2
+ name: reelson-compose
3
+ description: Use when the user wants the finished demo video — intro/outro title cards, numbered callouts, zoom-ins, branded framing — rendered to MP4/GIF from a screen recording, or mentions HyperFrames or reelson. Takes <videosDir>/<slug>/recording.mp4 (+ markers.json) from the reelson-record skill; video.json is the per-video source of truth, and `reelson build` turns it into a HyperFrames composition from a template plus mix-and-match intro/recap/outro sections (brand, logo, language and music from demo.config.json); `reelson render` produces video/renders/<slug>.mp4.
4
+ ---
5
+
6
+ # reelson-compose
7
+
8
+ Turns raw footage from `reelson-record` into a finished video with
9
+ [HyperFrames](https://hyperframes.heygen.com) (HTML + GSAP → deterministic MP4, rendered
10
+ frame-by-frame in headless Chrome), driven by the `reelson` CLI. Needs Node 22.18+ and `ffmpeg`;
11
+ HyperFrames is fetched by `npx` (pinned version).
12
+
13
+ **Read the kit's `docs/style-guide.md` first** (`realpath` this skill to find the kit). The
14
+ templates and sections implement it; design changes belong in a template or a section
15
+ (`templates/README.md`), never in a video's generated files.
16
+
17
+ ```
18
+ <videosDir>/<slug>/
19
+ ├── scenario.ts ← reelson-record (committed)
20
+ ├── markers.json ← reelson-record (committed)
21
+ ├── recording.mp4 ← reelson-record (ignored)
22
+ ├── video.json ← THE source of truth for this video (committed) — edit this
23
+ └── video/ ← generated by `reelson build` (ignored; delete any time)
24
+ ├── index.html, hyperframes.json, package.json
25
+ ├── assets/ recording, vendored GSAP + fonts, music.m4a, narration.m4a
26
+ └── renders/<slug>.mp4
27
+ ```
28
+
29
+ ## video.json
30
+
31
+ Every time in it is a **recording time** (seconds in recording.mp4, as in markers.json), so
32
+ changing the trim or the template never re-times anything. Validated against
33
+ `schemas/video.schema.json` (editors autocomplete it through `$schema`); a typo is an error.
34
+
35
+ ```jsonc
36
+ {
37
+ "title": "Find a customer", // cover title = the task
38
+ "subtitle": "Search any customer from the list",
39
+ "trim": { "start": "auto" }, // skip the login; optional "end"
40
+ "callouts": [ // numbered steps; also the recap
41
+ { "marker": "customers-list", "text": "Open Customers" },
42
+ { "marker": "search-results", "text": "Type the name in Search" }
43
+ ],
44
+ "zooms": [
45
+ { "clicks": [2, 3], "scale": 1.8 } // frame clicks #2–#3 of markers.json
46
+ ]
47
+ // optional: "template", "sections": { intro, recap, outro }, "recapTitle",
48
+ // "brand": { name, tagline, eyebrow, logo }, "music": file | false
49
+ }
50
+ ```
51
+
52
+ - **Trim**: `start` is `"auto"` (just before the first logged glide or marker, i.e. after the
53
+ login — what a new video.json gets), or tied to the footage: `{ "marker": "customers-list",
54
+ "offset": -0.8 }`, `{ "click": 3, "offset": -0.5 }` (from the glide towards click 3). `end`
55
+ takes the same anchors; leave it out to keep the recording to its end. Plain seconds work too
56
+ but go stale when the demo is re-recorded (`reelson check` warns).
57
+ - **Callouts** point at a `marker` label (or give an `at` for manual recordings). Word them as
58
+ imperative steps in the UI's language, ≤ 6 words. A marker callout starts **with its step** —
59
+ the first glide or click after the previous marker (the marker comes after the action) — and
60
+ `duration` is automatic (through the step and 1.2 s past its marker, at least 3 s, ending
61
+ before the next callout) unless given. `"anchor": "marker"` starts it on the marker instead
62
+ (for a callout about the result). `offset` (seconds, may be negative) nudges it from there and
63
+ keeps it tied to the marker, so it still follows a re-recording.
64
+ - **Voice-over**: `"voice": true` speaks each callout as it appears, with demo.config.json
65
+ `voice.provider`:
66
+ - `openai` (default): `gpt-4o-mini-tts`, voice `alloy`, pace and tone from `instructions`;
67
+ needs `OPENAI_API_KEY`. With `baseURL` it talks to a local OpenAI-compatible server instead
68
+ (Kokoro-FastAPI `http://localhost:8880/v1`, Speaches, LocalAI) — no key needed.
69
+ - `elevenlabs`: `eleven_multilingual_v2` (or `eleven_flash_v2_5`, faster; `eleven_v3`, most
70
+ expressive), voice George by default — `voice` takes a voice id, or a name when the key may
71
+ read voices; needs `ELEVENLABS_API_KEY`.
72
+ - `piper`: a local neural voice (`pipx install piper-tts`), one per language by default
73
+ (`ro_RO-mihai-medium`, `en_US-lessac-medium`, …), downloaded once into ~/.cache/reelson/piper.
74
+ - `command`: any local program — `"command": ["my-tts", "--out", "{out}", "{text}"]`
75
+ ({voice} {model} {speed} {language} too; the text also comes on stdin).
76
+
77
+ `speed` uses the provider's own rate (ElevenLabs 0.7–1.2, Piper, OpenAI tts-1; for
78
+ gpt-4o-mini-tts ask for the pace in `instructions`); `options` passes extra request fields
79
+ (e.g. ElevenLabs `{"voice_settings": {"stability": 0.4}}`) or Piper flags. Keys come from the
80
+ environment or a `.env` next to demo.config.json or in the reelson install (a checkout or the npm package). A callout's `say`
81
+ replaces what is spoken (a full sentence for a terse pill — "→" reads as a pause; `false`:
82
+ silent); `"voice": { "intro": "…" }` adds a line over the intro, and `provider` / `model` /
83
+ `voice` / `instructions` / `speed` there override the project for one video. `reelson voice
84
+ <slug>` makes the missing lines (render and build do it for you), trimmed of the silence around
85
+ them, into `<demo>/voice/<hash>.mp3` — cached by words + every sound setting, so each line is
86
+ made once; commit the folder to render without a key. `reelson doctor` says whether the
87
+ provider can speak here. A spoken callout stays up until its line is said, and the next step's
88
+ callout waits for it; when the line cannot fit (the video ends, or an `at` callout comes
89
+ first), the build and check warn — shorten its `say` or pause longer in the scenario. The
90
+ music ducks under it.
91
+ - **Zooms**: use `clicks: [first, last]` (1-based positions in markers.json `clicks`, which
92
+ `demo.click`/`demo.type` log). reelson computes the focus point and the timing from the
93
+ cursor — in with the glide to the first click, out with the glide to the next target,
94
+ never easing during a click or typing (style guide #13). `scale` 1.5–2.0; `x`/`y` (0..1)
95
+ override the focus; `in`/`out` override the eases. A manual zoom (`at`, `duration`, `x`, `y`)
96
+ is possible but rarely needed. Never add pauses to the scenario for a zoom. Two zooms must
97
+ not be on screen at once (`reelson check` flags it). A zoom holds its own clicks and ends at
98
+ the next pause (a click still in view < 1 s later joins it; > 3 s of nothing, it lingers and
99
+ leaves): to hold longer, widen `clicks`; for two overlapping zooms, use one over both ranges.
100
+ `"follow": true` pans with the cursor while zoomed (smoothed, aimed slightly ahead), so the
101
+ clicks need not fit in one view — good for a form filled top to bottom at a high `scale`.
102
+ - To find click numbers, read `clicks` in markers.json (each has `at`, `x`, `y`, `kind`).
103
+ - **Cursor**: recordings made with `record.cursor: "layer"` (the default) have their cursor
104
+ drawn by the video. `"cursor": { "size": 56, "ripple": false }` restyles it, `"idle": 3` fades
105
+ it out after 3 s without moving or clicking (back in as it moves), `"cursor": false` hides it. For a recording with the cursor filmed in, the build warns that `cursor` has no
106
+ effect.
107
+
108
+ ## Templates and sections
109
+
110
+ A video = a **template** (the stage: background, framed recording, callouts, hand-off cards)
111
+ + one **section** per slot: `intro`, `recap`, `outro`. Pick sections in demo.config.json
112
+ (project default) or video.json (this video), or with `reelson build --intro/--recap/--outro`
113
+ (written into video.json). `reelson templates` lists them all. Shipped with `classic`:
114
+
115
+ ```
116
+ INTRO (0 → exit) RECORDING RECAP OUTRO
117
+ poster wordmark over title, chip framed clip, callouts, steps title left, one wordmark big, ends at 15%
118
+ centred, belt hand-over zooms, hand-off cards column of steps compact wordmark | title, short
119
+ minimal small brand row, big title, compact centred chips, endcard brand + title + chip,
120
+ left-aligned, fades shorter ends fully readable
121
+ split brand panel left, title none (no recap)
122
+ right, doors open
123
+ ```
124
+
125
+ Defaults are `poster` / `steps` / `wordmark`. Rules of thumb: `minimal` + `compact` + `compact`
126
+ for short clips (< 15 s); `recap: none` when there are ≤ 2 steps; `endcard` when the video is
127
+ embedded where it pauses on the last frame (docs pages, GitHub). Every intro keeps frame 0 as
128
+ the poster (thumbnail in chat apps, GitHub, Finder); every outro ends on the brand.
129
+
130
+ **Logo**: `brand.logo` (demo.config.json, or per video in video.json `brand`) — an .svg (best),
131
+ .png (≥ 340 px tall) or .webp relative to the project root — replaces the text wordmark in
132
+ every intro and outro.
133
+
134
+ **Hand-offs**: when markers.json has `transitions` (from `demo.transition()`), the footage is
135
+ split into one clip per actor with a hand-off card between them; callouts after it shift
136
+ automatically and get the role as `group`, so the recap splits per role.
137
+
138
+ ## Workflow
139
+
140
+ ```bash
141
+ reelson build <slug> --title "Find a customer" --subtitle "..." # first run creates video.json
142
+ # edit video.json: word the callouts, add zooms (by click number), adjust trim
143
+ reelson build <slug> # regenerate video/ from video.json
144
+ reelson check <slug> # schemas, zoom timing, `hyperframes check`
145
+ reelson studio <slug> # for the user: preview + edit on a layer timeline
146
+ reelson snapshot <slug> --at 1.2,3.5,6,10 [--portrait | --square] # PNGs in video/snapshots/ — read them
147
+ reelson render <slug> [--gif] [--draft] [--portrait] [--square] [--all-formats] # build + render video/renders/<slug>.mp4
148
+ ```
149
+
150
+ 1. **First build.** It creates video.json with one callout per marker (the label as text), a
151
+ `"trim": { "start": "auto" }` (just before the first logged glide, i.e. after the login) and no
152
+ zooms, then prints the timeline. `--title/--subtitle/--trim-start (s or auto)/--trim-end/--template/
153
+ --intro/--recap/--outro/--music/--no-music` edit video.json in place on any build.
154
+ 2. **Edit video.json**, rebuild. Nothing in `video/` is edited by hand; it is overwritten.
155
+ 3. **Check and look.** `reelson check` must report "ready to render" (0 problems). Layout
156
+ `info` items about the recap/brand cross-fade overlap and off-canvas glows are expected.
157
+ Then snapshot the poster (t=0), the intro's hand-over, a callout, a zoom and the recap, and
158
+ look at them. When the user wants to review or adjust it themselves, run `reelson studio <slug>
159
+ --no-open` in the background and give them the URL: it follows your video.json edits live,
160
+ and the user's edits there (drag callouts, trim, zooms, sections, titles) are saved to
161
+ video.json — so re-read video.json before each of your own edits while it runs.
162
+ 4. **Render.** `reelson render` rebuilds first. ~30–60 s per 12 s of video. `--all` renders
163
+ every demo in the project, skipping the ones unchanged since their last render (`--force`
164
+ renders anyway); `--gif` adds a 720 px, 12 fps GIF for READMEs (cut from the MP4 with ffmpeg), `--portrait` adds a phone-first
165
+ 1080x1920 version (its own composition, video/portrait.html). Its source is video.json
166
+ `"portrait"`: `"mobile"` — the phone take (`reelson record <slug> --mobile`) whole in a phone
167
+ frame, taps as ripples; best for any responsive app; `"desktop"` — a camera over the desktop
168
+ recording that frames each element the demo works on (with its label/row) as close as it
169
+ fits, never cutting it — readable for compact UIs, small for wide admin screens; `"auto"`
170
+ (default) — the phone take when there is one. The intro, recap, outro and hand-off cards use
171
+ their portrait layouts: stacked, with bigger type. `--square` adds a 1080x1080 version (its own
172
+ composition, video/square.html) from the square take (`reelson record <slug> --square`: the app
173
+ in a square browser, `record.square.viewport`, default 1080x1080), filling the whole frame —
174
+ no bands; zooms carry over while the take has the same clicks as the desktop one; the cards
175
+ use their square layouts. Without a square take `--square` fails and says to record one.
176
+ `--all-formats` adds both (a missing square take is skipped with a note), and video.json
177
+ `"formats": ["portrait", "square"]` makes every plain `reelson render` add them. In landscape
178
+ and square the callouts sit over the bottom of the footage; one that would cover what the
179
+ cursor works on there moves to the top by itself. Every render also writes
180
+ `renders/<slug>.srt` and `.vtt` (and `<slug>.portrait.*` / `<slug>.square.*` for those
181
+ versions, timed to their takes): the title and the callouts as captions, timed to the video. For a quick look, `--draft` renders
182
+ 15 fps at draft quality (about 2x faster) to `renders/<slug>.draft.mp4` — or skip rendering
183
+ altogether and scrub in `reelson studio`.
184
+ 5. **Verify the artifact**, not the log: `ffprobe` duration ≈ the printed total; extract frames
185
+ (`ffmpeg -ss <t> -i video/renders/<slug>.mp4 -frames:v 1 f.png`) at t=0, ~2 s, a callout, a
186
+ zoom, the recap, the end. Look at them. Then report the file path.
187
+
188
+ After the app changes (a release, a redesign), run `reelson verify --all`: it re-records every
189
+ demo — each of its takes: desktop, and the phone and square ones when it has them — into a scratch folder and reports scenarios that no longer run, callouts/trims whose
190
+ marker is gone, and zoom/trim click numbers that now point at a different click. Fix those in
191
+ scenario.ts / video.json, then `reelson verify <slug> --update` keeps the new take.
192
+
193
+ Sharp UI text comes from the 2x capture; the build keeps it (`media.autoProxy: false`, no
194
+ `will-change` on `#frame`, jpg frames — png at 2x can stall the renderer).
195
+
196
+ ## Audio
197
+
198
+ The build handles audio; never add `<audio>` by hand.
199
+
200
+ - **Music bed** from demo.config.json `music.file` (or video.json `music`: a file for this video,
201
+ or `false`): trimmed to the length, normalised to `music.lufs` (−28 LUFS), faded in 0.8 s /
202
+ out 3 s → `assets/music.m4a`. Cached; re-rendered when the source, length or loudness changes.
203
+ One track per project is part of the brand.
204
+ - **Narration** when the recording has an audio track (OpenScreen): extracted and played in
205
+ sync; the bed drops to `music.lufsUnderNarration`. (It is not split at hand-off cards.)
206
+
207
+ ## HyperFrames rules that bite (when editing a template or section)
208
+
209
+ - A timed `<video>` may not sit inside a timed element (`#screen` is untimed on purpose).
210
+ - `fromTo()` renders its from-state at construction (`immediateRender`). Every "out" tween needs
211
+ `immediateRender: false` (`...later`); anything whose entry uses `...later` needs an explicit
212
+ hidden state in the section's closing `gsap.set`. Intro tweens that start at t=0 must NOT use
213
+ `later` (frame 0 is the poster).
214
+ - Animate transforms/opacity only (`x`, `y`, `scale`, `scaleX`, `opacity`, `filter`).
215
+ - One `gsap.timeline({ paused: true })`, registered as `window.__timelines[<composition id>]`;
216
+ explicit positions; no `.play()`, `Math.random()`, `Date.now()`, rAF, async.
217
+ - `<video>` must be `muted`. No CSS `transform` on anything GSAP moves. Root `data-duration`
218
+ defines the output length.
219
+ - Text from video.json goes in with `textContent`, never `innerHTML`.
220
+
221
+ ## Going beyond the template
222
+
223
+ Narration (`npx hyperframes tts`), captions, shader transitions, device mockups: install the
224
+ official skills (`npx skills add heygen-com/hyperframes`) or read
225
+ https://hyperframes.heygen.com/llms.txt. A new opening or ending that should be reusable → a new
226
+ section (`<videosDir>/_sections/<slot>/<name>/`); a new background or frame → a new template.