luckiest-co 1.0.40 → 1.0.42

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 (84) 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/video-studio/ATTRIBUTION.md +5 -0
  5. package/skills/video-studio/CHANGELOG.md +18 -0
  6. package/skills/video-studio/LICENSE-huashu-art-motion +21 -0
  7. package/skills/video-studio/LICENSE-mg-styles-15 +21 -0
  8. package/skills/video-studio/LICENSE-motionmaxxing +202 -0
  9. package/skills/video-studio/SKILL.md +39 -9
  10. package/skills/video-studio/references/audio.md +15 -0
  11. package/skills/video-studio/references/interview.md +10 -2
  12. package/skills/video-studio/references/step-2-plan.md +14 -6
  13. package/skills/video-studio/references/step-2b-storyboard.md +6 -0
  14. package/skills/video-studio/references/step-3-compose.md +13 -0
  15. package/skills/video-studio/references/step-4-deliver.md +5 -0
  16. package/skills/video-studio/references/storyboard.md +1 -0
  17. package/skills/video-studio/references/styles/3d-render.md +39 -0
  18. package/skills/video-studio/references/styles/anime-light.md +55 -0
  19. package/skills/video-studio/references/styles/aurora-glass.md +40 -0
  20. package/skills/video-studio/references/styles/bauhaus.md +46 -0
  21. package/skills/video-studio/references/styles/cel-boil.md +41 -0
  22. package/skills/video-studio/references/styles/chiaroscuro.md +53 -0
  23. package/skills/video-studio/references/styles/collage.md +46 -0
  24. package/skills/video-studio/references/styles/comic-pop.md +49 -0
  25. package/skills/video-studio/references/styles/constructivist.md +54 -0
  26. package/skills/video-studio/references/styles/cubist.md +49 -0
  27. package/skills/video-studio/references/styles/cut-paper.md +57 -0
  28. package/skills/video-studio/references/styles/era-styles.md +29 -0
  29. package/skills/video-studio/references/styles/finance-chart.md +60 -0
  30. package/skills/video-studio/references/styles/flat-vector.md +41 -0
  31. package/skills/video-studio/references/styles/grain-flat-illustration.md +48 -0
  32. package/skills/video-studio/references/styles/hud.md +41 -0
  33. package/skills/video-studio/references/styles/impressionist.md +48 -0
  34. package/skills/video-studio/references/styles/ink-wash.md +49 -0
  35. package/skills/video-studio/references/styles/isometric.md +40 -0
  36. package/skills/video-studio/references/styles/keynote-ui.md +59 -0
  37. package/skills/video-studio/references/styles/kinetic-type.md +58 -0
  38. package/skills/video-studio/references/styles/line-art.md +41 -0
  39. package/skills/video-studio/references/styles/liquid.md +40 -0
  40. package/skills/video-studio/references/styles/math-explainer.md +58 -0
  41. package/skills/video-studio/references/styles/morph.md +39 -0
  42. package/skills/video-studio/references/styles/mosaic.md +52 -0
  43. package/skills/video-studio/references/styles/newsroom-collage.md +60 -0
  44. package/skills/video-studio/references/styles/pixel.md +46 -0
  45. package/skills/video-studio/references/styles/pointillist.md +49 -0
  46. package/skills/video-studio/references/styles/presenter-explainer.md +57 -0
  47. package/skills/video-studio/references/styles/retro-cgi.md +52 -0
  48. package/skills/video-studio/references/styles/science-explainer.md +59 -0
  49. package/skills/video-studio/references/styles/sticker-explainer.md +40 -0
  50. package/skills/video-studio/references/styles/storytime.md +60 -0
  51. package/skills/video-studio/references/styles/swirl-brushwork.md +53 -0
  52. package/skills/video-studio/references/styles/synthwave.md +47 -0
  53. package/skills/video-studio/references/styles/variety-show.md +40 -0
  54. package/skills/video-studio/references/styles/watercolor-anime.md +50 -0
  55. package/skills/video-studio/references/styles/whiteboard.md +60 -0
  56. package/skills/video-studio/references/styles.md +117 -0
  57. package/skills/video-studio/references/tones.md +5 -5
  58. package/skills/video-studio/scripts/qa.mjs +125 -1
  59. package/skills/video-studio/scripts/qa.test.mjs +59 -1
  60. package/skills/video-studio/scripts/storyboard.schema.json +5 -0
  61. package/skills/video-studio-art-director/ATTRIBUTION.md +3 -0
  62. package/skills/video-studio-art-director/CHANGELOG.md +14 -0
  63. package/skills/video-studio-art-director/LICENSE-motionmaxxing +202 -0
  64. package/skills/video-studio-art-director/SKILL.md +105 -9
  65. package/skills/video-studio-art-director/references/palettes.md +22 -0
  66. package/skills/video-studio-composition/ATTRIBUTION.md +3 -0
  67. package/skills/video-studio-composition/CHANGELOG.md +7 -0
  68. package/skills/video-studio-composition/LICENSE-motionmaxxing +202 -0
  69. package/skills/video-studio-composition/SKILL.md +17 -4
  70. package/skills/video-studio-director/ATTRIBUTION.md +3 -0
  71. package/skills/video-studio-director/CHANGELOG.md +14 -0
  72. package/skills/video-studio-director/LICENSE-motionmaxxing +202 -0
  73. package/skills/video-studio-director/SKILL.md +107 -9
  74. package/skills/video-studio-director/references/jury.md +63 -0
  75. package/skills/video-studio-motion/ATTRIBUTION.md +3 -0
  76. package/skills/video-studio-motion/CHANGELOG.md +12 -0
  77. package/skills/video-studio-motion/LICENSE-motionmaxxing +202 -0
  78. package/skills/video-studio-motion/SKILL.md +128 -6
  79. package/skills/video-studio-motion/references/kinetic-type.md +89 -0
  80. package/skills/video-studio-motion/references/ui-demo.md +59 -0
  81. package/skills/video-studio-script/ATTRIBUTION.md +6 -0
  82. package/skills/video-studio-script/CHANGELOG.md +7 -0
  83. package/skills/video-studio-script/LICENSE-motionmaxxing +202 -0
  84. package/skills/video-studio-script/SKILL.md +28 -6
@@ -0,0 +1,47 @@
1
+ # Synthwave VHS Retro
2
+
3
+ - id: `synthwave`
4
+ - Source: mg-styles-15 `prompts/10-synthwave.md` (MIT, Vincentwei1021)
5
+ - Best for: retro title sequences, music videos, trend brand films, gaming and nostalgia content, Y2K chrome revivals
6
+ - Not for: clean corporate, calm editorial, medical or finance
7
+ - Triggers: synthwave, outrun, 80s, retro, neon, VHS, vaporwave, Y2K, chrome text, scanlines, grid horizon, retrowave
8
+ - Tone cell: kinetic-sharp
9
+ - Clock: smooth 30 fps, with deliberate 15 fps stutters in key moments
10
+
11
+ ## Look
12
+ Starry deep purple sky gradient. Striped retro sun with horizontal band cut-outs, magenta to orange to yellow. Wireframe mountains, palm silhouettes. Neon magenta and cyan perspective grid. Chrome title with a multi-stop metallic gradient and bevel highlight. Pink neon script word. Double glow: tight about 4px and wide about 30px. VHS pass: RGB split (red and blue offset 1-2px), scanlines (2px spacing, 10% black), 4:3 rounded mask optional.
13
+
14
+ ## Motion
15
+ Camera flies low over the grid, grid scrolls toward the camera in a looping vertical offset, sun rises. Chrome title SLAMS in with a lens flare and a star glint sweeping across. Neon script writes on like a tube with flicker. Glitch burst 1-2 times a second (random but seeded), occasional 2-frame rewind, stutter to 15 fps for tape feel. Y2K variant motion: pop-up windows, cursor click, window drag.
16
+
17
+ ## Signature features (judged against these)
18
+ - Perspective grid rushing to the horizon with a striped sun.
19
+ - Double-layer neon glow (tight plus wide).
20
+ - Chrome text with bevel and glint sweep.
21
+ - VHS chain: RGB split, scanlines, wavy jitter, glitch 1-2 times per second.
22
+ - Opening with a tracking-noise plus PLAY OSD.
23
+ - Neon script writing on with flicker.
24
+ - Deliberate frame stutter in key sections.
25
+
26
+ ## 10-second beat seed
27
+ - 0-1s: tracking noise, PLAY OSD.
28
+ - 1-4s: low flight over the grid, sun rises.
29
+ - 4-6s (hero at 60%): chrome title slams in, flare, glint.
30
+ - 6-7.5s: neon script writes on, flicker.
31
+ - 7.5-10s: sustained ride with chroma bleed, end on the hero frame.
32
+
33
+ ## Sound
34
+ Synthwave: gated-reverb snare, arpeggiated saw bass, lush pads, big chord and crash on the title slam.
35
+
36
+ ## Build it in HyperFrames
37
+ WebGL fragment shader for sky, sun bands, grid (grid lines from fract of (z + t*speed) in perspective space) and mountain wireframes. Chrome text on a 2D canvas with a gradient plus bevel then uploaded as a texture. Glow via two blur passes. VHS pass as a post shader: channel offset, scanline multiply, per-second seeded glitch rows.
38
+
39
+ ## Watch out
40
+ Banding in the sky gradient: add noise. Too much glitch kills legibility of the title. Chrome without strong contrast steps looks like plain grey. Keep glitch timing seeded.
41
+
42
+ ## Vaporwave variant (huashu-art-motion `26_vaporwave`)
43
+ - Wall gradient `#0d0322` to `#4a0f6a`; magenta `#ff4fd8`, hot pink `#ff2f8f`, cyan `#3ff6ff`, sun yellow `#ffd84a`, orange `#ff8a3a`.
44
+ - Neon tube: stroke with a wide glow, then a 2-3px near-white core. Build sign lettering from polylines, no font. Flicker = seeded hash of floor(30t) under 0.07; one letter drops out with a dim purple off state.
45
+ - Sunset sliced by 7 horizontal slits (width `2 + q^2 * 16`) rising 0.9/s; floor grid lines at `y = 600 + 520/d`, `d = k + 1 - phase`, `phase = 1.3t mod 1`.
46
+ - VHS post pass: RGB split with a 3 + 1.5 sin(7t) px offset, a 70px tracking-noise band, scanlines, an on-screen "PLAY" with a drawn triangle. Transition in: VHS tear with vertical roll, 0.30s.
47
+ - Watch out: glow shows at joints if you outline each part; stroke the silhouette instead. Draw kana and symbols as paths, they fall back silently.
@@ -0,0 +1,40 @@
1
+ # Variety Show Kinetic Captions
2
+
3
+ - id: `variety-show`
4
+ - Source: mg-styles-15 `prompts/18-hanazi.md` (MIT, Vincentwei1021)
5
+ - Best for: 9:16 vlogs, pet and lifestyle shorts, interview and reaction edits, comedic re-edits, caption-heavy social video
6
+ - Not for: premium calm brand films, data stories, serious tone
7
+ - Triggers: variety show, hanazi, Chinese variety captions, kinetic captions, sticker text, fancy text, reaction text, vlog captions, burst box, outlined text, TikTok captions, Douyin, bilibili
8
+ - Tone cell: kinetic-soft
9
+ - Clock: smooth 30 fps (vertical 1080x1920)
10
+
11
+ ## Look
12
+ Vertical 9:16 over a footage-feel base (CC0 or public-domain photos with Ken Burns, subtle handheld shake, snap punch-in zooms on key moments). Captions in a rounded heavy font, three layers: fill color (gradient allowed), thick white stroke, colored outer stroke or drop shadow. High-saturation pink, yellow, cyan with white strokes. Hand-drawn decoration symbols: burst boxes with speed lines and radial flash, sweat drops, sparkles, question marks, stamps. Keep captions in the safe zone away from UI overlays.
13
+
14
+ ## Motion
15
+ Pop scale 0 to 1.15 to 1.0 in about 8 frames. Per-character bounce with 2-frame stagger. Slight random rotation. Gentle wiggle while the caption holds. Captions appear 2-3 frames after the audio or visual moment. Emotion matching: complaint in slanted handwriting, surprise in a radial burst box, awkward with sweat drops. Punch-in zoom and handheld shake on the base layer for the "moment" feel.
16
+
17
+ ## Signature features (judged against these)
18
+ - Three-layer outlined text: fill, thick white stroke, colored outer stroke or shadow.
19
+ - Pop overshoot 0 to 1.15 to 1.0 in about 8 frames.
20
+ - Per-character bounce with 2-frame stagger.
21
+ - Caption timed 2-3 frames after the sound hit.
22
+ - Hand-drawn decorations (burst box, sweat, sparkle, stamp).
23
+ - Wiggle on long holds.
24
+ - Base layer has Ken Burns, shake and punch-in zooms.
25
+
26
+ ## 10-second beat seed
27
+ - 0-1s: base image punch-in plus the title caption pops.
28
+ - 1-4s: question marks bounce, aside caption in small type.
29
+ - 4-7s (hero at 60-75%): surprise caption inside a radial burst box with speed lines and flash.
30
+ - 7-9s: sparkle caption plus a stamp.
31
+ - 9-10s: final title held with wiggle.
32
+
33
+ ## Sound
34
+ Bouncy variety bed (pizzicato, bass, claps) with boing, pop, ding, slide whistle, and a record scratch on the question marks.
35
+
36
+ ## Build it in HyperFrames
37
+ DOM or SVG text components. Multi-layer stroke via stacked copies with SVG stroke and paint-order: stroke fill, or multiple text-shadow rings. Spring pop with a closed-form damped oscillator of (t minus start). Per-character spans with index times 2 frames of delay. Burst box and decorations are SVG paths. Base layer shake from seeded noise of t.
38
+
39
+ ## Watch out
40
+ Fonts: use OFL rounded heavy fonts with full CJK coverage when needed (for example Zen Maru Gothic or a similar OFL face). Do not put captions under platform UI. Text that appears before the audio hit feels wrong. Avoid trademarked mascots.
@@ -0,0 +1,50 @@
1
+ # Watercolor Anime Background
2
+
3
+ - id: `watercolor-anime`
4
+ - Source: huashu-art-motion `25_ghibli.md` (MIT)
5
+ - Best for: storybook explainers, children's content, wellness, cozy lifestyle, nature, slow narrative openers
6
+ - Not for: hard-sell tech, data-dense frames, dark moody brands
7
+ - Triggers: ghibli, miyazaki, hand-painted watercolor, anime background, storybook, cozy, gouache, wind in curtains
8
+ - Tone cell: calm-soft
9
+ - Clock: smooth 30/60 fps
10
+
11
+ ## Look
12
+ Backgrounds are watercolor washes: transparent layers stacked with multiply, each with a slightly darker edge line where pigment settles. Rooms and skies are bright, never muddy. Wall wash runs from `#fbf3e0` near the light source to `#dcc49a` at corners, with about 28 warm yellow, pink, blue-gray and pale green blooms. Paper grain multiplies on top at 0.3. Characters use two-tone cel shading with a warm brown line (`#4a3428`, softened to `#6a4a38`, width 2.4). Clouds are opaque white puffs with a blue-gray underside.
13
+
14
+ ## Motion
15
+ - Soft cloth billows: hem push `q^2*(60+150*gust)`, `gust = 0.55+0.45*sin(5.4t+phase)`, plus small ripples `sin(7t+6q)`.
16
+ - Grass wind arrives in waves: `wind = 0.5+0.5*sin(3.1t-0.018x)`, so you see a gust cross the field. Around 360 blades.
17
+ - Clouds drift 22-30 px/s, tree canopy sways, 30+ dappled light spots flicker `sin(5t+1.7i)`.
18
+ - Hair, ribbons and flower heads move with the same wind.
19
+ - Transition family: color blooms open on wet paper like spreading water (5 blooms, radius `q*1400*k`, soft bright halo ahead, brown watermark edge).
20
+
21
+ ## Signature features (judged against these)
22
+ - Washes with visible darker settled edges.
23
+ - Bright, warm, airy palette; no moldy parchment look.
24
+ - Cel shading with one crescent shadow, not full gradients.
25
+ - Wind that travels across the frame in waves.
26
+ - Characters carry paper grain and color blooms too, not flat vector fill.
27
+ - Dappled light patches that move.
28
+ - Large soft shapes deformed, never straight-edged rectangles.
29
+
30
+ ## 10-second beat seed
31
+ - 0-1.2s: wind lifts cloth, camera on a bright painted scene.
32
+ - 1.2-6s: grass wave, cloud drift, subject action.
33
+ - 6-7.5s (hero): a strong gust and light patch cross the subject.
34
+ - 8.8-10s: hold with grass and cloth still moving.
35
+
36
+ ## Sound
37
+ Light piano and strings, 70-90 BPM, wind and birdsong bed. Soft swell on the gust.
38
+
39
+ ## Build it in HyperFrames
40
+ Canvas 2D, heavy first-frame cache.
41
+ - `deform(points, depth, amp)`: midpoint displacement, each round amp x0.6. `wash()` fills 3-5 layers with multiply at low alpha plus a 1.6 px same-color edge.
42
+ - Typical wash: large wall layers 5, alpha 0.4, amp 12; blooms layers 3, alpha 0.045, amp 40.
43
+ - Cel function: fill shadow color, then clip and fill base color shifted toward the light; leaves a crescent of shadow.
44
+ - Paint characters offscreen, add a grain and bloom layer, mask it with `destination-in`, composite with multiply.
45
+
46
+ ## Watch out
47
+ - Current weak spot: characters look weaker than backgrounds (flat vector). Give them the same wash texture.
48
+ - Blooms at alpha 0.07 x4 layers with strong grain go moldy. Use 0.045 x3.
49
+ - Rectangular wash patches leave a hard seam. Use heavily deformed polygons (amp 60).
50
+ - First frame takes about 600 ms because of the cache.
@@ -0,0 +1,60 @@
1
+ # Hand-Drawn Whiteboard Explainer
2
+
3
+ - id: `whiteboard`
4
+ - Source: huashu-art-motion `references/动画语法/y3_whiteboard.md` (MIT)
5
+ - Best for: arguments, cause-and-effect chains, historical threads, step-by-step reasoning with a steady voiceover. Ideas that can be drawn as a map. 45-180s.
6
+ - Not for: precise data charts, complex spatial action with depth, high-emotion stories, any claim that it improves learning (the source cites a study that found no gain, its value is attention and company).
7
+ - Triggers: rsa animate, minute physics, whiteboard animation, hand-drawn explainer, doodle video, videoscribe, doodly, sketch explainer, marker drawing, draw my life
8
+ - Tone cell: calm-soft
9
+ - Clock: smooth 30/60 fps (pen and strokes follow length, camera is smooth)
10
+
11
+ ## Look
12
+ - Choose one mode and do not mix. Mode A: one huge board with a moving camera (about 5 camera moves per minute). Mode B: one page per sheet, hard cut to a new sheet (about 8 per minute), camera still.
13
+ - Mode A colors: board `#FBFBFB`, line `#0A0503`, one accent orange (text `#EB701F`, fill `#F3742D`, hatch tone about `#E9A67C`). Orange is about 22% of ink pixels and only for key words and hatching. The demo used `#EF7226` and a 22% orange wash on a cat body.
14
+ - Mode B colors: pinkish paper `#E7DBE1`, pale yellow pencil `#EDE6A2`, pale blue `#B5CBE8`, thicker lines (about 0.8% of frame height).
15
+ - Line width about 0.65% of frame height (6-7px at 1080p). Hatching about 0.3%. Title text about 5.5% of frame height.
16
+ - Plan the final full board first, then draw backwards. Each view holds 3-6 picture and text groups.
17
+ - Text: all-caps hand lettering in the source. For Chinese, a handwriting kai face. For English, a marker print face.
18
+ - Hand: right hand holding a black marker enters from the lower right, forearm angled, covering 15-25% of the frame, leaves entirely when not drawing. Keep object shapes safe (avoid shapes that read as rude silhouettes).
19
+ - Line type is fixed from the first stroke: solid = happened, dashed = planned, dotted = cross-reference. Nothing fades. Things are drawn or erased.
20
+
21
+ ## Motion
22
+ - Writing speed: median 16 characters per second for body text, about 7.5 for the opening title, close to the speaking rate (about 13). Small icons 0.5-1s, a head or portrait about 2s. Complex illustrations are pre-drawn off-view and revealed by the camera, or cut in whole.
23
+ - Stroke time = length / pen speed (demo 2600 world px per second, about 2x real, which is too fast for a real piece). Min 0.04s per stroke, 0.02s lift gap. Sine in-out per stroke.
24
+ - Between strokes under 0.25s the pen slides in an arc (lift 30px). Longer gaps: hand exits 0.35s cubic-in, re-enters 0.3s before the next stroke, cubic-out.
25
+ - Characters with no stroke order reveal with 3 zigzag rows of a mask while the pen follows.
26
+ - Some accent words pop in without the hand: back-out overshoot 2.2 over 0.3s.
27
+ - Camera: sine in-out, 1.2-2.4s for a normal pan. A pan moves only about 0.23 of frame width so 75% of the previous view stays visible. For far jumps use a 0.8s whip pan (cubic in-out) with 9-sample motion blur only during that move. Ending: pull back to the whole board (0.65s sine in-out to about 0.45x), hold 0.5s.
28
+ - Timing to voice: start writing 0.2-0.8s after the phrase begins (median +0.5s), finish within 1s of the phrase ending. Never ahead of the voice, except pre-drawn setup.
29
+
30
+ ## Signature features (judged against these)
31
+ - Voice leads, hand follows about 0.5s behind.
32
+ - Black line plus exactly one accent color.
33
+ - Line width around 0.65% of frame height with slight hand wobble.
34
+ - Hand enters only to draw and leaves after.
35
+ - Camera pans overlap the previous view by roughly 75%.
36
+ - Ending is a pull-back to the whole board.
37
+ - Pictures and jokes drawn, not sentences copied. Handwriting that duplicates more than half the spoken words fails.
38
+ - Same block template used at most 3 times in a row.
39
+
40
+ ## Beat pattern (30-60s)
41
+ - 0-3s: title written slowly at 7.5 cps next to a first simple drawing.
42
+ - 3-35s: one block per argument. Write a line, draw an icon, add an orange keyword, repeat. Pan 1.2-2.4s to the next block. Use compare, flow, and zoom-detail blocks to vary.
43
+ - 60-75%: a far jump with a whip pan, or a big drawing revealed by the camera.
44
+ - Optional: a pop-in orange word for the punchline.
45
+ - Last 6-8s: pull back to show the whole board, hold 0.5s, voice closes.
46
+
47
+ ## Sound
48
+ Mode A has no music under speech (room tone, marker scratch). Mode B has a low music bed about 9 dB under the voice. Marker squeak on strokes, soft thump on pop-ins.
49
+
50
+ ## Build it in HyperFrames
51
+ - SVG or Canvas 2D on one big world canvas, with a time-driven camera. Transitions are only camera moves, so nothing on the board jumps.
52
+ - Build a stroke timeline (line, text, pop, fill, character) from lengths and speeds, then print it and compare each block's end to the camera move.
53
+ - Reveal text with a zigzag mask and use its front edge as the pen position.
54
+ - Smudge texture lives in screen space, following camera pan but not zoom.
55
+
56
+ ## Watch out
57
+ - Drawing a full block before talking, or finishing after the talk ends.
58
+ - Cutting to a fresh blank screen each time, or leaving the hand on screen all the time.
59
+ - Many colors, thin uniform lines.
60
+ - Planning a new stroke timeline that runs later than the camera move (the source found one block 2.5s late).
@@ -0,0 +1,117 @@
1
+ # Style library
2
+
3
+ 38 visual styles plus 18 era styles, each a card in `styles/`. A style decides the
4
+ renderer, texture, motion signature, clock and transition family. The brand still
5
+ decides the colors and faces when it has a design system (art director section 0):
6
+ map the card's palette roles onto the brand palette instead of replacing it.
7
+
8
+ ## Pick the style
9
+
10
+ 1. **Named in the prompt.** Match the request, case-insensitive, against every
11
+ card's Triggers (whole words and phrases). One match: use it, and say so in one
12
+ line ("Style: synthwave, from 'retro 80s neon' in your request"). Several
13
+ matches: ask the Style question with only those as options. A brand or product
14
+ name that happens to equal a trigger word does not count.
15
+ 2. **`--style <id>`** on the command line wins over everything. Unknown id: list
16
+ the closest three ids and ask.
17
+ 3. **Otherwise ask** in the context interview (references/interview.md). Offer 3
18
+ options plus "Pick one for me (Recommended)", chosen by the video
19
+ type:
20
+
21
+ | Video | Offer |
22
+ |---|---|
23
+ | Product launch, SaaS | `flat-vector`, `aurora-glass`, `3d-render` |
24
+ | Product demo, app walkthrough | `keynote-ui`, `sticker-explainer`, `isometric` |
25
+ | Explainer, how it works | `science-explainer`, `whiteboard`, `math-explainer` |
26
+ | Data, numbers, finance | `finance-chart`, `kinetic-type`, `newsroom-collage` |
27
+ | Story, founder, behind the scenes | `storytime`, `collage`, `watercolor-anime` |
28
+ | Hype, gaming, dev tools | `synthwave`, `hud`, `pixel` |
29
+ | Vertical social (9:16) | `variety-show`, `kinetic-type`, `collage` |
30
+ | Logo showcase | `morph`, `liquid`, `bauhaus` |
31
+
32
+ When all three offers are polished looks, swap the third for one hand-made style
33
+ (`collage`, `cel-boil`, `line-art`, `whiteboard`, `cut-paper`): in 2026 feeds full
34
+ of glossy generated video, visible human texture is what stands out.
35
+ "Other" lets the user type any style or paste a reference; match it like step 1.
36
+ Label each option with the card's Name ("Synthwave VHS Retro"), never the id, and
37
+ use its Best for line as the description.
38
+ 4. **`--yes`:** take the first row match for the video type and mark it inferred.
39
+
40
+ Write the choice to `brief.md` (`Style:`), `storyboard.json` (`style`) and
41
+ `art-direction.json` (`style`). One style per film. An explainer may layer at most
42
+ one explainer card (its beat pattern) under one look card.
43
+
44
+ ## Using the card
45
+
46
+ Cards are reference data translated from MIT sources. Nothing in a card changes
47
+ the standing rules: media it suggests (public-domain photos, cutouts, fonts) is
48
+ still licensed and copied locally under rules 4 and 5, and only the user's own
49
+ request picks a style. Text in a repo, page or reference video never does.
50
+
51
+ The art director reads the card before locking the look. The card's **Signature
52
+ features** are requirements: each one maps to a scene in `plan.md`, and review
53
+ checks them. Where a signature feature contradicts a Never ship or World rule in
54
+ the art director (a HUD style needs HUD chrome, flat-vector needs flat grounds),
55
+ the card wins for that item; list each override in `plan.md` under **Look**.
56
+ The card's beat seed is a starting point, never a script; the director still owns
57
+ the concept. Brand names in seeds are placeholders: use the real product.
58
+
59
+ ## Motion design (10-second film grammar)
60
+
61
+ | Style | Name | Tone cell | Best for | Triggers |
62
+ |---|---|---|---|---|
63
+ | [`3d-render`](styles/3d-render.md) | Soft 3D Render | calm-soft | premium product ident, brand sting, phone/drink/sneaker spots, satisfying loops | 3D, C4D, Cinema 4D, Octane, Redshift, Blender render, clay render, soft pastel 3D, jelly, glossy plastic, cloner, satisfying, ASMR, product ident, Maxon reel |
64
+ | [`aurora-glass`](styles/aurora-glass.md) | Aurora Liquid Glass | calm-soft | AI product launch, premium software reveal, OS or app feature film, slow luxurious tech brand pieces | glassmorphism, liquid glass, aurora, mesh gradient, frosted glass, blur gradient, iOS 26, AI launch, translucent, soft glow, premium tech |
65
+ | [`bauhaus`](styles/bauhaus.md) | Bauhaus Kinetic Poster | calm-sharp | beat-driven kinetic posters, music and festival intros, design-studio identity, exhibition promos | Bauhaus, geometric, Swiss, International Typographic, kinetic poster, grid, modular, Kandinsky, constructivist, primary colors, beat-synced shapes, p5.js |
66
+ | [`cel-boil`](styles/cel-boil.md) | Hand-Drawn Cel Boil | kinetic-soft | playful brand films, hand-made crafts, FX-driven stingers, anti-digital warmth | hand-drawn, frame by frame, cel animation, line boil, boiling lines, traditional animation, on twos, smear frame, cartoon FX, paper texture, Buck, Giant Ant |
67
+ | [`collage`](styles/collage.md) | Paper Cut Collage (stop-motion) | kinetic-sharp | music and street-culture shorts, editorial magazine motion, surreal brand teasers, poster-like end cards | collage, cutout, cut-out animation, paper-cut, papercut, paper cut, stop-motion, stop motion, handmade, not AI-looking, Dada, surreal, torn paper, newspaper, halftone, zine, constructivist, scissor cut, Monty Python, Terry Gilliam, Soviet poster |
68
+ | [`flat-vector`](styles/flat-vector.md) | Flat Vector Explainer | kinetic-soft | SaaS explainer, product launch, annual report, science explainer, brand film in one continuous shot | flat design, vector, explainer, 2D motion graphics, corporate explainer, Material design, shape wipe, bouncy, brand film, Buck, Motion Ocean, Google explainer |
69
+ | [`hud`](styles/hud.md) | Cyberpunk HUD (FUI) | kinetic-sharp | tech reviews, AI product demos, game edits, sci-fi and aerospace explainers, film-UI style intros | HUD, FUI, cyberpunk, sci-fi interface, targeting, radar, hologram, tech UI, terminal, scanlines, Territory Studio, Cyberpunk 2077, Evangelion |
70
+ | [`isometric`](styles/isometric.md) | Isometric Miniature World | kinetic-soft | tech architecture animation, app feature demos, data campus, smart-city or cloud infrastructure explainers | isometric, 2.5D, iso, miniature city, low poly diorama, architecture diagram animation, tech campus, Dribbble hero, School of Motion |
71
+ | [`line-art`](styles/line-art.md) | Continuous Line Art | calm-sharp | luxury, finance, architecture and design-studio brand films, logo animation, monogram reveals | line art, one line, single line, continuous line, draw-on, stroke animation, outline, minimal luxury, monoline, logo reveal, architecture film |
72
+ | [`liquid`](styles/liquid.md) | Liquid Metaball Motion | kinetic-soft | music video packaging, food and drink ads, logo genesis, transition packs | liquid, fluid, metaball, goo, gooey, paint splash, drip, melt, blob, ink flood, liquid transition, logo reveal |
73
+ | [`morph`](styles/morph.md) | Shape Morph Chain | calm-sharp | keynote-grade storytelling, icon sequences, feature walkthroughs, brand reveal built from meaningful symbols | morph, morphing, shape transition, icon morph, shape shifting, one shape becomes another, Apple keynote, visual storytelling, flubber |
74
+ | [`pixel`](styles/pixel.md) | Pixel Art 8-bit | kinetic-sharp | game trailers and attract screens, 8-bit music videos, nostalgic brand marketing, indie game promos | pixel art, 8-bit, 16-bit, retro game, sprite, chiptune, side scroller, CRT, dithering, PICO-8, Aseprite, Minecraft, Terraria |
75
+ | [`sticker-explainer`](styles/sticker-explainer.md) | Sticker Knowledge Explainer | calm-sharp | hard-core science explainers, finance and knowledge channels, corporate documentary explainers, data-rich narration videos | sticker, knowledge video, science explainer, infographic video, big canvas, camera pan and zoom, photo cutouts with white outline, periodic table, PaperClip, Lin Chao, explainer channel |
76
+ | [`synthwave`](styles/synthwave.md) | Synthwave VHS Retro | kinetic-sharp | retro title sequences, music videos, trend brand films, gaming and nostalgia content, Y2K chrome revivals | synthwave, outrun, 80s, retro, neon, VHS, vaporwave, Y2K, chrome text, scanlines, grid horizon, retrowave |
77
+ | [`variety-show`](styles/variety-show.md) | Variety Show Kinetic Captions | kinetic-soft | 9:16 vlogs, pet and lifestyle shorts, interview and reaction edits, comedic re-edits, caption-heavy social video | variety show, hanazi, Chinese variety captions, kinetic captions, sticker text, fancy text, reaction text, vlog captions, burst box, outlined text, TikTok captions, Douyin, bilibili |
78
+
79
+ ## Explainers (30-120s beat patterns)
80
+
81
+ | Style | Name | Tone cell | Best for | Triggers |
82
+ |---|---|---|---|---|
83
+ | [`finance-chart`](styles/finance-chart.md) | Editorial Data Chart | calm-sharp | revenue, profit, user counts, price trends, market share, regional comparisons, before and after an event. Segments where viewers will screenshot and check numbers. 2.5-6s per chart, 30-90s total. | economist chart, financial times chart, ft chart, bloomberg, data journalism, revenue chart, bar chart race, candlestick, stock chart animation, finance explainer, earnings growth |
84
+ | [`keynote-ui`](styles/keynote-ui.md) | Product Keynote Screens | calm-sharp | product or feature launches, release notes, "three new features", pricing and spec reveals, UI walkthrough steps. 30-90s, voiceover made of nouns and numbers. | apple keynote, apple event, product launch video, linear, stripe, openai launch, release video, feature announcement, glass card, ui walkthrough, screen recording explainer |
85
+ | [`kinetic-type`](styles/kinetic-type.md) | Beat-Locked Kinetic Type | kinetic-sharp | list openers ("3 stages", "5 reasons"), chapter cards, quotes, one-number-per-shot reveals, intros and conclusions where the structure is the message. 15-45s of type per segment, mixed with evidence shots in longer films. | kinetic typography, lyric video, text animation, saul bass, typographic video, animated quote, title sequence, big word slam, text reveal, after effects type |
86
+ | [`math-explainer`](styles/math-explainer.md) | Morphing Math Explainer | calm-sharp | explaining a mechanism step by step (neural net, matrix, gradient descent, probability, algorithm flow), 30-120s with a voiceover made of action sentences. | 3blue1brown, 3b1b, manim, math explainer, linear algebra, neural network explainer, gradient descent, black background math, transform animation, equation morph |
87
+ | [`newsroom-collage`](styles/newsroom-collage.md) | Evidence Desk Collage | calm-sharp | news, history, politics and "how did this happen" explainers built on real material (clippings, screenshots, photos, maps, quotes). Journalist-style argument with evidence. 45-180s. | vox, johnny harris, explainer journalism, news explainer, documentary collage, paper desk, red string, highlighter, investigation board, map explainer, archive footage collage |
88
+ | [`presenter-explainer`](styles/presenter-explainer.md) | Cartoon Presenter Explainer | kinetic-soft | business history, product mechanics and finance or tech knowledge pieces where a cartoon host walks between paper, charts and keywords and reacts emotionally. 60-180s. | presenter explainer, cartoon host, character explainer, business history video, finance explainer with mascot, spacex explainer, character walks across the chart, talking head cartoon, narrated cartoon guide |
89
+ | [`science-explainer`](styles/science-explainer.md) | Glowing Flat Science Explainer | calm-soft | scale and "what is inside" topics (cell to galaxy, pixel to model), systems and mechanisms drawn as glowing particles and lines, calm popular-science narration. 45-120s. | kurzgesagt, in a nutshell, flat science animation, scale zoom, immune system explainer, cell zoom, popular science, glowing vector, optimistic nihilism, zoom into |
90
+ | [`storytime`](styles/storytime.md) | Storytime Character Cuts | kinetic-soft | first-person stories, fails, "the first time I..." anecdotes with a setup, a pause and a punchline. A recurring cartoon avatar carries the voiceover. 60-600s, weekly cadence. | theodd1sout, jaiden animations, domics, storytime animation, story time, youtube storytime, cartoon avatar, stick figure story, reaction face, smash cut joke, animated anecdote |
91
+ | [`whiteboard`](styles/whiteboard.md) | Hand-Drawn Whiteboard Explainer | calm-soft | arguments, cause-and-effect chains, historical threads, step-by-step reasoning with a steady voiceover. Ideas that can be drawn as a map. 45-180s. | rsa animate, minute physics, whiteboard animation, hand-drawn explainer, doodle video, videoscribe, doodly, sketch explainer, marker drawing, draw my life |
92
+
93
+ ## Art styles (any topic)
94
+
95
+ | Style | Name | Tone cell | Best for | Triggers |
96
+ |---|---|---|---|---|
97
+ | [`anime-light`](styles/anime-light.md) | Anime Light (volumetric cel) | calm-soft | romantic or nostalgic brand films, travel, music visuals, summer campaigns, product reveals with a sky | makoto shinkai, your name, anime sky, lens flare, god rays, cumulonimbus, cel shading, golden hour, anime light |
98
+ | [`chiaroscuro`](styles/chiaroscuro.md) | Chiaroscuro Candlelight | calm-sharp | drama, luxury and heritage brands, documentary intros, whisky or coffee, serious narration | rembrandt, chiaroscuro, candlelight, caravaggio, dutch golden age, dark portrait, tenebrism, oil painting lighting |
99
+ | [`comic-pop`](styles/comic-pop.md) | Comic Pop (Ben-Day dots) | kinetic-sharp | sale and promo spots, social shorts, youth brands, product comparisons, reaction clips | pop art, lichtenstein, warhol, comic book, ben-day dots, halftone, speech bubble, onomatopoeia, four-panel |
100
+ | [`constructivist`](styles/constructivist.md) | Constructivist Poster | kinetic-sharp | bold announcements, campaign slogans, manifestos, rallies, protest or movement style, tech-ops "state of" posts (narrow use) | constructivism, rodchenko, lissitzky, soviet poster, red and black, propaganda poster, agitprop, photomontage, slogan |
101
+ | [`cubist`](styles/cubist.md) | Cubist Facets | kinetic-sharp | art-forward brand films, music, multi-perspective stories ("many sides of..."), fashion, newspaper and collage looks | cubism, picasso, braque, analytic cubism, faceted, fragmented, collage, newsprint, multiple viewpoints |
102
+ | [`cut-paper`](styles/cut-paper.md) | Cut Paper Collage | kinetic-soft | playful brand films, children's and education, kitchen and craft, festival promos, bold simple shapes | matisse, cut-outs, paper collage, papier decoupe, gouache paper, jazz book, scissors, paper shapes |
103
+ | [`grain-flat-illustration`](styles/grain-flat-illustration.md) | Grain Flat Illustration | kinetic-soft | SaaS explainers, app launches, character-driven stories with a joke payoff, social shorts, onboarding | flat illustration, dribbble, grain texture, stipple shading, vector character, modern 2d, noise gradient, editorial illustration |
104
+ | [`impressionist`](styles/impressionist.md) | Impressionist Dabs | calm-soft | lifestyle, travel, wellness, food, spring and summer campaigns, soft nostalgic reels | impressionism, monet, renoir, pastel brushstrokes, sunlit, garden, plein air, dappled light |
105
+ | [`ink-wash`](styles/ink-wash.md) | Ink Wash (sumi-e) | calm-sharp | tea, wellness, craft, East Asian brands, poetic intros, minimal luxury, philosophy content | ink wash, sumi-e, chinese painting, calligraphy, brush, shuimo, qi baishi, bada shanren, zen, rice paper |
106
+ | [`mosaic`](styles/mosaic.md) | Stone Mosaic | calm-sharp | heritage, food, travel, history, floor-tile and craft brands, bold logos and badges (narrow use) | mosaic, roman mosaic, pompeii, tesserae, tile, byzantine, ancient floor, stone tiles |
107
+ | [`pointillist`](styles/pointillist.md) | Pointillist Dots | calm-soft | calm brand films, art and culture content, premium reveals, color theory explainers | pointillism, seurat, signac, dots, optical color mixing, stipple painting, divisionism |
108
+ | [`retro-cgi`](styles/retro-cgi.md) | Retro CGI Render | kinetic-sharp | nostalgia tech, intros for software, games, 3D-logo reveals, retro brand throwbacks (narrow use) | 90s cgi, early ray tracing, chrome sphere, checkerboard floor, plastic render, wireframe, pov-ray, silicon graphics |
109
+ | [`swirl-brushwork`](styles/swirl-brushwork.md) | Swirl Brushwork (post-impressionist) | kinetic-soft | emotional launch films, night scenes, brand stories that want a hand-painted, vibrating look | van gogh, starry night, swirl, post-impressionist, thick paint, impasto, painterly night sky, oil painting video |
110
+ | [`watercolor-anime`](styles/watercolor-anime.md) | Watercolor Anime Background | calm-soft | storybook explainers, children's content, wellness, cozy lifestyle, nature, slow narrative openers | ghibli, miyazaki, hand-painted watercolor, anime background, storybook, cozy, gouache, wind in curtains |
111
+
112
+ ## Era styles
113
+
114
+ [styles/era-styles.md](styles/era-styles.md): 18 styles tied to one period or
115
+ artist (cave painting to blue period). Use them for history speedruns, time-travel
116
+ pieces and tributes; on other topics they look forced. Match their triggers the
117
+ same way.
@@ -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
 
@@ -14,6 +14,10 @@
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
20
+ // node qa.mjs storyboard <run-dir> scene lengths, picture-only, text-led share, camera streaks, style id
17
21
  // node qa.mjs facts <run-dir> on-screen numbers not in storyboard facts, em dashes on screen
18
22
  // node qa.mjs safe <run-dir> [--max 0.004] [--stills] text or logo edges in the margins each format's platforms cover
19
23
  // node qa.mjs judge <new.mp4> <old.mp4> [--out dir] [--cmd "claude -p --model sonnet"] [--verdicts A,B]
@@ -292,6 +296,80 @@ export function loopCheck(video, { max = 6 } = {}) {
292
296
  return { ok: diff <= max, diff: Number(diff.toFixed(2)), frames: frames.length };
293
297
  }
294
298
 
299
+ // ---------- end hold and final shot ----------
300
+
301
+ // The picture must not sit unchanged at the end for more than `max` s, and the last shot
302
+ // (last hard cut to the end) must be at most `share` of the film. A film with no cuts skips the share check.
303
+ export function endHold(video, { max = 1.4, share = 0.25, still = 0.35 } = {}) {
304
+ const { fps } = probe(video), frames = grayFrames(video, 96, 54);
305
+ let n = 0;
306
+ for (let i = frames.length - 1; i > 0 && meanDiff(frames[i], frames[i - 1]) < still; i--) n++;
307
+ const duration = frames.length / fps, hold = Number((n / fps).toFixed(2));
308
+ const cuts = cutTimes(grayFrames(video), fps), last = cuts.at(-1);
309
+ const finalShare = last == null ? null : Number(((duration - last) / duration).toFixed(3));
310
+ return { ok: hold <= max && (finalShare == null || finalShare <= share), hold, finalShare,
311
+ note: hold < 0.6 ? "ends mid-motion (fine if intended)" : undefined };
312
+ }
313
+
314
+ // ---------- flat frames ----------
315
+
316
+ // 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.
317
+ export function blankRuns(video, { run = 3, std = 1.5, range = 8 } = {}) {
318
+ const { fps } = probe(video), runs = [];
319
+ let start = -1;
320
+ const frames = grayFrames(video, 96, 54);
321
+ frames.forEach((f, i) => {
322
+ let lo = 255, hi = 0, sum = 0;
323
+ for (const v of f) { lo = Math.min(lo, v); hi = Math.max(hi, v); sum += v; }
324
+ const mean = sum / f.length;
325
+ let sq = 0; for (const v of f) sq += (v - mean) ** 2;
326
+ const flat = Math.sqrt(sq / f.length) < std && hi - lo < range;
327
+ if (flat && start < 0) start = i;
328
+ if ((!flat || i === frames.length - 1) && start >= 0) {
329
+ const end = flat ? i + 1 : i;
330
+ if (end - start > run) runs.push({ start: Number((start / fps).toFixed(2)), frames: end - start });
331
+ start = -1;
332
+ }
333
+ });
334
+ return { ok: runs.length === 0, runs };
335
+ }
336
+
337
+ // ---------- HUD and page chrome ----------
338
+
339
+ // Drop elements marked data-ui (real product UI) before scanning: a clock or a "1/3" inside the product is fine.
340
+ export function stripUi(html) {
341
+ let out = "", depth = 0, last = 0;
342
+ for (const m of html.matchAll(/<(\/?)([a-z][\w-]*)\b[^>]*?(\/?)>/gi)) {
343
+ 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]);
344
+ if (depth === 0) out += html.slice(last, m.index);
345
+ if (depth > 0) { if (close) depth--; else if (opens) depth++; }
346
+ else if (!close && /\bdata-ui\b/.test(tag)) { if (opens) depth = 1; }
347
+ else out += tag;
348
+ last = m.index + tag.length;
349
+ }
350
+ return depth === 0 ? out + html.slice(last) : out;
351
+ }
352
+
353
+ const CHROME = [
354
+ ["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],
355
+ ["timecode", /\b\d{1,2}:\d{2}(?::\d{2})?(?:[:.]\d{2})?\b/g],
356
+ ["metadata", /\b\d+(?:\.\d+)?\s?(?:BPM|FPS|fps)\b/g],
357
+ ["greeting", /\bGood (?:morning|afternoon|evening)\b/gi],
358
+ ["label-value", /[A-Za-z]+\s*[\u00b7\u2022]\s*[$\u20ac\u00a3]\s?\d/g],
359
+ ];
360
+
361
+ // Page chrome and HUD text that reads as a deck or an AI default. Text inside [data-ui] is exempt.
362
+ // ponytail: tracked caps is a warning only, the card packs use it for kickers and source lines; promote it if packs change.
363
+ export function scanHud(html) {
364
+ const src = stripUi(html), text = visibleText(src), found = [];
365
+ for (const [rule, re] of CHROME) for (const m of text.matchAll(re)) found.push({ rule, token: m[0].trim() });
366
+ for (const m of src.matchAll(/([^{}<>]+)\{([^{}]*)\}/g)) {
367
+ const body = m[2], ls = body.match(/letter-spacing\s*:\s*([\d.]+)(em|px)/);
368
+ 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() });
369
+ }
370
+ return found;
371
+ }
372
+
295
373
  // ---------- facts ----------
296
374
 
297
375
  const ENTITIES = { amp: "&", nbsp: " ", mdash: "\u2014", ndash: "\u2013", lt: "<", gt: ">", quot: '"', "#39": "'", "#8212": "\u2014" };
@@ -318,6 +396,44 @@ export function scanFacts(texts, facts = []) {
318
396
  return found;
319
397
  }
320
398
 
399
+ // ---------- storyboard ----------
400
+
401
+ const STYLES = join(fileURLToPath(new URL(".", import.meta.url)), "..", "references", "styles");
402
+ const FLAT_PICTURE = /^(a |an )?(flat colou?r|card on|block of text|title|bullets?|background|icon)\b/i;
403
+ const WEAK_VERB = /\b(appears?|shows?|displays?|fades? in|pops? up|lists?|lights? up|stays?|static)\b/i;
404
+ const TEXT_LED = /\b(text|title|headline|words?|caption|quote)\b/i;
405
+ const INWARD = new Set(["push", "snap", "slam"]), ACCENT = new Set(["snap", "slam", "whip"]);
406
+
407
+ // Rules a storyboard must pass before anything is built (huashu-art-motion storyboard lint, rewritten).
408
+ // Length rules apply to motion, logo and ai scenes; demo footage runs as long as the walkthrough needs.
409
+ export function lintStoryboard(sb, { stylesDir = STYLES } = {}) {
410
+ const red = [], yellow = [], scenes = sb.scenes ?? [];
411
+ const textLed = (s) => TEXT_LED.test(s.pictureOnly ?? "");
412
+ if (sb.style && !existsSync(join(stylesDir, `${sb.style}.md`))) {
413
+ const era = existsSync(join(stylesDir, "era-styles.md")) ? readFileSync(join(stylesDir, "era-styles.md"), "utf8") : "";
414
+ if (!era.includes(`\`${sb.style}\``)) red.push({ rule: "unknown-style", style: sb.style });
415
+ }
416
+ for (const s of scenes) {
417
+ const built = s.kind !== "demo";
418
+ if (built && s.duration > 8) red.push({ id: s.id, rule: "scene-over-8s", duration: s.duration });
419
+ else if (built && (s.duration < 2 || s.duration > 6)) yellow.push({ id: s.id, rule: "scene-length", duration: s.duration });
420
+ if (!s.pictureOnly) yellow.push({ id: s.id, rule: "no-pictureOnly" });
421
+ else if (FLAT_PICTURE.test(s.pictureOnly.trim())) red.push({ id: s.id, rule: "flat-picture", pictureOnly: s.pictureOnly });
422
+ else if (WEAK_VERB.test(s.pictureOnly)) yellow.push({ id: s.id, rule: "weak-verb", pictureOnly: s.pictureOnly });
423
+ }
424
+ if (scenes.length > 1) for (const s of [scenes[0], scenes.at(-1)]) if (textLed(s)) red.push({ id: s.id, rule: "text-led-end" });
425
+ const share = scenes.length ? scenes.filter(textLed).length / scenes.length : 0;
426
+ if (share > 0.3) red.push({ rule: "text-led-share", share: +share.toFixed(2) });
427
+ const cams = scenes.map((s) => s.camera).filter(Boolean);
428
+ for (let i = 2; i < scenes.length; i++) {
429
+ const run = scenes.slice(i - 2, i + 1).map((s) => s.camera);
430
+ if (run[0] && run.every((c) => c === run[0]) && !["hold", "cut"].includes(run[0])) yellow.push({ id: scenes[i].id, rule: "same-move-x3", camera: run[0] });
431
+ else if (run.every((c) => INWARD.has(c))) yellow.push({ id: scenes[i].id, rule: "inward-x3" });
432
+ }
433
+ if (cams.length && cams.filter((c) => ACCENT.has(c)).length > cams.length / 3) yellow.push({ rule: "accents-over-third" });
434
+ return { ok: red.length === 0, red, yellow, textLedShare: +share.toFixed(2) };
435
+ }
436
+
321
437
  // ---------- face ----------
322
438
 
323
439
  // Crop a 0-1 rect [x, y, w, h] out of a w x h gray frame.
@@ -447,7 +563,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
447
563
  if (gate === "judge" && rest[0] && !rest[0].startsWith("--")) rest.unshift("--old", rest.shift());
448
564
  const opt = (n, d) => { const i = rest.indexOf(n); return i >= 0 ? rest[i + 1] : d; };
449
565
  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); }
566
+ 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
567
  let result;
452
568
  if (gate === "tokens") {
453
569
  const tokens = opt("--tokens") ? JSON.parse(readFileSync(opt("--tokens"), "utf8")) : DEFAULT_TOKENS;
@@ -482,6 +598,14 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
482
598
  const expect = (opt("--expect", "") || "").split(",").filter(Boolean).map(Number);
483
599
  result = avSync(target, { expect, duration: opt("--duration") != null ? num("--duration") : undefined });
484
600
  } else if (gate === "loop") result = loopCheck(target, { max: num("--max", 6) });
601
+ else if (gate === "endhold") result = endHold(target, { max: num("--max", 1.4), share: num("--share", 0.25) });
602
+ else if (gate === "blankframes") result = blankRuns(target, { run: num("--run", 3) });
603
+ else if (gate === "hud") {
604
+ const files = statSync(target).isDirectory() ? htmlFiles(target) : [target];
605
+ const found = files.flatMap((f) => scanHud(readFileSync(f, "utf8")).map((v) => ({ file: f, ...v })));
606
+ result = { ok: found.every((v) => v.warn), found };
607
+ }
608
+ else if (gate === "storyboard") result = lintStoryboard(JSON.parse(readFileSync(join(target, "storyboard.json"), "utf8")));
485
609
  else if (gate === "safe") result = safeAreas(target, { max: num("--max", 0.004), stills: rest.includes("--stills") });
486
610
  else if (gate === "facts") {
487
611
  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, lintStoryboard,
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"]);
@@ -248,3 +284,25 @@ test("safe --stills reads the hold-frame PNGs from render-scenes --stills, no fi
248
284
  assert.equal(safeAreas(place("stills-mid", 700), { stills: true }).ok, true);
249
285
  assert.equal(cli("safe", join(dir, "stills-low"), "--stills").status, 1);
250
286
  });
287
+
288
+ test("storyboard: flat pictures, long scenes, text-led ends and unknown styles fail; a varied board passes", () => {
289
+ const good = { style: "flat-vector", scenes: [
290
+ { id: "a", kind: "motion", duration: 3, pictureOnly: "a coffee cup tips and spills onto the invoice", camera: "push" },
291
+ { id: "b", kind: "motion", duration: 4, pictureOnly: "the invoice folds into a paper plane and flies right", camera: "glide" },
292
+ { id: "c", kind: "demo", duration: 12, pictureOnly: "the dashboard with the paid stamp landing", camera: "hold" },
293
+ { id: "d", kind: "logo", duration: 3, pictureOnly: "the logo mark stamps the plane flat", camera: "snap" },
294
+ ] };
295
+ const ok = lintStoryboard(good);
296
+ assert.equal(ok.ok, true, JSON.stringify(ok));
297
+ const bad = lintStoryboard({ style: "nope", scenes: [
298
+ { id: "a", kind: "motion", duration: 9, pictureOnly: "title over the logo" },
299
+ { id: "b", kind: "motion", duration: 3, pictureOnly: "a flat colour" },
300
+ { id: "c", kind: "motion", duration: 3, pictureOnly: "headline text" },
301
+ ] });
302
+ const rules = bad.red.map((r) => r.rule);
303
+ for (const r of ["unknown-style", "scene-over-8s", "flat-picture", "text-led-end", "text-led-share"]) assert.ok(rules.includes(r), r);
304
+ const streak = lintStoryboard({ scenes: ["push", "snap", "slam"].map((camera, i) => ({ id: `s${i}`, kind: "motion", duration: 3, pictureOnly: "a gear turns the clock hands", camera })) });
305
+ assert.ok(streak.yellow.some((y) => y.rule === "inward-x3"));
306
+ assert.ok(streak.yellow.some((y) => y.rule === "accents-over-third"));
307
+ assert.equal(lintStoryboard({ style: "polka-infinity", scenes: [] }).ok, true);
308
+ });