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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/ad-creative/CHANGELOG.md +7 -0
- package/skills/ad-creative/SKILL.md +23 -4
- package/skills/ad-creative/references/generative-tools.md +6 -30
- package/skills/ads/CHANGELOG.md +9 -0
- package/skills/ads/SKILL.md +19 -9
- package/skills/ads/references/reading-google-ads-data.md +121 -0
- package/skills/ai-seo/CHANGELOG.md +11 -0
- package/skills/ai-seo/SKILL.md +21 -22
- package/skills/ai-seo/references/content-types.md +11 -0
- package/skills/ai-seo/references/linkedin-ai-citations.md +122 -0
- package/skills/ai-seo/references/platform-ranking-factors.md +32 -19
- package/skills/ai-seo/references/positioning-and-consensus.md +102 -0
- package/skills/coder/CHANGELOG.md +23 -0
- package/skills/coder/SKILL.md +16 -4
- package/skills/coder-brainstorming/CHANGELOG.md +9 -0
- package/skills/coder-brainstorming/SKILL.md +5 -3
- package/skills/coder-debugging/CHANGELOG.md +11 -0
- package/skills/coder-debugging/SKILL.md +15 -3
- package/skills/coder-executing-plans/CHANGELOG.md +9 -0
- package/skills/coder-executing-plans/SKILL.md +5 -3
- package/skills/coder-finishing-a-branch/CHANGELOG.md +9 -0
- package/skills/coder-finishing-a-branch/SKILL.md +7 -3
- package/skills/coder-guard/CHANGELOG.md +7 -0
- package/skills/coder-guard/SKILL.md +6 -6
- package/skills/coder-receiving-code-review/CHANGELOG.md +9 -0
- package/skills/coder-receiving-code-review/SKILL.md +5 -3
- package/skills/coder-shipping/CHANGELOG.md +9 -0
- package/skills/coder-shipping/SKILL.md +4 -4
- package/skills/coder-subagent-driven-development/CHANGELOG.md +9 -0
- package/skills/coder-subagent-driven-development/SKILL.md +5 -3
- package/skills/coder-writing-plans/CHANGELOG.md +9 -0
- package/skills/coder-writing-plans/SKILL.md +5 -3
- package/skills/cold-email/CHANGELOG.md +7 -0
- package/skills/cold-email/SKILL.md +15 -8
- package/skills/cold-email/references/follow-up-sequences.md +4 -4
- package/skills/cold-email/references/frameworks.md +5 -5
- package/skills/cold-email/references/personalization.md +10 -10
- package/skills/competitor-profiling/CHANGELOG.md +8 -0
- package/skills/competitor-profiling/SKILL.md +34 -9
- package/skills/competitor-profiling/references/tool-reference.md +141 -5
- package/skills/competitors/CHANGELOG.md +8 -0
- package/skills/competitors/SKILL.md +32 -5
- package/skills/competitors/references/content-architecture.md +6 -0
- package/skills/competitors/references/templates.md +3 -0
- package/skills/copy-editing/CHANGELOG.md +10 -0
- package/skills/copy-editing/SKILL.md +41 -9
- package/skills/copy-editing/references/checklist.md +11 -0
- package/skills/copywriting/CHANGELOG.md +11 -0
- package/skills/copywriting/SKILL.md +32 -4
- package/skills/copywriting/references/copy-frameworks.md +2 -3
- package/skills/copywriting/references/natural-transitions.md +4 -20
- package/skills/customer-research/CHANGELOG.md +8 -0
- package/skills/customer-research/SKILL.md +6 -3
- package/skills/directory-submissions/CHANGELOG.md +9 -0
- package/skills/directory-submissions/SKILL.md +21 -10
- package/skills/emails/CHANGELOG.md +8 -0
- package/skills/emails/SKILL.md +22 -3
- package/skills/emails/references/sequence-templates.md +3 -3
- package/skills/image/CHANGELOG.md +8 -0
- package/skills/image/SKILL.md +4 -4
- package/skills/image/references/ai-image-prompting.md +1 -1
- package/skills/marketing-plan/CHANGELOG.md +8 -0
- package/skills/marketing-plan/SKILL.md +8 -4
- package/skills/marketing-plan/references/market-sizing.md +166 -0
- package/skills/marketing-plan/references/methodology.md +2 -0
- package/skills/model-router/CHANGELOG.md +9 -0
- package/skills/model-router/SKILL.md +3 -3
- package/skills/model-router/references/weave-routing-notes.md +1 -0
- package/skills/model-router/scripts/route.py +3 -1
- package/skills/prospecting/CHANGELOG.md +7 -0
- package/skills/prospecting/SKILL.md +3 -3
- package/skills/prospecting/references/saas-prospecting.md +1 -1
- package/skills/public-relations/CHANGELOG.md +10 -0
- package/skills/public-relations/SKILL.md +16 -4
- package/skills/public-relations/references/crisis-communications.md +111 -0
- package/skills/public-relations/references/newsjacking.md +2 -0
- package/skills/public-relations/references/statement-templates.md +143 -0
- package/skills/sales-enablement/CHANGELOG.md +8 -0
- package/skills/sales-enablement/SKILL.md +72 -4
- package/skills/sales-enablement/references/win-loss-analysis.md +72 -0
- package/skills/skill-audit/ATTRIBUTION.md +5 -0
- package/skills/skill-audit/CHANGELOG.md +19 -0
- package/skills/skill-audit/SKILL.md +121 -0
- package/skills/skill-audit-security/ATTRIBUTION.md +14 -0
- package/skills/skill-audit-security/CHANGELOG.md +14 -0
- package/skills/skill-audit-security/LICENSE-APACHE-2.0 +190 -0
- package/skills/skill-audit-security/SKILL.md +99 -0
- package/skills/skill-audit-security/references/rule-ids.md +28 -0
- package/skills/skill-audit-security/references/source-review.md +58 -0
- package/skills/sms/CHANGELOG.md +8 -0
- package/skills/sms/SKILL.md +18 -4
- package/skills/sms/references/whatsapp.md +258 -0
- package/skills/social/CHANGELOG.md +11 -0
- package/skills/social/SKILL.md +31 -7
- package/skills/social/references/listening.md +2 -0
- package/skills/social/references/platforms.md +15 -9
- package/skills/social/references/post-templates.md +9 -9
- package/skills/social/references/short-form-video.md +5 -5
- package/skills/social/references/x-algorithm.md +69 -0
- package/skills/trends/CHANGELOG.md +3 -0
- package/skills/trends/SKILL.md +14 -4
- package/skills/video/CHANGELOG.md +8 -0
- package/skills/video/SKILL.md +11 -25
- package/skills/video-studio/ATTRIBUTION.md +3 -0
- package/skills/video-studio/CHANGELOG.md +25 -0
- package/skills/video-studio/LICENSE-motionmaxxing +202 -0
- package/skills/video-studio/SKILL.md +115 -25
- package/skills/video-studio/references/audio.md +15 -0
- package/skills/video-studio/references/interview.md +75 -0
- package/skills/video-studio/references/step-2-plan.md +39 -7
- package/skills/video-studio/references/storyboard.md +3 -1
- package/skills/video-studio/references/tones.md +5 -5
- package/skills/video-studio/scripts/pyproject.toml +4 -2
- package/skills/video-studio/scripts/qa.mjs +85 -1
- package/skills/video-studio/scripts/qa.test.mjs +37 -1
- package/skills/video-studio/scripts/render-scenes.mjs +1 -0
- package/skills/video-studio/scripts/storyboard.schema.json +6 -1
- package/skills/video-studio/scripts/uv.lock +8 -5
- package/skills/video-studio-art-director/ATTRIBUTION.md +3 -0
- package/skills/video-studio-art-director/CHANGELOG.md +7 -0
- package/skills/video-studio-art-director/LICENSE-motionmaxxing +202 -0
- package/skills/video-studio-art-director/SKILL.md +42 -7
- package/skills/video-studio-composition/ATTRIBUTION.md +3 -0
- package/skills/video-studio-composition/CHANGELOG.md +7 -0
- package/skills/video-studio-composition/LICENSE-motionmaxxing +202 -0
- package/skills/video-studio-composition/SKILL.md +17 -4
- package/skills/video-studio-director/ATTRIBUTION.md +3 -0
- package/skills/video-studio-director/CHANGELOG.md +15 -0
- package/skills/video-studio-director/LICENSE-motionmaxxing +202 -0
- package/skills/video-studio-director/SKILL.md +105 -10
- package/skills/video-studio-motion/ATTRIBUTION.md +3 -0
- package/skills/video-studio-motion/CHANGELOG.md +8 -0
- package/skills/video-studio-motion/LICENSE-motionmaxxing +202 -0
- package/skills/video-studio-motion/SKILL.md +98 -5
- package/skills/video-studio-motion/references/kinetic-type.md +89 -0
- package/skills/video-studio-motion/references/ui-demo.md +59 -0
- package/skills/video-studio-review/CHANGELOG.md +11 -0
- package/skills/video-studio-review/SKILL.md +31 -5
- package/skills/video-studio-review/editor/index.html +21 -3
- package/skills/video-studio-review/references/apply-feedback.md +19 -0
- package/skills/video-studio-review/scripts/build-reel.mjs +35 -10
- package/skills/video-studio-review/scripts/build-reel.test.mjs +15 -0
- package/skills/video-studio-script/ATTRIBUTION.md +6 -0
- package/skills/video-studio-script/CHANGELOG.md +12 -0
- package/skills/video-studio-script/LICENSE-motionmaxxing +202 -0
- package/skills/video-studio-script/SKILL.md +41 -10
- package/skills/video-studio-showreel/CHANGELOG.md +21 -0
- package/skills/video-studio-showreel/SKILL.md +231 -0
- package/skills/video-studio-showreel/references/motion-design-pack-notes.md +28 -0
- package/skills/video-studio-showreel/references/zingage-breakdown.md +47 -0
- package/skills/video-studio-showreel/scripts/build.py +10 -0
- package/skills/video-studio-showreel/templates/showreel.html +264 -0
- package/skills/video-watcher/CHANGELOG.md +6 -0
- package/skills/video-watcher/SKILL.md +3 -3
- package/skills/video-watcher/scripts/setup.py +9 -7
- package/skills/trends/agents/openai.yaml +0 -8
- package/skills/trends/scripts/build-skill.sh +0 -39
- package/skills/trends/scripts/compare.sh +0 -61
- package/skills/trends/scripts/evaluate_search_quality.py +0 -573
- package/skills/trends/scripts/test-v1-vs-v2.sh +0 -220
- package/skills/trends/scripts/test_device_auth.py +0 -128
- 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.
|
|
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.
|
|
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.
|
|
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`.
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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',
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|