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
@@ -0,0 +1,75 @@
1
+ # Context interview
2
+
3
+ Every new run starts here, before Step 1. Five minutes of questions saves a
4
+ storyboard nobody wanted. The answers go to `<output-dir>/brief.md`, and the
5
+ director, the script, and the plan read that file instead of guessing.
6
+
7
+ ## 1. Infer first
8
+
9
+ Read the request, then skim the repo the way Step 1 does (README, landing page,
10
+ `package.json`, an existing `plan.md` or `brief.md`). Fill in every field below
11
+ that they already answer. A field counts as answered only when the request or the
12
+ repo states one value for it, not when you could guess it. When sources disagree
13
+ (the README and `package.json` describe different products) or name several
14
+ candidates (two audiences), the field is open: ask, with those candidates as the
15
+ options.
16
+
17
+ ## 2. Ask what is left
18
+
19
+ Use AskUserQuestion. At most 4 questions per call and at most 2 calls. Skip every
20
+ question the request or repo already answers. Start the first call with one
21
+ line naming what you inferred and from where, for example: "From your README: it
22
+ is for freelancers, and the message is 'get paid on time'. A few questions on the
23
+ rest."
24
+
25
+ Each question offers 2 to 4 options drawn from the repo (the user can always pick
26
+ "Other" and type). Put your best guess first and mark it "(Recommended)".
27
+
28
+ | Call | Field | Question | Example options |
29
+ |---|---|---|---|
30
+ | 1 | Audience | Who is this video for? | Two or three audiences from the README or site |
31
+ | 1 | Message | What is the one thing they should remember? | Two or three lines from the product's own copy |
32
+ | 1 | Where | Where will it be posted? | "X and LinkedIn, 16:9, 20s", "TikTok and Reels, 9:16, 30s", "YouTube or website, 16:9, 60-90s" |
33
+ | 1 | Type | What kind of video? | "Motion graphics (HyperFrames)", "AI-generated video", "A mix of both" |
34
+ | 2 | Why now | What should viewers do after watching? | "Sign up", "Join the waitlist", "Watch the launch", with the real URL |
35
+ | 2 | References | Any video or style you want it to feel like? | "No reference", "I'll paste a link" |
36
+ | 2 | Must show | Anything that has to be in it? | "Our logo and the dashboard", "A real customer number", "Nothing specific" |
37
+ | 2 | Voice | Voiceover or no voiceover? | "No voice, music and text only", "AI voice (Kokoro)", "I'll record it" |
38
+
39
+ If call 1 answers everything that matters (the user typed a full brief in
40
+ "Other"), skip call 2. Never ask a third time: anything still open is inferred
41
+ and marked as such.
42
+
43
+ "A mix of both" means some scenes are `ai` and the rest are motion, demo, or
44
+ logo scenes. AI scenes cost credits, so the cost check comes later, before any
45
+ generation (SKILL.md Step 3).
46
+
47
+ ## 3. Write brief.md
48
+
49
+ ```markdown
50
+ # Brief: <product>
51
+
52
+ - Audience: <who>
53
+ - Message: <one line, no "and">
54
+ - Where: <platforms>, <format>, <length>s
55
+ - Type: <motion graphics | AI-generated | mix>
56
+ - Call to action: <action>, <url>
57
+ - References: <links or none>
58
+ - Must show: <assets or none>
59
+ - Voice: <none | Kokoro | user recording>
60
+
61
+ Inferred, not asked: <field>: <value> (from <source>), ...
62
+ ```
63
+
64
+ Mark every value you did not hear from the user in the "Inferred" line (write
65
+ "none" when everything was asked), so the user can spot a wrong guess. A URL no
66
+ file states is written as `<url> [confirm]`. In chat, summarize the brief in four
67
+ lines or fewer and give its path, then go to Step 1.
68
+
69
+ ## With --yes
70
+
71
+ Ask nothing. Infer every field from the request and the repo, write `brief.md`
72
+ with every field listed under "Inferred", show it, and continue. Defaults when
73
+ nothing says otherwise: Where is "X and LinkedIn, 16:9" at the middle of the
74
+ mode's default length (20s for `launch`), Type is motion graphics, Voice is none,
75
+ References and Must show are none, and a missing URL is `[confirm]`.
@@ -1,6 +1,8 @@
1
1
  # Step 2: Write the video plan
2
2
 
3
- Write `<output-dir>/plan.md`. One focused page. This is the creative north star for the entire video.
3
+ Write `<output-dir>/plan.md`. One focused page. Start from `brief.md` (the
4
+ context interview): its audience, message, platform, length, call to action, and
5
+ type are settled. Do not re-ask or change them; flag a conflict to the user instead. This is the creative north star for the entire video.
4
6
 
5
7
  The plan should specify what the video must communicate and what project material must be used. It should not prescribe low-level Hyperframes implementation details. Hyperframes will decide the concrete composition structure, animation mechanics, and render workflow from the brief in Step 3.
6
8
 
@@ -80,11 +82,14 @@ approved (`step-4-deliver.md`, "Other formats (last)").
80
82
 
81
83
  ### Scene 1 — [name] — [duration]s
82
84
  [What's on screen. What text appears. What product material must be referenced.]
85
+ Belief: [the viewer ... ; one belief, no "and"; importance 1, 2 or 3]
86
+ Picture only: [what the picture shows with the text covered; "a card on a flat colour" is not an answer]
83
87
  Sequential/interaction: [yes — describe what appears one by one or what interaction is simulated, e.g. "3 stat cards arrive one by one" or "cursor clicks the match button"; or none]
84
88
  Audio intent: [what the sound should do emotionally]
85
89
  Audio-coupled idea: [typed text, beat-aligned reveal, counter ticks, card-by-card sequence, simulated tap/swipe/type — or none]
86
90
  Music: [mood, or "none" only if disabled / missing / intentionally silent]
87
91
  Transition mood: [clean / hard / dramatic / soft / chaotic] → Scene 2
92
+ Handoff: [what carries into Scene 2, or "cut on motion"; Scene 2 opens already moving]
88
93
 
89
94
  ### Scene 2 — [name] — [duration]s
90
95
  [...]
@@ -180,7 +185,7 @@ These are among the most effective moments in a video — they make the product
180
185
  If a scene has either of these, commit to it explicitly in the scene description:
181
186
  - Good: "3 horse profiles slide in one by one, each with a card sound"
182
187
  - Good: "Simulate a right-swipe on Thunder's profile card"
183
- - Good: "The hook line types out character by character with keyboard sounds"
188
+ - Good: "The query types into the product's own search field with keyboard sounds" (type only into a real input field, never a headline)
184
189
  - Avoid: leaving it vague and hoping Hyperframes adds rhythm on its own
185
190
 
186
191
  Hyperframes can implement both patterns well — but only if the plan specifies what's appearing, in what order, and that interaction is being simulated. The plan is the contract.
@@ -251,15 +256,42 @@ If SFX are enabled, note likely sound opportunities but leave exact filenames to
251
256
 
252
257
  ## Transition vocabulary
253
258
 
254
- Match transitions to tone:
259
+ Match transitions to tone. Never crossfade whole scenes: the `crossfade` gate fails
260
+ an opacity tween on a full-frame layer. Cut on motion instead: scene A speeds up
261
+ over its last 3 to 8 frames, scene B opens already moving. For a softer join, use
262
+ a blur-matched cut (A blurs out to about 18px, B starts at about 80px and resolves
263
+ in about 8 frames). Fades are for single objects, not scenes. Handoff recipes are
264
+ in `luckiest-video-studio-motion`.
255
265
 
256
266
  | Tone | Preferred transitions |
257
267
  |---|---|
258
- | `default` | Crossfade, clean wipe |
259
- | `polished` | Soft crossfade, slide |
268
+ | `default` | Cut on motion, clean wipe |
269
+ | `polished` | Blur-matched cut, slide |
260
270
  | `yc-parody` | Hard cut, minimal |
261
271
  | `chaotic` | Hard cut, flash, zoom |
262
- | `deadpan` | Slow crossfade, long hold |
263
- | `cinematic` | Dramatic wipe, crossfade |
272
+ | `deadpan` | Hard cut after a long hold |
273
+ | `cinematic` | Dramatic wipe, blur-matched cut |
264
274
  | `app-store` | Slide, smooth wipe |
265
275
 
276
+ ## Storyboard stills
277
+
278
+ Every scene gets one still, `storyboard/<id>.png` at the master size, before the
279
+ storyboard review. The still shows the hold frame: what the viewer sees when the
280
+ scene settles. Cheap and local only, never a generated image or clip.
281
+
282
+ | Kind | Still |
283
+ |---|---|
284
+ | `motion`, `ai` | A static HTML frame, `storyboard/frames/<id>.html`: the brand background, the scene's main text in the brand fonts, and the real assets it will show, placed where the hold frame puts them. For `ai`, add a one-line caption of what the shot shows. |
285
+ | `demo` | A frame grab of the footage at the hold point: `ffmpeg -ss <t> -i <file> -frames:v 1 storyboard/<id>.png`. No footage yet: an HTML frame naming what will be recorded. |
286
+ | `logo` | The logo file centered on the brand background, as an HTML frame. |
287
+
288
+ Capture each HTML frame with headless Chrome (no install):
289
+
290
+ ```bash
291
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --hide-scrollbars \
292
+ --window-size=<width>,<height> --screenshot=<abs-run-dir>/storyboard/<id>.png "file://<abs-run-dir>/storyboard/frames/<id>.html"
293
+ ```
294
+
295
+ Look at each still before the review. A scene whose `still` field names another
296
+ file uses that one instead. When a review edit changes a line or a layout,
297
+ redraw only that scene's still.
@@ -36,7 +36,9 @@ this file and re-renders only what it touched. Schema:
36
36
  | `loudness` | Integrated loudness of `final.mp4` in LUFS, default `-14` (the streaming norm), two-pass with a -1.2 dBTP ceiling. `false` skips it. |
37
37
  | `sfx` | Optional `{ "file": "audio/sfx.wav", "volume": 1 }` from `scripts/sfx-cues.mjs`, mixed under the joined video at 0 s. Cue times are global, so re-run the mixer after a scene's length changes. |
38
38
  | `scenes[].id` | Lowercase and hyphens. Stable: edits refer to scenes by id. |
39
- | `scenes[].kind` | `motion` (HyperFrames scene), `logo` (HyperFrames logo recipe), `demo` (footage). |
39
+ | `scenes[].kind` | `motion` (HyperFrames scene), `logo` (HyperFrames logo recipe), `demo` (footage), `ai` (AI-generated shot, storyboard stage only: it becomes `demo` once its clip is generated and approved). |
40
+ | `scenes[].still` | Optional. The scene's storyboard still, default `storyboard/<id>.png`. The storyboard review shows it. |
41
+ | `scenes[].prompt` | For `ai`, the generation prompt from `luckiest-video-studio-recreate`. Kept after the scene becomes `demo`, so the shot can be regenerated. |
40
42
  | `scenes[].composition` | For motion and logo, the sub-composition under `composition/`. Default `compositions/<id>.html`. |
41
43
  | `scenes[].source`, `sourceStart` | For demo, the footage file and where to start in it. |
42
44
  | `scenes[].focus` | For demo, `{ "x": 0.5, "y": 0.5 }` as fractions of the source frame: the point kept in view when the footage is cropped to a different shape (a 16:9 screen recording in a vertical cut). Set it on the cursor, the face, or the UI the line talks about. |
@@ -31,7 +31,7 @@ Horse Tinder.
31
31
  Find your perfect stablemate.
32
32
  ```
33
33
 
34
- **Transitions:** Crossfade or clean slide.
34
+ **Transitions:** Cut on motion or clean slide.
35
35
 
36
36
  **When to use:** Most absurd consumer apps. Projects that have personality without trying too hard.
37
37
 
@@ -57,7 +57,7 @@ upon completion.
57
57
 
58
58
  **Outro style:** Product name. Tagline. Silence.
59
59
 
60
- **Transitions:** Slow crossfade (0.6-0.8s).
60
+ **Transitions:** Blur-matched cut (out to 18px, in from 80px, about 8 frames).
61
61
 
62
62
  **When to use:** Projects that aren't jokes. Products that want to feel premium. Anytime the user says "clean" or "elegant."
63
63
 
@@ -92,7 +92,7 @@ The ride-hailing app for ride-hailing assets.
92
92
  taxifortaxis.com
93
93
  ```
94
94
 
95
- **Transitions:** Hard cut or minimal crossfade (0.2s).
95
+ **Transitions:** Hard cut, landing mid-move.
96
96
 
97
97
  **When to use:** Any recursive or absurd concept that benefits from being played straight. "Psychologists for Chatbots", "Taxi for Taxis", "Briefcase for Baby."
98
98
 
@@ -157,7 +157,7 @@ Now I fear birds, weather, and gravity.
157
157
  Fish Flight School.
158
158
  ```
159
159
 
160
- **Transitions:** Very slow crossfade (0.8-1.0s). Or long hold before the next scene.
160
+ **Transitions:** Long hold, then a hard cut. A held frame keeps one living thing (a slow drift or a caret).
161
161
 
162
162
  **When to use:** Projects that have a quote as the strongest thing on the site. "Psychologists for Chatbots" testimonial-forward approach. Anything where restraint makes the joke land harder.
163
163
 
@@ -192,7 +192,7 @@ FISH FLIGHT SCHOOL
192
192
  The sky was never the limit.
193
193
  ```
194
194
 
195
- **Transitions:** Dramatic wipe or slow crossfade with scale (scene enters at 0.95, reaches 1.0).
195
+ **Transitions:** Dramatic wipe, or a blur-matched cut with scale (scene enters at 0.95, reaches 1.0).
196
196
 
197
197
  **When to use:** Anything with natural epic quality. "Fish Flight School" maps perfectly. Anything involving scale, nature, or grand claims.
198
198
 
@@ -5,10 +5,12 @@ description = "Development tooling for the /brag skill."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
7
7
  "librosa>=0.10.2",
8
- "numpy>=1.26",
9
- "scipy>=1.11",
8
+ "numpy>=2.4.6",
9
+ "scipy>=1.17.1",
10
10
  "soundfile>=0.12",
11
11
  ]
12
12
 
13
13
  [tool.uv]
14
14
  package = false
15
+ # urllib3 comes in through librosa's download helpers; 2.8.0 fixes GHSA-8988-9cw3-xx77, GHSA-gh4c-6fx4-qh6g, GHSA-vxq7-64xx-v4gw.
16
+ constraint-dependencies = ["urllib3>=2.8.0"]
@@ -14,6 +14,9 @@
14
14
  // node qa.mjs face <run-dir> [--max 18] cards covering the speaker's face (scene faceRect)
15
15
  // node qa.mjs avsync <video> [--expect 1.5,4.2] [--duration 20] audio present, length right, hits within one frame
16
16
  // node qa.mjs loop <video> [--max 6] last frame must match the first, so the piece loops
17
+ // node qa.mjs endhold <video> [--max 1.4] [--share 0.25] end card held too long, final shot too long
18
+ // node qa.mjs blankframes <video> [--run 3] runs of empty swatch frames
19
+ // node qa.mjs hud <composition-dir> counters, timecodes, BPM/FPS, greetings, "label · $value", tracked caps
17
20
  // node qa.mjs facts <run-dir> on-screen numbers not in storyboard facts, em dashes on screen
18
21
  // node qa.mjs safe <run-dir> [--max 0.004] [--stills] text or logo edges in the margins each format's platforms cover
19
22
  // node qa.mjs judge <new.mp4> <old.mp4> [--out dir] [--cmd "claude -p --model sonnet"] [--verdicts A,B]
@@ -292,6 +295,80 @@ export function loopCheck(video, { max = 6 } = {}) {
292
295
  return { ok: diff <= max, diff: Number(diff.toFixed(2)), frames: frames.length };
293
296
  }
294
297
 
298
+ // ---------- end hold and final shot ----------
299
+
300
+ // The picture must not sit unchanged at the end for more than `max` s, and the last shot
301
+ // (last hard cut to the end) must be at most `share` of the film. A film with no cuts skips the share check.
302
+ export function endHold(video, { max = 1.4, share = 0.25, still = 0.35 } = {}) {
303
+ const { fps } = probe(video), frames = grayFrames(video, 96, 54);
304
+ let n = 0;
305
+ for (let i = frames.length - 1; i > 0 && meanDiff(frames[i], frames[i - 1]) < still; i--) n++;
306
+ const duration = frames.length / fps, hold = Number((n / fps).toFixed(2));
307
+ const cuts = cutTimes(grayFrames(video), fps), last = cuts.at(-1);
308
+ const finalShare = last == null ? null : Number(((duration - last) / duration).toFixed(3));
309
+ return { ok: hold <= max && (finalShare == null || finalShare <= share), hold, finalShare,
310
+ note: hold < 0.6 ? "ends mid-motion (fine if intended)" : undefined };
311
+ }
312
+
313
+ // ---------- flat frames ----------
314
+
315
+ // A flat frame is an empty swatch: luma std under 1.5 and range under 8 at 96x54. Fails on a run longer than `run` frames.
316
+ export function blankRuns(video, { run = 3, std = 1.5, range = 8 } = {}) {
317
+ const { fps } = probe(video), runs = [];
318
+ let start = -1;
319
+ const frames = grayFrames(video, 96, 54);
320
+ frames.forEach((f, i) => {
321
+ let lo = 255, hi = 0, sum = 0;
322
+ for (const v of f) { lo = Math.min(lo, v); hi = Math.max(hi, v); sum += v; }
323
+ const mean = sum / f.length;
324
+ let sq = 0; for (const v of f) sq += (v - mean) ** 2;
325
+ const flat = Math.sqrt(sq / f.length) < std && hi - lo < range;
326
+ if (flat && start < 0) start = i;
327
+ if ((!flat || i === frames.length - 1) && start >= 0) {
328
+ const end = flat ? i + 1 : i;
329
+ if (end - start > run) runs.push({ start: Number((start / fps).toFixed(2)), frames: end - start });
330
+ start = -1;
331
+ }
332
+ });
333
+ return { ok: runs.length === 0, runs };
334
+ }
335
+
336
+ // ---------- HUD and page chrome ----------
337
+
338
+ // Drop elements marked data-ui (real product UI) before scanning: a clock or a "1/3" inside the product is fine.
339
+ export function stripUi(html) {
340
+ let out = "", depth = 0, last = 0;
341
+ for (const m of html.matchAll(/<(\/?)([a-z][\w-]*)\b[^>]*?(\/?)>/gi)) {
342
+ const [tag, close, , selfClose] = m, opens = !close && !selfClose && !/^(br|img|input|meta|link|hr|source|path|circle|rect|line|polyline|polygon|use)$/i.test(m[2]);
343
+ if (depth === 0) out += html.slice(last, m.index);
344
+ if (depth > 0) { if (close) depth--; else if (opens) depth++; }
345
+ else if (!close && /\bdata-ui\b/.test(tag)) { if (opens) depth = 1; }
346
+ else out += tag;
347
+ last = m.index + tag.length;
348
+ }
349
+ return depth === 0 ? out + html.slice(last) : out;
350
+ }
351
+
352
+ const CHROME = [
353
+ ["counter", /\b0\d\s*(?:\/\s*(?:\d{1,2}|[A-Z]{2,})|[-\u2013]\s*[A-Z]{2,})\b|\b\d{1,2}\s*\/\s*[A-Z]{2,}\b/g],
354
+ ["timecode", /\b\d{1,2}:\d{2}(?::\d{2})?(?:[:.]\d{2})?\b/g],
355
+ ["metadata", /\b\d+(?:\.\d+)?\s?(?:BPM|FPS|fps)\b/g],
356
+ ["greeting", /\bGood (?:morning|afternoon|evening)\b/gi],
357
+ ["label-value", /[A-Za-z]+\s*[\u00b7\u2022]\s*[$\u20ac\u00a3]\s?\d/g],
358
+ ];
359
+
360
+ // Page chrome and HUD text that reads as a deck or an AI default. Text inside [data-ui] is exempt.
361
+ // ponytail: tracked caps is a warning only, the card packs use it for kickers and source lines; promote it if packs change.
362
+ export function scanHud(html) {
363
+ const src = stripUi(html), text = visibleText(src), found = [];
364
+ for (const [rule, re] of CHROME) for (const m of text.matchAll(re)) found.push({ rule, token: m[0].trim() });
365
+ for (const m of src.matchAll(/([^{}<>]+)\{([^{}]*)\}/g)) {
366
+ const body = m[2], ls = body.match(/letter-spacing\s*:\s*([\d.]+)(em|px)/);
367
+ if (/text-transform\s*:\s*uppercase/.test(body) && ls && (ls[2] === "em" ? +ls[1] >= 0.05 : +ls[1] >= 2)) found.push({ rule: "tracked-caps", warn: true, token: m[1].trim().split("\n").pop().trim() });
368
+ }
369
+ return found;
370
+ }
371
+
295
372
  // ---------- facts ----------
296
373
 
297
374
  const ENTITIES = { amp: "&", nbsp: " ", mdash: "\u2014", ndash: "\u2013", lt: "<", gt: ">", quot: '"', "#39": "'", "#8212": "\u2014" };
@@ -447,7 +524,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
447
524
  if (gate === "judge" && rest[0] && !rest[0].startsWith("--")) rest.unshift("--old", rest.shift());
448
525
  const opt = (n, d) => { const i = rest.indexOf(n); return i >= 0 ? rest[i + 1] : d; };
449
526
  const num = (n, d) => Number(opt(n, d));
450
- if (!gate || !target) { console.error("Usage: qa.mjs <tokens|crossfade|motion|legibility|pops|deadframes|presence|beatsync|beatgrid|face|avsync|loop|facts|safe|judge> <path> [options]"); process.exit(1); }
527
+ if (!gate || !target) { console.error("Usage: qa.mjs <tokens|crossfade|motion|legibility|pops|deadframes|presence|beatsync|beatgrid|face|avsync|loop|endhold|blankframes|hud|facts|safe|judge> <path> [options]"); process.exit(1); }
451
528
  let result;
452
529
  if (gate === "tokens") {
453
530
  const tokens = opt("--tokens") ? JSON.parse(readFileSync(opt("--tokens"), "utf8")) : DEFAULT_TOKENS;
@@ -482,6 +559,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
482
559
  const expect = (opt("--expect", "") || "").split(",").filter(Boolean).map(Number);
483
560
  result = avSync(target, { expect, duration: opt("--duration") != null ? num("--duration") : undefined });
484
561
  } else if (gate === "loop") result = loopCheck(target, { max: num("--max", 6) });
562
+ else if (gate === "endhold") result = endHold(target, { max: num("--max", 1.4), share: num("--share", 0.25) });
563
+ else if (gate === "blankframes") result = blankRuns(target, { run: num("--run", 3) });
564
+ else if (gate === "hud") {
565
+ const files = statSync(target).isDirectory() ? htmlFiles(target) : [target];
566
+ const found = files.flatMap((f) => scanHud(readFileSync(f, "utf8")).map((v) => ({ file: f, ...v })));
567
+ result = { ok: found.every((v) => v.warn), found };
568
+ }
485
569
  else if (gate === "safe") result = safeAreas(target, { max: num("--max", 0.004), stills: rest.includes("--stills") });
486
570
  else if (gate === "facts") {
487
571
  const sb = JSON.parse(readFileSync(join(target, "storyboard.json"), "utf8"));
@@ -10,7 +10,7 @@ import {
10
10
  scanTokens, scanCrossfades, scanMotion, legibility, findPops, deadFrames, presence,
11
11
  beatSync, findAnchor, cutTimes, beatGrid, grayFrames, probe,
12
12
  faceCover, pairVerdict, parseWinner, audioOnsets, avSync, visibleText, scanFacts, loopCheck,
13
- fullFrameSelectors, safeAreas,
13
+ fullFrameSelectors, safeAreas, endHold, blankRuns, scanHud,
14
14
  } from "./qa.mjs";
15
15
 
16
16
  const dir = mkdtempSync(join(tmpdir(), "qa-gates-"));
@@ -209,6 +209,42 @@ test("loop: last frame matching the first passes, a different end frame fails",
209
209
  assert.equal(cli("loop", ends).status, 1);
210
210
  });
211
211
 
212
+ test("endhold: a 2 s static end card fails, a short resolved hold and a one-shot film pass", () => {
213
+ const long = video("endhold-long.mp4", [["testsrc", 2], ["0x202020", 2]]);
214
+ const r = endHold(long);
215
+ assert.equal(r.ok, false);
216
+ assert.ok(r.hold >= 1.9 && r.finalShare > 0.25, JSON.stringify(r));
217
+ assert.equal(cli("endhold", long).status, 1);
218
+ const short = video("endhold-short.mp4", [["testsrc", 3.5], ["0x202020", 0.8]]);
219
+ assert.equal(endHold(short).ok, true, JSON.stringify(endHold(short)));
220
+ const oneShot = video("endhold-oneshot.mp4", [["testsrc", 3]]);
221
+ assert.deepEqual([endHold(oneShot).ok, endHold(oneShot).finalShare], [true, null]);
222
+ });
223
+
224
+ test("blankframes: half a second of empty swatch fails, a 3-frame dip passes", () => {
225
+ const empty = video("flat-empty.mp4", [["testsrc", 1], ["0x335577", 0.5], ["testsrc", 1]]);
226
+ const r = blankRuns(empty);
227
+ assert.equal(r.ok, false);
228
+ assert.equal(r.runs[0].frames, 15);
229
+ assert.equal(cli("blankframes", empty).status, 1);
230
+ const dip = video("flat-dip.mp4", [["testsrc", 1], ["0x335577", 0.1], ["testsrc", 1]]);
231
+ assert.equal(blankRuns(dip).ok, true, JSON.stringify(blankRuns(dip)));
232
+ });
233
+
234
+ test("hud: HUD counters, timecodes, BPM, greetings and label-value fail; product UI and plain copy pass", () => {
235
+ const bad = `<p>02 / SPEAK</p><p>01 - THE WAIT</p><p>00:14</p><p>120 BPM</p><p>Good morning, Sam</p><p>Revenue · $4,200</p>`;
236
+ assert.deepEqual(scanHud(bad).map((v) => v.rule), ["counter", "counter", "timecode", "metadata", "greeting", "label-value"]);
237
+ const good = `<h1>Rent the name before you buy it</h1><p>3 / 10 founders</p><div data-ui class="phone"><span>9:41</span><p>Good morning</p></div>`;
238
+ assert.deepEqual(scanHud(good), []);
239
+ const caps = scanHud(`<style>.k{text-transform:uppercase;letter-spacing:0.12em}</style><p class="k">source</p>`);
240
+ assert.deepEqual(caps.map((v) => [v.rule, v.warn]), [["tracked-caps", true]]);
241
+ const comp = join(dir, "chrome-comp"); mkdirSync(comp, { recursive: true });
242
+ writeFileSync(join(comp, "a.html"), bad);
243
+ assert.equal(cli("hud", comp).status, 1);
244
+ writeFileSync(join(comp, "a.html"), good);
245
+ assert.equal(cli("hud", comp).status, 0);
246
+ });
247
+
212
248
  test("crossfade: a full-frame layer is found at the composition's own size, not only 1920x1080", () => {
213
249
  const html = `<div data-composition-id="m" data-width="1080" data-height="1920"></div><style>.bg{width:1080px;height:1920px}.v{width:var(--w);height:var(--h)}.wide{width:1920px;height:1080px}</style>`;
214
250
  assert.deepEqual([...fullFrameSelectors(html)], [".bg", ".v"]);
@@ -34,6 +34,7 @@ const ids = new Set();
34
34
  for (const s of sb.scenes) {
35
35
  if (ids.has(s.id)) fail(`duplicate scene id "${s.id}"`);
36
36
  ids.add(s.id);
37
+ if (s.kind === "ai") fail(`scene ${s.id}: "ai" scene has no clip yet. Generate it, then set kind "demo" and source to the clip`);
37
38
  if (!["motion", "logo", "demo"].includes(s.kind)) fail(`scene ${s.id}: unknown kind "${s.kind}"`);
38
39
  if (!(s.duration > 0)) fail(`scene ${s.id}: duration must be > 0`);
39
40
  }
@@ -55,13 +55,18 @@
55
55
  "required": ["id", "kind", "duration"],
56
56
  "properties": {
57
57
  "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
58
- "kind": { "enum": ["motion", "logo", "demo"] },
58
+ "kind": { "enum": ["motion", "logo", "demo", "ai"], "description": "ai: AI-generated shot, storyboard stage only; set to demo with source once its clip exists" },
59
59
  "duration": { "type": "number", "exclusiveMinimum": 0 },
60
60
  "line": { "type": "string", "description": "What this scene says or shows, one line, used in TIMING.md" },
61
+ "belief": { "type": "string", "description": "The belief this scene creates: 'the viewer ...'. One change in the viewer, no 'and'. Written by the director" },
62
+ "importance": { "enum": [1, 2, 3], "description": "1 support, 2 build, 3 the point. At most a quarter of scenes are 3" },
63
+ "pictureOnly": { "type": "string", "description": "What the picture shows with the text covered. 'A flat colour', 'a card on a flat colour' and 'a block of text' are rejected answers" },
61
64
  "composition": { "type": "string", "description": "motion and logo: sub-composition path relative to composition/, default compositions/<id>.html" },
62
65
  "recipe": { "type": "string", "description": "logo: recipe name from references/logo-recipes.md" },
63
66
  "source": { "type": "string", "description": "demo: footage path relative to the run folder" },
64
67
  "sourceStart": { "type": "number", "minimum": 0, "default": 0 },
68
+ "still": { "type": "string", "description": "Storyboard hold-frame still relative to the run folder, default storyboard/<id>.png" },
69
+ "prompt": { "type": "string", "description": "ai: the generation prompt, kept after the scene becomes demo" },
65
70
  "hold": { "type": "number", "minimum": 0, "description": "Seconds into the scene where render-scenes --stills captures the hold frame. Default 0.5s before the cut." },
66
71
  "focus": { "type": "object", "required": ["x", "y"], "properties": { "x": { "type": "number", "minimum": 0, "maximum": 1 }, "y": { "type": "number", "minimum": 0, "maximum": 1 } }, "description": "demo: crop center as fractions of the source frame when its shape differs from the output, default { x: 0.5, y: 0.5 }" },
67
72
  "faceRect": { "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 1 }, "minItems": 4, "maxItems": 4, "description": "demo: speaker's face as [x, y, w, h] fractions of the frame, checked by the face gate" },
@@ -6,6 +6,9 @@ resolution-markers = [
6
6
  "python_full_version < '3.13'",
7
7
  ]
8
8
 
9
+ [manifest]
10
+ constraints = [{ name = "urllib3", specifier = ">=2.8.0" }]
11
+
9
12
  [[package]]
10
13
  name = "audioop-lts"
11
14
  version = "0.2.2"
@@ -89,8 +92,8 @@ dependencies = [
89
92
  [package.metadata]
90
93
  requires-dist = [
91
94
  { name = "librosa", specifier = ">=0.10.2" },
92
- { name = "numpy", specifier = ">=1.26" },
93
- { name = "scipy", specifier = ">=1.11" },
95
+ { name = "numpy", specifier = ">=2.4.6" },
96
+ { name = "scipy", specifier = ">=1.17.1" },
94
97
  { name = "soundfile", specifier = ">=0.12" },
95
98
  ]
96
99
 
@@ -811,9 +814,9 @@ wheels = [
811
814
 
812
815
  [[package]]
813
816
  name = "urllib3"
814
- version = "2.7.0"
817
+ version = "2.8.0"
815
818
  source = { registry = "https://pypi.org/simple" }
816
- sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
819
+ sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
817
820
  wheels = [
818
- { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
821
+ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
819
822
  ]
@@ -9,6 +9,7 @@
9
9
  | **"AI finally designs like me"** by The Design Guy (YouTube, 2026-09-27, https://www.youtube.com/watch?v=E3Lgtf4xjeM) | Video, all rights reserved | Ideas only: DESIGN.md as a file of decisions already made, rules as choice plus reason plus example, locked/guided/open freedom levels, mining repeated review corrections for rules, pointing to existing assets instead of describing them, and checking that the agent read the file. No text or footage is copied | n/a |
10
10
  | **prompt-images** by Replicate (`replicate/skills`, https://github.com/replicate/skills/tree/main/skills/prompt-images) | Apache-2.0, Copyright 2025 Replicate, Inc. | Adapted into `references/shot-prompting.md`: natural-language shot descriptions, naming subjects, camera, film, lighting and composition vocabulary, quoted text, editing and consistency rules, guidance and negative-prompt pitfalls. Rewritten and reordered, not copied verbatim | `LICENSE-replicate` |
11
11
  | **"How to make AI images look less like AI"** by Pixova (https://www.pixova.io/blog/how-to-make-ai-images-look-less-like-ai) | Article, all rights reserved | Ideas only, in section 3b: directional light with hard shadows, film stock and grain, skin texture, off-center candid framing, specific environments, banned prompt words, lower CFG. No text is copied | n/a |
12
+ | **motionmaxxing** by Tejas Makwana (`Tejashmakwana/motionmaxxing`, FinalMotion, finalmotion.video), commit 8c8ec0f2 | Apache-2.0 | World per act, one system per film, the hero frame, and the additions to the Banned list. Rewritten in our own words; no code, runtime, media, studies or `library/` rows are copied | `LICENSE-motionmaxxing` |
12
13
 
13
14
  ## Changes in this edition
14
15
 
@@ -23,3 +24,5 @@
23
24
  - Added `art-direction.json` as a file the studio and other sub-skills read.
24
25
  - Mapped studio tone presets to tone cells.
25
26
  - Added style-frame approval at studio Step 2b and `--review` with Approve or Block.
27
+
28
+ 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,12 @@
1
1
  # Changelog — luckiest-video-studio-art-director
2
2
 
3
+ ## 1.6.0 — 2026-10-12
4
+
5
+ - World per act (ground, light, depth, texture, sound) and a named hero frame in the look lock; accent colour carries one meaning.
6
+ - Never-ship list: page chrome and HUD, deck layouts, generic web-component UI, the name's first association, invented proof, house-style defaults, cards on empty grounds. Review gains a never-ship column.
7
+ - One clock and one blur grammar per film in the motion language and `art-direction.json`.
8
+ - From Tejas Makwana's motionmaxxing (Apache-2.0), in our own words.
9
+
3
10
  ## 1.5.0 — 2026-10-08
4
11
  - "Do not repeat earlier runs": reads earlier `art-direction.json` files in the
5
12
  workspace and bans their signature transition, wow device, texture, and concept,