luckiest-co 1.0.39 → 1.0.41

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/skills/ad-creative/CHANGELOG.md +7 -0
  5. package/skills/ad-creative/SKILL.md +23 -4
  6. package/skills/ad-creative/references/generative-tools.md +6 -30
  7. package/skills/ads/CHANGELOG.md +9 -0
  8. package/skills/ads/SKILL.md +19 -9
  9. package/skills/ads/references/reading-google-ads-data.md +121 -0
  10. package/skills/ai-seo/CHANGELOG.md +11 -0
  11. package/skills/ai-seo/SKILL.md +21 -22
  12. package/skills/ai-seo/references/content-types.md +11 -0
  13. package/skills/ai-seo/references/linkedin-ai-citations.md +122 -0
  14. package/skills/ai-seo/references/platform-ranking-factors.md +32 -19
  15. package/skills/ai-seo/references/positioning-and-consensus.md +102 -0
  16. package/skills/coder/CHANGELOG.md +23 -0
  17. package/skills/coder/SKILL.md +16 -4
  18. package/skills/coder-brainstorming/CHANGELOG.md +9 -0
  19. package/skills/coder-brainstorming/SKILL.md +5 -3
  20. package/skills/coder-debugging/CHANGELOG.md +11 -0
  21. package/skills/coder-debugging/SKILL.md +15 -3
  22. package/skills/coder-executing-plans/CHANGELOG.md +9 -0
  23. package/skills/coder-executing-plans/SKILL.md +5 -3
  24. package/skills/coder-finishing-a-branch/CHANGELOG.md +9 -0
  25. package/skills/coder-finishing-a-branch/SKILL.md +7 -3
  26. package/skills/coder-guard/CHANGELOG.md +7 -0
  27. package/skills/coder-guard/SKILL.md +6 -6
  28. package/skills/coder-receiving-code-review/CHANGELOG.md +9 -0
  29. package/skills/coder-receiving-code-review/SKILL.md +5 -3
  30. package/skills/coder-shipping/CHANGELOG.md +9 -0
  31. package/skills/coder-shipping/SKILL.md +4 -4
  32. package/skills/coder-subagent-driven-development/CHANGELOG.md +9 -0
  33. package/skills/coder-subagent-driven-development/SKILL.md +5 -3
  34. package/skills/coder-writing-plans/CHANGELOG.md +9 -0
  35. package/skills/coder-writing-plans/SKILL.md +5 -3
  36. package/skills/cold-email/CHANGELOG.md +7 -0
  37. package/skills/cold-email/SKILL.md +15 -8
  38. package/skills/cold-email/references/follow-up-sequences.md +4 -4
  39. package/skills/cold-email/references/frameworks.md +5 -5
  40. package/skills/cold-email/references/personalization.md +10 -10
  41. package/skills/competitor-profiling/CHANGELOG.md +8 -0
  42. package/skills/competitor-profiling/SKILL.md +34 -9
  43. package/skills/competitor-profiling/references/tool-reference.md +141 -5
  44. package/skills/competitors/CHANGELOG.md +8 -0
  45. package/skills/competitors/SKILL.md +32 -5
  46. package/skills/competitors/references/content-architecture.md +6 -0
  47. package/skills/competitors/references/templates.md +3 -0
  48. package/skills/copy-editing/CHANGELOG.md +10 -0
  49. package/skills/copy-editing/SKILL.md +41 -9
  50. package/skills/copy-editing/references/checklist.md +11 -0
  51. package/skills/copywriting/CHANGELOG.md +11 -0
  52. package/skills/copywriting/SKILL.md +32 -4
  53. package/skills/copywriting/references/copy-frameworks.md +2 -3
  54. package/skills/copywriting/references/natural-transitions.md +4 -20
  55. package/skills/customer-research/CHANGELOG.md +8 -0
  56. package/skills/customer-research/SKILL.md +6 -3
  57. package/skills/directory-submissions/CHANGELOG.md +9 -0
  58. package/skills/directory-submissions/SKILL.md +21 -10
  59. package/skills/emails/CHANGELOG.md +8 -0
  60. package/skills/emails/SKILL.md +22 -3
  61. package/skills/emails/references/sequence-templates.md +3 -3
  62. package/skills/image/CHANGELOG.md +8 -0
  63. package/skills/image/SKILL.md +4 -4
  64. package/skills/image/references/ai-image-prompting.md +1 -1
  65. package/skills/marketing-plan/CHANGELOG.md +8 -0
  66. package/skills/marketing-plan/SKILL.md +8 -4
  67. package/skills/marketing-plan/references/market-sizing.md +166 -0
  68. package/skills/marketing-plan/references/methodology.md +2 -0
  69. package/skills/model-router/CHANGELOG.md +9 -0
  70. package/skills/model-router/SKILL.md +3 -3
  71. package/skills/model-router/references/weave-routing-notes.md +1 -0
  72. package/skills/model-router/scripts/route.py +3 -1
  73. package/skills/prospecting/CHANGELOG.md +7 -0
  74. package/skills/prospecting/SKILL.md +3 -3
  75. package/skills/prospecting/references/saas-prospecting.md +1 -1
  76. package/skills/public-relations/CHANGELOG.md +10 -0
  77. package/skills/public-relations/SKILL.md +16 -4
  78. package/skills/public-relations/references/crisis-communications.md +111 -0
  79. package/skills/public-relations/references/newsjacking.md +2 -0
  80. package/skills/public-relations/references/statement-templates.md +143 -0
  81. package/skills/sales-enablement/CHANGELOG.md +8 -0
  82. package/skills/sales-enablement/SKILL.md +72 -4
  83. package/skills/sales-enablement/references/win-loss-analysis.md +72 -0
  84. package/skills/skill-audit/ATTRIBUTION.md +5 -0
  85. package/skills/skill-audit/CHANGELOG.md +19 -0
  86. package/skills/skill-audit/SKILL.md +121 -0
  87. package/skills/skill-audit-security/ATTRIBUTION.md +14 -0
  88. package/skills/skill-audit-security/CHANGELOG.md +14 -0
  89. package/skills/skill-audit-security/LICENSE-APACHE-2.0 +190 -0
  90. package/skills/skill-audit-security/SKILL.md +99 -0
  91. package/skills/skill-audit-security/references/rule-ids.md +28 -0
  92. package/skills/skill-audit-security/references/source-review.md +58 -0
  93. package/skills/sms/CHANGELOG.md +8 -0
  94. package/skills/sms/SKILL.md +18 -4
  95. package/skills/sms/references/whatsapp.md +258 -0
  96. package/skills/social/CHANGELOG.md +11 -0
  97. package/skills/social/SKILL.md +31 -7
  98. package/skills/social/references/listening.md +2 -0
  99. package/skills/social/references/platforms.md +15 -9
  100. package/skills/social/references/post-templates.md +9 -9
  101. package/skills/social/references/short-form-video.md +5 -5
  102. package/skills/social/references/x-algorithm.md +69 -0
  103. package/skills/trends/CHANGELOG.md +3 -0
  104. package/skills/trends/SKILL.md +14 -4
  105. package/skills/video/CHANGELOG.md +8 -0
  106. package/skills/video/SKILL.md +11 -25
  107. package/skills/video-studio/ATTRIBUTION.md +3 -0
  108. package/skills/video-studio/CHANGELOG.md +25 -0
  109. package/skills/video-studio/LICENSE-motionmaxxing +202 -0
  110. package/skills/video-studio/SKILL.md +115 -25
  111. package/skills/video-studio/references/audio.md +15 -0
  112. package/skills/video-studio/references/interview.md +75 -0
  113. package/skills/video-studio/references/step-2-plan.md +39 -7
  114. package/skills/video-studio/references/storyboard.md +3 -1
  115. package/skills/video-studio/references/tones.md +5 -5
  116. package/skills/video-studio/scripts/pyproject.toml +4 -2
  117. package/skills/video-studio/scripts/qa.mjs +85 -1
  118. package/skills/video-studio/scripts/qa.test.mjs +37 -1
  119. package/skills/video-studio/scripts/render-scenes.mjs +1 -0
  120. package/skills/video-studio/scripts/storyboard.schema.json +6 -1
  121. package/skills/video-studio/scripts/uv.lock +8 -5
  122. package/skills/video-studio-art-director/ATTRIBUTION.md +3 -0
  123. package/skills/video-studio-art-director/CHANGELOG.md +7 -0
  124. package/skills/video-studio-art-director/LICENSE-motionmaxxing +202 -0
  125. package/skills/video-studio-art-director/SKILL.md +42 -7
  126. package/skills/video-studio-composition/ATTRIBUTION.md +3 -0
  127. package/skills/video-studio-composition/CHANGELOG.md +7 -0
  128. package/skills/video-studio-composition/LICENSE-motionmaxxing +202 -0
  129. package/skills/video-studio-composition/SKILL.md +17 -4
  130. package/skills/video-studio-director/ATTRIBUTION.md +3 -0
  131. package/skills/video-studio-director/CHANGELOG.md +15 -0
  132. package/skills/video-studio-director/LICENSE-motionmaxxing +202 -0
  133. package/skills/video-studio-director/SKILL.md +105 -10
  134. package/skills/video-studio-motion/ATTRIBUTION.md +3 -0
  135. package/skills/video-studio-motion/CHANGELOG.md +8 -0
  136. package/skills/video-studio-motion/LICENSE-motionmaxxing +202 -0
  137. package/skills/video-studio-motion/SKILL.md +98 -5
  138. package/skills/video-studio-motion/references/kinetic-type.md +89 -0
  139. package/skills/video-studio-motion/references/ui-demo.md +59 -0
  140. package/skills/video-studio-review/CHANGELOG.md +11 -0
  141. package/skills/video-studio-review/SKILL.md +31 -5
  142. package/skills/video-studio-review/editor/index.html +21 -3
  143. package/skills/video-studio-review/references/apply-feedback.md +19 -0
  144. package/skills/video-studio-review/scripts/build-reel.mjs +35 -10
  145. package/skills/video-studio-review/scripts/build-reel.test.mjs +15 -0
  146. package/skills/video-studio-script/ATTRIBUTION.md +6 -0
  147. package/skills/video-studio-script/CHANGELOG.md +12 -0
  148. package/skills/video-studio-script/LICENSE-motionmaxxing +202 -0
  149. package/skills/video-studio-script/SKILL.md +41 -10
  150. package/skills/video-studio-showreel/CHANGELOG.md +21 -0
  151. package/skills/video-studio-showreel/SKILL.md +231 -0
  152. package/skills/video-studio-showreel/references/motion-design-pack-notes.md +28 -0
  153. package/skills/video-studio-showreel/references/zingage-breakdown.md +47 -0
  154. package/skills/video-studio-showreel/scripts/build.py +10 -0
  155. package/skills/video-studio-showreel/templates/showreel.html +264 -0
  156. package/skills/video-watcher/CHANGELOG.md +6 -0
  157. package/skills/video-watcher/SKILL.md +3 -3
  158. package/skills/video-watcher/scripts/setup.py +9 -7
  159. package/skills/trends/agents/openai.yaml +0 -8
  160. package/skills/trends/scripts/build-skill.sh +0 -39
  161. package/skills/trends/scripts/compare.sh +0 -61
  162. package/skills/trends/scripts/evaluate_search_quality.py +0 -573
  163. package/skills/trends/scripts/test-v1-vs-v2.sh +0 -220
  164. package/skills/trends/scripts/test_device_auth.py +0 -128
  165. package/skills/trends/scripts/verify_v3.py +0 -195
@@ -16,7 +16,7 @@ user-invocable: true
16
16
  license: MIT. See ATTRIBUTION.md
17
17
  allowed-tools: Bash, Read, Write, Edit, Glob, Grep
18
18
  metadata:
19
- version: "1.1.0"
19
+ version: "1.2.0"
20
20
  listing_id: luckiest-video-studio-motion
21
21
  author: luckiest
22
22
  ---
@@ -32,11 +32,11 @@ interruption, and how often a user triggers something.
32
32
  ## Staying current
33
33
 
34
34
  On activation, call the Luckiest MCP `check_updates` tool with
35
- `{ listingId: "luckiest-video-studio-motion", installedSemver: "1.1.0" }`. If
35
+ `{ listingId: "luckiest-video-studio-motion", installedSemver: "1.2.0" }`. If
36
36
  `upToDate: false`, surface the `notice` once and continue. Never block on it.
37
37
 
38
38
  When done, call `report_usage` once with
39
- `{ listing_id: "luckiest-video-studio-motion", skill_version: "1.1.0", matched: true, success: <true|false> }`.
39
+ `{ listing_id: "luckiest-video-studio-motion", skill_version: "1.2.0", matched: true, success: <true|false> }`.
40
40
  Metadata only, never prompt text. Skip silently if unavailable.
41
41
 
42
42
  ## Modes
@@ -87,6 +87,20 @@ Match overshoot to the tone in
87
87
  [tones.md](../luckiest-video-studio/references/tones.md): crisp for polished, bouncy
88
88
  only for playful or chaotic.
89
89
 
90
+ **Land decelerates, leave accelerates.** On professional films most of an
91
+ entrance's travel happens in its first 4 to 8 frames, then a long tail with no
92
+ bounce; exits ease in, then leave or shrink. One visible overshoot or spring per
93
+ film, 4 to 12% past the target, spent on the hero moment. Everything else lands
94
+ clean.
95
+
96
+ | Role | GSAP | CSS cubic-bezier (for stills and editors) |
97
+ |---|---|---|
98
+ | Soft land (most entrances) | `expo.out` | `cubic-bezier(0.16, 1, 0.3, 1)` |
99
+ | Snap and settle (CTA, toggles, counters) | `power4.out` | `cubic-bezier(0.165, 0.84, 0.44, 1)` |
100
+ | A to B on screen | `power3.inOut` | `cubic-bezier(0.645, 0.045, 0.355, 1)` |
101
+ | Accelerating exit | `power2.in` to `power3.in` | `cubic-bezier(0.55, 0.055, 0.675, 0.19)` |
102
+ | The one overshoot | `back.out(1.4)` or a spring | n/a |
103
+
90
104
  ## 3. Duration and hold
91
105
 
92
106
  | Element | Duration |
@@ -101,10 +115,30 @@ After the move, hold. A short label settles about 0.8s and a sentence about
101
115
  0.3s per word (the main skill's readability law). Fast in, long hold beats slow in,
102
116
  short hold.
103
117
 
118
+ Measured on professional films at 30fps (use as the default when the tone gives
119
+ no reason otherwise):
120
+
121
+ | Move | Frames | Seconds |
122
+ |---|---|---|
123
+ | Median move | 10 | 0.33 |
124
+ | Translate in | 12 | 0.4 |
125
+ | Opacity on a single object | 6 | 0.2 |
126
+ | Blur resolve | 7 | 0.23 |
127
+ | Translate out | 13-14 | 0.45 |
128
+ | Scale out | 10 | 0.33 |
129
+ | CTA entrance | 6 | 0.2 |
130
+
131
+ A held frame is never dead: keep one living thing (a slow drift of 1 to 3px per
132
+ frame, a caret, moving light). Something new every 0.3 seconds at most, a major
133
+ change about every 0.8 seconds.
134
+
104
135
  ## 4. Physicality and origin
105
136
 
106
137
  - **Nothing appears from nothing.** Cards, panels, text blocks, and screenshots
107
- enter from `scale: 0.9-0.97` with `autoAlpha: 0`, never `scale: 0`. A small accent
138
+ enter from `scale: 0.9-0.97` with `autoAlpha: 0`, never `scale: 0`. Hero
139
+ arrivals decelerate from oversize instead: a logo crashes in from about 3.5x to
140
+ 1.0 within 20 frames, a cursor enters 1.5 to 5x its size, a thesis word shrinks
141
+ into place. A small accent
108
142
  (a dot, badge, or icon) may pop from `scale: 0` only with a `back.out` or
109
143
  `steps()` ease, where the overshoot is the point.
110
144
  - **Scale from the source.** A callout over demo footage grows from the point it
@@ -131,14 +165,71 @@ short hold.
131
165
  cut instead.
132
166
  - **Stagger groups.** Items entering together stagger 40-100ms (30-80ms for mock
133
167
  UI). Everything at once reads as a slide change. A long stagger reads as slow.
168
+ Measured: the gap is about half the move's duration (a 3-frame median); a CTA
169
+ wave uses 8-frame gaps and finishes inside 0.6s.
170
+ - **Never crossfade scenes.** Things arrive by moving, cutting, unmasking or
171
+ focusing. Fades are for single objects (calm brands and privacy dissolves are
172
+ the exceptions, stated in the plan).
134
173
  - **Asymmetric timing.** Slow where the viewer should take something in, snappy
135
174
  where the system responds: a 1.2s linear progress fill, then a 0.2s `power3.out`
136
175
  confirmation.
137
176
  - **Sound on the same frame.** An SFX lands on the frame its visual cause lands,
138
177
  never a frame early or late ([audio.md](../luckiest-video-studio/references/audio.md)).
139
178
 
179
+ ### Handoffs between scenes
180
+
181
+ The last frame of scene A is the first frame of scene B. For each cut, write:
182
+
183
+ 1. What carries across (an object, a shape, a colour, or nothing).
184
+ 2. A's exit state over its last 1 to 3 frames: position, size as % of frame
185
+ height, speed, blur, ground colour.
186
+ 3. B's frame 0 matches that state within 2% of the frame, and something in B is
187
+ already moving.
188
+ 4. A ground colour change is caused by an object (a card fills the frame), never
189
+ a flat swap.
190
+ 5. Cut at peak speed, never on rest. A speeds up over its last 3 to 8 frames; 0 to
191
+ 3 empty frames between are fine.
192
+ 6. Which spoken word lands on the cut.
193
+
194
+ An object pinned at the same coordinates through three or more scenes is a
195
+ defect. A carrier travels at every cut.
196
+
197
+ | What changes from A to B | Use |
198
+ |---|---|
199
+ | Same object, new state | Match cut on the object (morph, scale, recolor) |
200
+ | New subject, same world | Cut on motion, or a whip (8 frames, peak speed a quarter in, about 13% of frame width per frame) |
201
+ | Soft change of topic | Blur-matched cut: A blurs out to about 18px, B starts at about 80px and resolves in about 8 frames |
202
+ | New ground colour | Feathered wipe painted in B's ground (feather 30 to 40% of the axis, 0.56 to 0.6s, starting 0.1 to 0.2s before the voice line ends) |
203
+ | Light to dark theme | A 4-frame grey dip hides the flip |
204
+ | Into the product | A camera push into the UI, then cut inside it |
205
+
206
+ Cuts fall every 0.3 to 2.3 seconds. Transitions start 3 to 6 frames before the
207
+ voice line ends.
208
+
209
+ ### Camera
210
+
211
+ - **Push** size by duration: 1.25x over 6 frames, 1.4x over 12 to 18, 1.5x over
212
+ 20 to 45. 2.4x only on a slow drift.
213
+ - **Snap zoom:** one jump frame changes scale by a ratio of 0.56 to 0.77 (or its
214
+ inverse), then a 9 to 18 frame tail settles it.
215
+ - **Parallax:** near layers move 2 to 2.4x the far ones.
216
+ - **Drift:** 1 to 3px per frame keeps a hold alive.
217
+
218
+ ### One system per film
219
+
220
+ One frame clock (smooth, or on twos for a hand-made look), one blur grammar
221
+ (motion blur, or smear frames, or crisp steps), one accent colour with one
222
+ meaning (for example "done"). Mixing clocks or blur styles inside one film reads
223
+ as two films.
224
+
140
225
  ## 6. Type and depth
141
226
 
227
+ Read [references/kinetic-type.md](references/kinetic-type.md) when a scene
228
+ animates words, a counter, or typing. Read
229
+ [references/ui-demo.md](references/ui-demo.md) when a scene shows product UI, a
230
+ cursor, a click, or a toggle.
231
+
232
+
142
233
  - Display text tracks tight (`letter-spacing: -0.02em` and below as size grows,
143
234
  `line-height` about 1.05). Captions and small labels track slightly open.
144
235
  - Build hierarchy with weight, size, and leading together, not size alone.
@@ -161,7 +252,9 @@ has been read. For each scene in `storyboard.json`, read its composition and wri
161
252
  2. **Verdict per scene: Block or Approve.** Default to flagging; approval is
162
253
  earned. Block on: a motion beat with no purpose, an `.in` ease on an entrance,
163
254
  `scale: 0` on a card or text block, a full-frame opacity crossfade, drifting text
164
- the viewer is reading, or overshoot that fights the tone.
255
+ the viewer is reading, overshoot that fights the tone, a second visible
256
+ overshoot in the film, a cut on rest, an object pinned through three scenes, or
257
+ a headline typed letter by letter.
165
258
 
166
259
  Fix in this order, preferring earlier moves: delete the motion, reduce it, fix the
167
260
  easing, fix the origin, mask the transition (blur or clip-path), stagger the
@@ -0,0 +1,89 @@
1
+ # Kinetic type
2
+
3
+ Numbers for words on screen, measured on professional films at 30fps. Ideas and
4
+ values from Tejas Makwana's `motionmaxxing` (Apache-2.0), restated for HyperFrames
5
+ and GSAP. See `../ATTRIBUTION.md`.
6
+
7
+ ## Words arrive at speech rhythm
8
+
9
+ Animate whole words, not letters. Gap between word onsets:
10
+
11
+ | Pace | Gap |
12
+ |---|---|
13
+ | Fast hook | 2-4 frames |
14
+ | Headline | 4-6 frames |
15
+ | Voice-led line | 8-12 frames |
16
+
17
+ - The stinger (the last, most important word) waits 1.5 to 2x the normal gap.
18
+ - Vary each gap by up to 40% so it does not tick like a metronome.
19
+ - Each word rises 3 to 7% of the frame height, with most of its travel in the
20
+ first 5 frames (`expo.out`).
21
+ - An accent colour on a word lasts 0.13 to 0.28 seconds, then decays to the ink
22
+ colour. One accent word per line at most.
23
+
24
+ ```js
25
+ // gaps in frames at 30fps; pick the row for the pace
26
+ const gaps = [4, 5, 4, 8]; // last gap is the stinger
27
+ let t = 0;
28
+ words.forEach((w, i) => {
29
+ tl.from(w, { yPercent: 60, autoAlpha: 0, duration: 0.33, ease: "expo.out" }, t);
30
+ t += (gaps[i] ?? 5) / 30;
31
+ });
32
+ ```
33
+
34
+ ## Reading holds
35
+
36
+ | Text | Hold after it lands |
37
+ |---|---|
38
+ | Any text | 0.45 s minimum |
39
+ | Per word | about 0.2 s |
40
+ | 6 to 8 words | 1.7 to 2.0 s |
41
+ | A number | 0.5 to 1.2 s |
42
+ | About 20 characters | 1.2 s |
43
+
44
+ The studio's readability law (about 0.3 s per word for a sentence) is the safe
45
+ ceiling for narration-free videos; these are the floor. Never cut a line before
46
+ its floor.
47
+
48
+ ## On-screen words and the voice
49
+
50
+ - Words land 0 to 2 frames before they are heard (up to 5 frames for names).
51
+ - At most about 7 words on screen at once. Claims are 2 to 6 words.
52
+
53
+ ## Typing
54
+
55
+ Type only into the product's own input field. A headline is never typed letter
56
+ by letter with a caret: it reads as a template.
57
+
58
+ - 14 to 30 characters per second when the viewer must read it, bursts of 70 to 85
59
+ otherwise.
60
+ - Pause 6 to 10 frames after a space or a comma.
61
+ - The caret appears 1 frame before the first letter and stays solid (no blink)
62
+ while typing.
63
+ - Use a real query or prompt from the product, 20 to 70 characters.
64
+
65
+ ## Counters
66
+
67
+ - Step once per frame, 15 to 23 steps over 0.5 to 0.9 seconds, decelerating.
68
+ - Land on a round value for a claim ("2x"), an un-round value for data
69
+ ("1,284"). The number must be real (the `facts` gate).
70
+ - Spend the hero number once per film, at 22 to 29% of the frame height.
71
+
72
+ ## Sizes and weights
73
+
74
+ | Role | Size (share of frame height) |
75
+ |---|---|
76
+ | Headline | 7-11% (9% typical) |
77
+ | Supporting line | 5-7% |
78
+ | Hero number | 22-29%, once per film |
79
+ | Readable floor | 4% (under 2% reads as texture) |
80
+
81
+ - Weights 400 to 550. 600 only on a wordmark.
82
+ - Track display sizes tight, -1.5% to -5.5%. Sentence case.
83
+ - Use the brand's own face, a serif if the brand uses one.
84
+ - Most type is small. Scale is spent once; the ending usually gets smaller.
85
+
86
+ At 1080p these map onto the studio's type tokens (22, 30, 42, 60, 84px, checked
87
+ by `qa.mjs tokens`): headline 84px (7.8%), supporting 60px (5.6%), readable floor
88
+ 42px (3.9%). A hero number is off the default scale: add its one size to the run's
89
+ tokens file and pass it with `qa.mjs tokens --tokens <file>`.
@@ -0,0 +1,59 @@
1
+ # UI demos
2
+
3
+ How to show a real product on screen so it reads as proof, measured on
4
+ professional films at 30fps. Ideas and values from Tejas Makwana's
5
+ `motionmaxxing` (Apache-2.0), restated for HyperFrames and GSAP. See
6
+ `../ATTRIBUTION.md`.
7
+
8
+ ## Magnify with the camera, not the type
9
+
10
+ - Keep the UI at its real size (15 to 17pt text in the product) and push the
11
+ camera in: crop 1.4 to 4.4x.
12
+ - Proof is readable when UI text is at least 4% of the frame height and the
13
+ fragment is at least 55% of the frame width, or cropped by the frame edge.
14
+ - Keep the top and bottom 28% of the frame clear of UI when a line of copy shares
15
+ the shot.
16
+ - Show a magnified fragment with identity (the product's own colours, icons and
17
+ copy), lit and placed in a world, not a small card on a flat colour.
18
+ - Captured screens follow the studio's standing rule: no real customer data,
19
+ emails, keys or tokens on screen. Swap them for the product's demo data before
20
+ the capture, never blur them after.
21
+ - Never invent the UI. A rounded card with a status dot and "label . $value", a
22
+ "Good morning" card, grey skeleton bars, or made-up names and numbers read as
23
+ generic web components. Use the captured screens.
24
+
25
+ ## Cursor
26
+
27
+ | Step | Value |
28
+ |---|---|
29
+ | Size | 4 to 6% of the frame width |
30
+ | Entry | 1.5 to 5x its size, 60% of the travel in the first frame, over 0.5 to 0.9 s |
31
+ | Park | Still for 2 to 15 frames before it acts |
32
+ | Press | Squeezes to 0.71 to 0.9 scale over 2 frames |
33
+ | Target | Answers 0 to 1 frame after the press, dipping to 0.63 to 0.81 scale |
34
+ | Release | Cut before it; the release is never shown |
35
+
36
+ The cursor arrives, parks, then acts. It never clicks while still moving.
37
+
38
+ ## States
39
+
40
+ - A toggle flips in 2 frames.
41
+ - A cascade (rows reacting to one action) starts 5 frames after the action, then
42
+ 2 to 3 frames per item, all done within 0.6 s.
43
+ - A checklist row takes about 0.9 s.
44
+ - "Working" shimmer: a light band about 23% of the frame width sweeps for 13
45
+ frames, twice, with a 6-frame gap.
46
+ - A log or feed scroll accelerates from about 6 to 250px per frame over about
47
+ 1.8 s, then holds 0.8 s.
48
+
49
+ ## Depth behind UI
50
+
51
+ - Backdrop blur: 4px keeps the background readable, 7 to 15px for panels that
52
+ are not the hero, at most 11px behind a headline.
53
+ - Two panels in one frame only when one is at least twice the other's area, or
54
+ the camera moves between them. One hero per frame.
55
+
56
+ ## Typing in the product
57
+
58
+ See [kinetic-type.md](kinetic-type.md#typing): real queries, 14 to 30 characters
59
+ per second, a solid caret.
@@ -1,5 +1,16 @@
1
1
  # Changelog: luckiest-video-studio-review
2
2
 
3
+ ## 1.1.0 (2026-10-10)
4
+ - `build-reel.mjs --storyboard`: review the storyboard before any render. Joins
5
+ each scene's still (`scenes[].still`, default `storyboard/<id>.png`) into
6
+ `storyboard/animatic.mp4` and writes `reel.json` with `"stage": "storyboard"`.
7
+ Each scene's `line` (and `prompt` for `ai` scenes) is an editable field.
8
+ No `final.mp4` needed. Without the flag, nothing changes.
9
+ - Player: in the storyboard stage it draws the scene's still with its line and
10
+ length over the frame, Send to Claude works with no edits, and the Do line
11
+ says it is the storyboard. Render prompts are unchanged.
12
+ - `apply-feedback.md`: a storyboard stage section.
13
+
3
14
  ## 1.0.0 (2026-10-07)
4
15
  Built from Motion OS (`jasonlee-breadcrumb/motion-os`, MIT) as the review player
5
16
  sub-skill of luckiest-video-studio. Replaces the default Concat handoff and the
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: video-studio-review
3
3
  description: >-
4
- Open a rendered luckiest-video-studio run in the review player, a local
4
+ Open a luckiest-video-studio storyboard (before render) or a rendered run in the review player, a local
5
5
  scene-by-scene player (built on Motion OS) where the user edits each scene's
6
6
  copy and colors, pins notes on the exact frame, marks scenes approved, and
7
7
  copies one prompt back. Then apply that prompt: edits land in storyboard.json,
@@ -18,7 +18,7 @@ user-invocable: true
18
18
  license: MIT. See ATTRIBUTION.md
19
19
  allowed-tools: Bash, Read, Write, Edit, Glob, AskUserQuestion
20
20
  metadata:
21
- version: "1.0.0"
21
+ version: "1.1.0"
22
22
  listing_id: luckiest-video-studio-review
23
23
  author: luckiest
24
24
  ---
@@ -36,11 +36,11 @@ so the run stays the source of truth and untouched scenes never re-render.
36
36
  ## Staying current
37
37
 
38
38
  On activation, call the Luckiest MCP `check_updates` tool with
39
- `{ listingId: "luckiest-video-studio-review", installedSemver: "1.0.0" }`. If
39
+ `{ listingId: "luckiest-video-studio-review", installedSemver: "1.1.0" }`. If
40
40
  `upToDate: false`, surface the `notice` once and continue. Never block on it.
41
41
 
42
42
  When done, call `report_usage` once with
43
- `{ listing_id: "luckiest-video-studio-review", skill_version: "1.0.0", matched: true, success: <true|false> }`.
43
+ `{ listing_id: "luckiest-video-studio-review", skill_version: "1.1.0", matched: true, success: <true|false> }`.
44
44
  Metadata only, never prompt text. Skip silently if unavailable.
45
45
 
46
46
  ## Standing rules
@@ -57,7 +57,8 @@ Metadata only, never prompt text. Skip silently if unavailable.
57
57
  ## Step 1: Open the player
58
58
 
59
59
  Needs `node` 18+ and `ffprobe`. Run `render-scenes.mjs` first: the player shows
60
- `final.mp4`.
60
+ `final.mp4`. To review a storyboard before anything renders, see **Storyboard
61
+ review** below.
61
62
 
62
63
  ```bash
63
64
  node <skill-dir>/scripts/build-reel.mjs <run-dir> # writes <run-dir>/reel.json
@@ -102,6 +103,31 @@ node <skill-dir>/scripts/build-reel.mjs <run-dir>
102
103
  `build-reel.mjs` bumps `version`; the open player reloads within about 3 seconds.
103
104
  Reply with a short list of what changed and anything you could not do.
104
105
 
106
+ ## Storyboard review
107
+
108
+ Before the render, the studio's Step 2 reviews the plan scene by scene in the
109
+ same player. No `final.mp4` is needed:
110
+
111
+ ```bash
112
+ node <skill-dir>/scripts/build-reel.mjs <run-dir> --storyboard
113
+ node <skill-dir>/editor/serve.mjs <run-dir>
114
+ ```
115
+
116
+ It needs one still per scene (`scenes[].still`, default `storyboard/<id>.png`)
117
+ and `ffmpeg`. It joins the stills, each held for its scene's `duration`, into
118
+ `storyboard/animatic.mp4` and writes `reel.json` with `"stage": "storyboard"`.
119
+ The player draws each scene's still with its line and length over the frame.
120
+ Each scene's `line` is an editable field (and `prompt` for `ai` scenes), so the
121
+ Script tab holds the whole script. Edit, notes, Timing, and Approved work as on
122
+ a render. A missing still stops the build and names the file.
123
+
124
+ Send to Claude works even with no edits, so the user can send "all approved".
125
+ Its prompt has the same format as a render's, with a Do line that says it is the
126
+ storyboard. Apply it as the storyboard section of
127
+ [references/apply-feedback.md](references/apply-feedback.md) says, then rerun
128
+ `build-reel.mjs --storyboard`. When the prompt says every scene is approved, hand
129
+ back to the studio for Step 2b.
130
+
105
131
  ## Talking-head cuts
106
132
 
107
133
  `luckiest-video-studio-cuts` reviews its silence cut in the same player. Its
@@ -75,6 +75,10 @@ button{cursor:pointer}
75
75
  .viewer video{position:absolute;inset:0;width:100%;height:100%;display:block;background:#fff}
76
76
  #live{position:absolute;left:0;top:0;width:1920px;height:1080px;transform-origin:0 0;overflow:hidden}
77
77
  .overlay{position:absolute;inset:0}
78
+ /* storyboard stage: the scene's still and its script line over the frame */
79
+ .sb-still{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
80
+ .sb-cap{position:absolute;left:0;right:0;bottom:0;padding:10px 14px;background:linear-gradient(transparent,rgba(0,0,0,.78));color:#fff;font-size:15px;line-height:1.35;pointer-events:none}
81
+ .sb-cap small{display:block;font-family:var(--mono);font-size:11px;opacity:.75;margin-bottom:2px}
78
82
  .overlay.note-mode{cursor:crosshair}
79
83
  .hot{position:absolute;border:2px solid var(--accent);border-radius:6px;pointer-events:none;background:color-mix(in srgb,var(--accent) 7%,transparent)}
80
84
  .hot.hover{border-style:dashed;background:none}
@@ -291,6 +295,7 @@ button{cursor:pointer}
291
295
  <div class="viewer-wrap">
292
296
  <div class="viewer" id="viewer">
293
297
  <video id="vid" playsinline preload="auto"></video>
298
+ <img class="sb-still" id="sbStill" alt="" hidden><div class="sb-cap" id="sbCap" hidden></div>
294
299
  <div class="overlay" id="overlay"><div id="ghosts"></div><div id="hots"></div><div id="popLayer"></div></div>
295
300
  </div>
296
301
  </div>
@@ -362,6 +367,7 @@ function loadProject(){
362
367
  ui = {...ui, scene:P.scenes[0].id, sel:null, pop:null, selNote:null};
363
368
  $('#projTitle').textContent = P.title; $('#projVer').textContent = 'v' + ver; document.title = P.title + ' · Motion OS';
364
369
  vid.src = P.src + '?v=' + ver; vid.currentTime = 0.7;
370
+ $('#sbStill').hidden = $('#sbCap').hidden = P.stage !== 'storyboard';
365
371
  setupMusic(); renderAll(); tick();
366
372
  }
367
373
 
@@ -389,6 +395,7 @@ function markDirty(){
389
395
  if (!P) return;
390
396
  try { localStorage.setItem(KEY(), JSON.stringify(st)); } catch (e) {}
391
397
  $('#sendN').textContent = pendingEdits() + openNotes().length;
398
+ if (P.stage === 'storyboard') storyboardFrame(sceneAt(now()));
392
399
  }
393
400
 
394
401
  /* ================= playback ================= */
@@ -439,7 +446,15 @@ function tick(){
439
446
  const t = now(), s = sceneAt(t);
440
447
  if (s.id !== ui.scene && playing() && !ui.loop) { ui.scene = s.id; if (ui.sel && elById(ui.sel)?.s !== s.id) ui.sel = null; renderTL(); if (ui.tab === 'scene') renderPane(); }
441
448
  $('#tc').innerHTML = `${fmt(t)} <small>/ ${fmt(P.duration)} · ${s.id} ${esc(s.name)}</small>`;
442
- placePH(); renderHots();
449
+ placePH(); renderHots(); if (P.stage === 'storyboard') storyboardFrame(s);
450
+ }
451
+ // Storyboard stage (reel.json "stage": "storyboard"): nothing is rendered yet, so draw the scene's still with its
452
+ // line and length on the frame. The line shows the user's unsent edit, so they see what they will send.
453
+ function storyboardFrame(s){
454
+ const e = s.els[0], img = $('#sbStill'), src = s.still + '?v=' + (P.version || 1);
455
+ if (img.getAttribute('src') !== src) img.setAttribute('src', src);
456
+ const [a, b] = elTime(e), kind = e.props.motion?.v || '';
457
+ $('#sbCap').innerHTML = `<small>${esc(s.id)} · ${esc(kind)} · ${(b - a).toFixed(1)}s</small>${esc(e.props.line ? pv(e, 'line') : e.label)}`;
443
458
  }
444
459
  function setupMusic(){
445
460
  if (music) { music.pause(); music = null; }
@@ -801,13 +816,16 @@ function prompt(){
801
816
  const links = st.links.filter(l => !l.sent);
802
817
  if (links.length) { L.push('## Links to download into assets/'); links.forEach(l => L.push('- ' + l.url)); L.push(''); }
803
818
  L.push('## Scenes', P.scenes.map(s => `${s.id} ${s.name} (${fmt(s.t[0])}-${fmt(s.t[1])}): ${st.status[s.id] === 'approved' ? 'approved, do not touch' : st.status[s.id] === 'changes' ? 'needs changes' : 'in review'}`).join('\n'), '');
804
- L.push('## Do', `Use the luckiest-video-studio-review skill. Apply the edits exactly and work through the notes. Leave approved scenes alone. Re-render, check it, then update reel.json and set "version" to ${ver + 1} so Motion OS reloads.`);
819
+ if (P.stage === 'storyboard') L.push('## Do', P.scenes.every(s => st.status[s.id] === 'approved')
820
+ ? 'Use the luckiest-video-studio-review skill. This is the storyboard and every scene is approved. Apply any edits above to storyboard.json, then go on to the assets, the style frame, and the render.'
821
+ : `Use the luckiest-video-studio-review skill. This is the storyboard, nothing is rendered yet. Apply the edits exactly to storyboard.json and work through the notes. Leave approved scenes alone. Redraw only the changed stills, then rerun build-reel.mjs --storyboard so Motion OS reloads at version ${ver + 1}.`);
822
+ else L.push('## Do', `Use the luckiest-video-studio-review skill. Apply the edits exactly and work through the notes. Leave approved scenes alone. Re-render, check it, then update reel.json and set "version" to ${ver + 1} so Motion OS reloads.`);
805
823
  return L.join('\n');
806
824
  }
807
825
  function sheet(html){ const d = document.createElement('div'); d.className = 'scrim'; d.innerHTML = `<div class="sheet" role="dialog" aria-modal="true">${html}</div>`;
808
826
  d.addEventListener('click', e => { if (e.target === d || e.target.closest('[data-close]')) d.remove(); }); document.body.appendChild(d); return d; }
809
827
  $('#sendBtn').onclick = () => {
810
- if (!(pendingEdits() + openNotes().length)) return toast('Nothing to send yet. Edit something, or press N to pin a note.');
828
+ if (!(pendingEdits() + openNotes().length) && P.stage !== 'storyboard') return toast('Nothing to send yet. Edit something, or press N to pin a note.');
811
829
  const text = prompt();
812
830
  const d = sheet(`<h3>Send to Claude</h3><p>Copy this, then paste it into Claude.</p><textarea class="out" readonly></textarea><div class="row"><button class="btn ghost" data-close>Close</button><button class="btn primary" id="doCopy">Copy prompt</button></div>`);
813
831
  d.querySelector('textarea').value = text;
@@ -69,3 +69,22 @@ look at a contact sheet before showing anything:
69
69
  Then rerun `build-reel.mjs`. The prompt
70
70
  asks for version `N+1`; `build-reel.mjs` sets it. Reply with what changed, what
71
71
  was skipped and why, and anything you could not do.
72
+
73
+ ## Storyboard stage
74
+
75
+ A prompt whose Do line says "This is the storyboard" comes from a reel built with
76
+ `build-reel.mjs --storyboard`. Nothing is rendered yet, so:
77
+
78
+ - `Line` edits set `scenes[N].line`, and `Prompt` edits set `scenes[N].prompt`,
79
+ with the same before-value check. When the run has a `script.md`, write the new
80
+ line there too (the script sub-skill's Step 5).
81
+ - `timing (s)` edits set the scene's `duration`.
82
+ - Notes are read against the scene's still (`storyboard/<id>.png`), not a frame
83
+ of `final.mp4`. A note about the idea (the hook is weak, a beat is missing)
84
+ goes back to `luckiest-video-studio-director` to fix the shot list.
85
+ - A scene marked approved that also carries an edit was approved as edited:
86
+ apply that edit, then leave the scene alone.
87
+ - Never render. Redraw only the stills whose scene changed, then rerun
88
+ `build-reel.mjs --storyboard` so the player reloads at the next version.
89
+ - When every scene in the Scenes block is approved, apply any edits, then hand
90
+ back to `luckiest-video-studio` for Step 2b. That is the storyboard gate.
@@ -3,7 +3,10 @@
3
3
  // per storyboard scene, in storyboard order, with each scene's variables as editable fields. Rerunning bumps
4
4
  // "version" so an open player reloads itself. <run-dir>/qa/review-notes.json ([{ t, x, y, text, scene? }]), when
5
5
  // present, is pinned on the frame as Claude's own notes. Then serve it: node <skill-dir>/editor/serve.mjs <run-dir>
6
- // Usage: node build-reel.mjs <run-dir>
6
+ // Usage: node build-reel.mjs <run-dir> [--storyboard]
7
+ // --storyboard: review the plan before anything renders. Joins each scene's hold-frame still (scenes[].still, default
8
+ // storyboard/<id>.png) for its duration into storyboard/animatic.mp4 with ffmpeg and serves that instead of final.mp4.
9
+ // Each scene's line and (for "ai" scenes) its prompt become editable fields.
7
10
  import { spawnSync } from "node:child_process";
8
11
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
12
  import { basename, dirname, join, resolve } from "node:path";
@@ -12,6 +15,8 @@ import { fileURLToPath } from "node:url";
12
15
  const MEDIA = { image: /\.(png|jpe?g|webp|gif|svg)$/i, video: /\.(mp4|mov|webm)$/i, audio: /\.(mp3|wav|m4a)$/i };
13
16
  const mediaType = (v) => Object.keys(MEDIA).find((k) => MEDIA[k].test(v));
14
17
  const slug = (v) => v.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
18
+ export const stillOf = (s) => s.still ?? `storyboard/${s.id}.png`;
19
+ const ANIMATIC = "storyboard/animatic.mp4";
15
20
  const words = (id) => id.replace(/-/g, " ").replace(/^./, (c) => c.toUpperCase());
16
21
 
17
22
  // One field per variable: hex -> color, file path -> media, number -> number, other strings -> text.
@@ -26,7 +31,7 @@ function field(k, v) {
26
31
  }
27
32
 
28
33
  // Pure: the reel for a storyboard. prevVersion is the version of the reel.json already in the run, or 0.
29
- export function buildReel(sb, runDir, prevVersion = 0, duration = 0, qaNotes = []) {
34
+ export function buildReel(sb, runDir, prevVersion = 0, duration = 0, qaNotes = [], storyboard = false) {
30
35
  const assets = new Map();
31
36
  // The player's color fields pick from the brand palette, so every distinct hex becomes a palette entry and the
32
37
  // field links to it. Editing a palette color in the Style tab then changes every scene that uses it.
@@ -48,12 +53,17 @@ export function buildReel(sb, runDir, prevVersion = 0, duration = 0, qaNotes = [
48
53
  props[k] = f;
49
54
  if (f.type === "media") asset(v, "Run");
50
55
  }
56
+ if (storyboard) {
57
+ props.line = { type: "longtext", label: "Line", v: s.line ?? "" };
58
+ if (s.prompt) props.prompt = { type: "longtext", label: "Prompt", v: s.prompt };
59
+ asset(stillOf(s), "Storyboard");
60
+ }
51
61
  if (s.kind === "demo" && s.source) { props.source = { type: "media", label: "Footage", v: s.source }; asset(s.source, "Footage"); }
52
62
  for (const a of s.assets ?? []) asset(a, "Run");
53
63
  props.motion = { type: "motion", label: "Motion", v: [s.kind, s.recipe].filter(Boolean).join(", ") };
54
- const src = s.kind === "demo" ? s.source : `composition/${s.composition ?? `compositions/${s.id}.html`}`;
64
+ const src = storyboard ? stillOf(s) : s.kind === "demo" ? s.source : `composition/${s.composition ?? `compositions/${s.id}.html`}`;
55
65
  return {
56
- id: s.id, name: words(s.id), t: range,
66
+ id: s.id, name: words(s.id), t: range, ...(storyboard && { still: stillOf(s) }),
57
67
  els: [{ id: s.id, label: s.line || words(s.id), t: range, box: null, src: `storyboard.json scenes[${i}] · ${src}`, props }],
58
68
  };
59
69
  });
@@ -64,7 +74,8 @@ export function buildReel(sb, runDir, prevVersion = 0, duration = 0, qaNotes = [
64
74
  title: sb.title,
65
75
  version: prevVersion + 1,
66
76
  path: runDir,
67
- src: "final.mp4",
77
+ src: storyboard ? ANIMATIC : "final.mp4",
78
+ ...(storyboard && { stage: "storyboard" }),
68
79
  w: sb.format.width, h: sb.format.height, fps: sb.format.fps,
69
80
  // The joined file can run a frame or two off the storyboard sum; trust the file when it was probed.
70
81
  duration: duration || +t.toFixed(3),
@@ -79,18 +90,32 @@ export function buildReel(sb, runDir, prevVersion = 0, duration = 0, qaNotes = [
79
90
  }
80
91
 
81
92
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
82
- const runDir = resolve(process.argv[2] ?? ".");
93
+ const args = process.argv.slice(2);
94
+ const storyboard = args.includes("--storyboard");
95
+ const runDir = resolve(args.find((a) => !a.startsWith("--")) ?? ".");
83
96
  const fail = (m) => { console.error(`error: ${m}`); process.exit(1); };
84
97
  if (!existsSync(join(runDir, "storyboard.json"))) fail(`no storyboard.json in ${runDir}`);
85
- if (!existsSync(join(runDir, "final.mp4"))) fail(`no final.mp4 in ${runDir}; run render-scenes.mjs first`);
86
98
  const sb = JSON.parse(readFileSync(join(runDir, "storyboard.json"), "utf8"));
99
+ const video = storyboard ? ANIMATIC : "final.mp4";
100
+ if (storyboard) {
101
+ const missing = sb.scenes.map(stillOf).filter((f) => !existsSync(join(runDir, f)));
102
+ if (missing.length) fail(`missing storyboard stills: ${missing.join(", ")}`);
103
+ const { width: W, height: H, fps } = sb.format;
104
+ // One still per scene, held for its duration, letterboxed to the master size, joined in order.
105
+ const inputs = sb.scenes.flatMap((s) => ["-loop", "1", "-t", String(s.duration), "-i", join(runDir, stillOf(s))]);
106
+ const fit = sb.scenes.map((_, i) => `[${i}:v]scale=${W}:${H}:force_original_aspect_ratio=decrease,pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=${fps},format=yuv420p[v${i}]`);
107
+ const filter = `${fit.join(";")};${sb.scenes.map((_, i) => `[v${i}]`).join("")}concat=n=${sb.scenes.length}:v=1:a=0[out]`;
108
+ const ff = spawnSync("ffmpeg", ["-v", "error", "-y", ...inputs, "-filter_complex", filter, "-map", "[out]", "-c:v", "libx264", "-tune", "stillimage", "-movflags", "+faststart", join(runDir, video)], { encoding: "utf8" });
109
+ if (ff.status !== 0) fail(`ffmpeg could not build ${video}: ${ff.stderr || ff.error?.message}`);
110
+ } else if (!existsSync(join(runDir, "final.mp4"))) fail(`no final.mp4 in ${runDir}; run render-scenes.mjs first, or pass --storyboard to review the plan`);
87
111
  const reelFile = join(runDir, "reel.json");
88
112
  const prev = existsSync(reelFile) ? JSON.parse(readFileSync(reelFile, "utf8")).version ?? 0 : 0;
89
- const probe = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", join(runDir, "final.mp4")], { encoding: "utf8" });
113
+ const probe = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", join(runDir, video)], { encoding: "utf8" });
90
114
  const notesFile = join(runDir, "qa", "review-notes.json");
91
115
  const qaNotes = existsSync(notesFile) ? JSON.parse(readFileSync(notesFile, "utf8")) : [];
92
- const reel = buildReel(sb, runDir, prev, +(Number(probe.stdout?.trim()) || 0).toFixed(3), qaNotes);
116
+ // The animatic is built from the storyboard, so its scene sum is exact; probe only the rendered final.mp4.
117
+ const reel = buildReel(sb, runDir, prev, storyboard ? 0 : +(Number(probe.stdout?.trim()) || 0).toFixed(3), qaNotes, storyboard);
93
118
  writeFileSync(reelFile, JSON.stringify(reel, null, 1) + "\n");
94
119
  const serve = join(dirname(fileURLToPath(import.meta.url)), "..", "editor", "serve.mjs");
95
- console.log(`wrote ${reelFile} v${reel.version} (${reel.scenes.length} scenes, ${reel.duration}s). Open it: node "${resolve(serve)}" "${runDir}"`);
120
+ console.log(`wrote ${reelFile} v${reel.version}${storyboard ? " storyboard" : ""} (${reel.scenes.length} scenes, ${reel.duration}s). Open it: node "${resolve(serve)}" "${runDir}"`);
96
121
  }
@@ -60,3 +60,18 @@ test("rerunning bumps the version so an open player reloads", () => {
60
60
  assert.equal(buildReel(sb, "/r").version, 1);
61
61
  assert.equal(buildReel(sb, "/r", 4).version, 5);
62
62
  });
63
+
64
+ test("storyboard mode plays the animatic and makes each line and ai prompt editable", () => {
65
+ const withAi = { ...sb, scenes: [...sb.scenes, { id: "city", kind: "ai", duration: 4, line: "The city wakes up", prompt: "Aerial dawn over a city", still: "storyboard/city-sketch.png" }] };
66
+ const reel = buildReel(withAi, "/r", 0, 0, [], true);
67
+ assert.equal(reel.src, "storyboard/animatic.mp4");
68
+ assert.equal(reel.stage, "storyboard");
69
+ const city = reel.scenes[3].els[0];
70
+ assert.deepEqual([city.props.line.v, city.props.prompt.v], ["The city wakes up", "Aerial dawn over a city"]);
71
+ assert.equal(city.src, "storyboard.json scenes[3] · storyboard/city-sketch.png");
72
+ assert.equal(reel.scenes[0].els[0].src, "storyboard.json scenes[0] · storyboard/hook.png");
73
+ assert.ok(reel.assets.some((a) => a.id === "storyboard/hook.png" && a.type === "image"));
74
+ const rendered = buildReel(sb, "/r");
75
+ assert.equal(rendered.stage, undefined);
76
+ assert.equal(rendered.scenes[0].els[0].props.line, undefined);
77
+ });
@@ -14,3 +14,9 @@ The per-act timing and the words-per-second figures were measured by Luckiest on
14
14
  2026-10-02 from the timestamped captions of 11 Grumo explainers on the GRUMO
15
15
  YouTube channel. Part boundaries were marked by hand to about ±2 seconds. They
16
16
  describe those videos, not a rule Grumo has published.
17
+
18
+ | Source | License | What this sub-skill uses | License file |
19
+ |---|---|---|---|
20
+ | **motionmaxxing** by Tejas Makwana (`Tejashmakwana/motionmaxxing`, FinalMotion, finalmotion.video), commit 8c8ec0f2 | Apache-2.0 | Speaking rate, word budgets per length, script cues, the mute test, the official-line rule, and text leading the voice. Rewritten in our own words; no code, runtime, media, studies or `library/` rows are copied | `LICENSE-motionmaxxing` |
21
+
22
+ motionmaxxing NOTICE: "motionmaxxing: looksmaxxing for motion graphics. Made by Tejas Makwana (x.com/makwanatejas170), github.com/Tejashmakwana/motionmaxxing. Licensed under the Apache License, Version 2.0." Changes: ideas and measured numbers restated for HyperFrames and GSAP runs; its GSAP runtime, scripts, showcase media and precedent library are not included.
@@ -1,5 +1,17 @@
1
1
  # Changelog — luckiest-video-studio-script
2
2
 
3
+ ## 1.3.0 — 2026-10-12
4
+
5
+ - Word budgets for short films (28 to 38 words per 15 s, 60 to 80 per 30 s) and slower sign-offs.
6
+ - Banned slogan shapes, the official line used word for word, write like the brand then cut half, `[cue]` marks for words that need their own picture, and a mute test.
7
+ - On-screen words land 0 to 2 frames before they are heard. Site text is treated as data.
8
+ - From Tejas Makwana's motionmaxxing (Apache-2.0), in our own words.
9
+
10
+ ## 1.2.0 — 2026-10-10
11
+ - Gathers from `brief.md` first. Inside a studio run the lines are approved in the
12
+ storyboard review instead of a separate chat approval, and review edits are
13
+ written back to `script.md`.
14
+
3
15
  ## 1.1.0 — 2026-10-05
4
16
  - Runs the director's intent, insight, and hook steps before writing when it is installed.
5
17