kaizen-agent 1.48.0 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +2 -2
  2. package/cli/build-web.ts +4 -2
  3. package/cli/hermes.ts +65 -0
  4. package/cli/install.ts +18 -5
  5. package/cli/localsend.test.ts +84 -0
  6. package/cli/localsend.ts +131 -0
  7. package/cli/notes.test.ts +6 -4
  8. package/cli/settings.ts +1 -0
  9. package/cli/state.test.ts +37 -0
  10. package/cli/state.ts +35 -20
  11. package/cli/video.test.ts +479 -0
  12. package/cli/video.ts +507 -0
  13. package/cli/web.ts +127 -1
  14. package/package.json +1 -1
  15. package/skills/kaizen/config.default.yml +4 -0
  16. package/skills/media/kaizen-book/SKILL.md +107 -0
  17. package/skills/media/kaizen-book/assets/reader.html +440 -0
  18. package/skills/media/kaizen-book/assets/styles.json +45 -0
  19. package/skills/media/kaizen-book/examples/book.json +14 -0
  20. package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
  21. package/skills/media/kaizen-book/examples/manuscript.md +127 -0
  22. package/skills/media/kaizen-book/references/interview.md +85 -0
  23. package/skills/media/kaizen-book/references/proofread.md +47 -0
  24. package/skills/media/kaizen-book/references/writing.md +53 -0
  25. package/skills/media/kaizen-book/scripts/build_book.py +279 -0
  26. package/skills/media/kaizen-book/scripts/check.sh +10 -0
  27. package/skills/media/kaizen-book/scripts/check_book.py +128 -0
  28. package/skills/media/kaizen-book/scripts/deploy.py +58 -0
  29. package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
  30. package/skills/media/kaizen-book/scripts/setup.sh +9 -0
  31. package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
  32. package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
  33. package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
  34. package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
  35. package/skills/media/kaizen-video/SKILL.md +112 -0
  36. package/skills/{kaizen-video → media/kaizen-video}/assets/CREDITS.txt +5 -0
  37. package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
  38. package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
  39. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
  40. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
  41. package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
  42. package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
  43. package/skills/media/kaizen-video/film.html +246 -0
  44. package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
  45. package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
  46. package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
  47. package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
  48. package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
  49. package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
  50. package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
  51. package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
  52. package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
  53. package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
  54. package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
  55. package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
  56. package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
  57. package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
  58. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
  59. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
  60. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
  61. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
  62. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
  63. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
  64. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
  65. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
  66. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
  67. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
  68. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
  69. package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
  70. package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
  71. package/skills/{kaizen-video → media/kaizen-video}/references/presets.md +22 -0
  72. package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
  73. package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
  74. package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
  75. package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
  76. package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
  77. package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
  78. package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
  79. package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
  80. package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
  81. package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
  82. package/skills/{kaizen-video → media/kaizen-video}/scripts/check.sh +8 -0
  83. package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
  84. package/skills/media/kaizen-video/scripts/make_film.py +263 -0
  85. package/skills/{kaizen-video → media/kaizen-video}/scripts/make_tiktok.py +8 -3
  86. package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
  87. package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
  88. package/skills/media/kaizen-video/scripts/srt.py +9 -0
  89. package/skills/media/kaizen-video/scripts/still.sh +7 -0
  90. package/web/board/views.ts +1 -1
  91. package/web/board.html +2 -0
  92. package/web/board.js +2 -2
  93. package/web/notebook.js +1 -1
  94. package/web/notes.html +2 -0
  95. package/web/term.js +1 -1
  96. package/web/video.html +298 -0
  97. package/web/video.js +81 -0
  98. package/web/video.ts +610 -0
  99. package/skills/kaizen-video/SKILL.md +0 -73
  100. package/skills/story-video/SKILL.md +0 -108
  101. package/skills/story-video/engine/package.json +0 -12
  102. package/skills/story-video/engine/src/Root.tsx +0 -24
  103. package/skills/story-video/engine/src/engine/Film.tsx +0 -405
  104. package/skills/story-video/engine/src/engine/presets.json +0 -58
  105. package/skills/story-video/engine/src/index.ts +0 -3
  106. package/skills/story-video/engine/tsconfig.json +0 -1
  107. package/skills/story-video/examples/sample-film/README.md +0 -12
  108. package/skills/story-video/examples/sample-film/film.json +0 -16
  109. package/skills/story-video/references/presets.md +0 -21
  110. package/skills/story-video/scripts/check.sh +0 -19
  111. package/skills/story-video/scripts/setup.sh +0 -25
  112. package/skills/story-video/scripts/srt.py +0 -30
  113. package/skills/story-video/scripts/still.sh +0 -10
  114. /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
  115. /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
  116. /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
  117. /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
  118. /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
  119. /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
  120. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Fredoka.ttf +0 -0
  121. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Montserrat.ttf +0 -0
  122. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/SpecialElite.ttf +0 -0
  123. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/investigations.mp3 +0 -0
  124. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/lights.mp3 +0 -0
  125. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/sunny.mp3 +0 -0
  126. /package/skills/{kaizen-video → media/kaizen-video}/examples/config.json +0 -0
  127. /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
  128. /package/skills/{kaizen-video → media/kaizen-video}/hyperframes.json +0 -0
  129. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/CREDITS.txt +0 -0
  130. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/README.md +0 -0
  131. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/ending.wav +0 -0
  132. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/reveal.wav +0 -0
  133. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/shutter.wav +0 -0
  134. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/stamp.wav +0 -0
  135. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/index.html +0 -0
  136. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/package.json +0 -0
  137. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/CREDITS.txt +0 -0
  138. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/README.md +0 -0
  139. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
  140. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
  141. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
  142. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
  143. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
  144. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
  145. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
  146. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
  147. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
  148. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/index.html +0 -0
  149. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/package.json +0 -0
  150. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/CREDITS.txt +0 -0
  151. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/README.md +0 -0
  152. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
  153. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
  154. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
  155. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
  156. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
  157. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
  158. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
  159. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
  160. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
  161. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/index.html +0 -0
  162. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/package.json +0 -0
  163. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/CREDITS.txt +0 -0
  164. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/README.md +0 -0
  165. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
  166. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
  167. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
  168. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
  169. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
  170. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
  171. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
  172. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
  173. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/index.html +0 -0
  174. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/package.json +0 -0
  175. /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
  176. /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
  177. /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
  178. /package/skills/{kaizen-video → media/kaizen-video}/scripts/render_cut.py +0 -0
  179. /package/skills/{kaizen-video → media/kaizen-video}/scripts/setup.sh +0 -0
  180. /package/skills/{kaizen-video → media/kaizen-video}/scripts/transcribe.py +0 -0
  181. /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
  182. /package/skills/{kaizen-video → media/kaizen-video}/tools/carve.mjs +0 -0
  183. /package/skills/{kaizen-video → media/kaizen-video}/tools/lib/main-module.mjs +0 -0
@@ -0,0 +1,246 @@
1
+ <!doctype html>
2
+ <html lang="{{LANG}}" data-resolution="{{RES}}">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width={{W}}, height={{H}}" />
6
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
7
+ <script src="film-data.js"></script>
8
+ <style>
9
+ /*FONTS*/
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+ html, body { width: {{W}}px; height: {{H}}px; overflow: hidden; }
12
+ #root { position: relative; width: 100%; height: 100%; overflow: hidden; }
13
+ .fill { position: absolute; inset: 0; display: flex; flex-direction: column; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div id="root" data-composition-id="main" data-start="0" data-duration="{{DURATION}}" data-width="{{W}}" data-height="{{H}}">
18
+ <div id="beats" class="fill"></div>
19
+ <div id="grade" class="fill" style="pointer-events: none"></div>
20
+ <div id="captions" class="fill" style="pointer-events: none"></div>
21
+ <!-- AUDIO -->
22
+ </div>
23
+
24
+ <script>
25
+ // One film = FILM (film-data.js, written by make_film.py): the style, the beats with their absolute start/len
26
+ // in seconds, and the caption groups. One function per beat kind builds its DOM and its tweens.
27
+ const F = FILM, S = F.style, P = S.palette, FD = S.fonts, W = F.W, H = F.H, portrait = H > W, short = portrait && H < W * 1.5; // short: 4:5, a portrait frame 1350 tall
28
+ const pad = portrait ? 80 : 140, lead = S.pace.lead, f = n => n / 30;
29
+ const isInter = FD.display === "Inter", light = P.bg.toUpperCase() > "#C";
30
+ const kinetic = S.captions === "kinetic"; // big captions take more of the bottom edge
31
+ const tl = gsap.timeline({ paused: true });
32
+ const $ = (css, html = "", parent) => { const e = document.createElement("div"); Object.assign(e.style, css); e.innerHTML = html; if (parent) parent.append(e); return e; };
33
+ const FILL = { position: "absolute", inset: 0, display: "flex", flexDirection: "column" };
34
+ const body = (size, color = P.muted, extra = {}) => ({ fontFamily: `"${FD.body}"`, fontSize: size + "px", color, ...extra });
35
+
36
+ // Text entrance by style: rise | slide | pop. `at` is seconds after the beat starts.
37
+ const enter = (b, el, at, origin = "left center") => {
38
+ const t = b.start + at, pop = S.text === "pop";
39
+ const from = { opacity: 0, ...(S.text === "rise" ? { y: 26 } : S.text === "slide" ? { x: -50 } : { scale: 0.6 }) };
40
+ gsap.set(el, { transformOrigin: origin });
41
+ tl.fromTo(el, from, { opacity: 1, x: 0, y: 0, scale: 1, duration: pop ? 0.4 : 0.5, ease: pop ? "back.out(2.2)" : "power2.out" }, t);
42
+ };
43
+ const kick = (b, text, at, parent) => { const e = $({ ...body(22, P.accent), fontWeight: 700, letterSpacing: "9px", textTransform: "uppercase" }, text, parent); enter(b, e, at); return e; };
44
+ const title = (b, text, at, size, o = {}, parent) => {
45
+ const e = $({ fontFamily: `"${FD.display}"`, fontWeight: isInter ? 900 : 400, fontStyle: o.italic ? "italic" : "normal", fontSize: size + "px",
46
+ lineHeight: 1.08, color: o.color || P.fg, letterSpacing: isInter ? "-1px" : "0", textAlign: o.center ? "center" : "left" }, text, parent);
47
+ enter(b, e, at, o.center ? "center" : "left center"); return e;
48
+ };
49
+ const italic = !isInter;
50
+ const textBlock = (b, parent, maxW, size = 88) => {
51
+ const box = $({ maxWidth: maxW + "px" }, "", parent);
52
+ if (b.kick) kick(b, b.kick, lead, box);
53
+ if (b.title) title(b, b.title, lead + 0.2, size, {}, box).style.marginTop = "22px";
54
+ if (b.sub) { const s = $(body(30, P.muted, { lineHeight: 1.4, marginTop: "28px" }), b.sub, box); enter(b, s, lead + 1.0); }
55
+ return box;
56
+ };
57
+ const photo = (b, parent, amount, style = {}) => {
58
+ const box = $({ ...FILL, overflow: "hidden", ...style }, "", parent);
59
+ const im = document.createElement("img"); im.src = b.img; im.setAttribute("data-layout-allow-overflow", "");
60
+ Object.assign(im.style, { width: "100%", height: "100%", objectFit: "cover" }); box.append(im);
61
+ tl.fromTo(im, { scale: 1, x: 0 }, { scale: 1 + amount, x: -24, duration: b.len, ease: "none" }, b.start);
62
+ return box;
63
+ };
64
+ const logoFor = () => (light && F.logoOnLight) || F.logo;
65
+ const items = (b, parent, line) => {
66
+ if (!b.items) return;
67
+ const box = $({ marginTop: "34px" }, "", parent);
68
+ b.items.forEach((it, i) => {
69
+ const e = $({ fontFamily: `"${FD.display}"`, fontStyle: italic ? "italic" : "normal", fontSize: "40px", color: P.accent, marginTop: "10px" }, it, box);
70
+ enter(b, e, lead + 0.67 + ((line - 0.67) * i) / b.items.length);
71
+ });
72
+ };
73
+
74
+ const KINDS = {
75
+ black(b, v, line) {
76
+ const c = $({ ...FILL, justifyContent: "center", alignItems: "center", textAlign: "center", padding: pad + "px" }, "", v);
77
+ title(b, b.title, lead, portrait ? 96 : 84, { italic, center: true }, c);
78
+ if (b.sub) { const s = $(body(34), b.sub, c); s.style.marginTop = "30px"; enter(b, s, lead + line * 0.55, "center"); }
79
+ },
80
+ question(b, v) {
81
+ const c = $({ ...FILL, justifyContent: "center", alignItems: "center", textAlign: "center", padding: pad * 1.5 + "px" }, "", v);
82
+ title(b, b.title, lead, portrait ? 92 : 88, { italic, center: true, color: P.bg }, c);
83
+ },
84
+ reveal(b, v, line) {
85
+ const c = $({ ...FILL, justifyContent: "center", alignItems: "center", textAlign: "center", padding: pad + "px" }, "", v);
86
+ const logo = logoFor();
87
+ if (logo) {
88
+ const im = document.createElement("img"); im.src = logo; Object.assign(im.style, { width: (portrait ? 760 : 820) + "px" }); c.append(im);
89
+ tl.fromTo(im, { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.8, ease: "power2.out" }, b.start + lead);
90
+ tl.fromTo(im, { scale: 1.04 }, { scale: 1, duration: b.len - lead, ease: "none" }, b.start + lead);
91
+ } else {
92
+ const t = title(b, b.title, lead, 170, { center: true }, c);
93
+ t.style.maxWidth = W - pad * 2 + "px"; t.style.overflowWrap = "anywhere";
94
+ if (t.scrollWidth > W - pad * 2 || b.title.length * 170 * 0.55 > W - pad * 2) t.style.fontSize = Math.floor(Math.min(170, (W - pad * 2) / (b.title.length * 0.58))) + "px";
95
+ }
96
+ if (b.sub) { const s = title(b, b.sub, lead + 1.0, 46, { italic, center: true }, c); s.style.marginTop = "40px"; }
97
+ if (b.line) { const k = kick(b, b.line, lead + f(50), c); k.style.marginTop = "34px"; }
98
+ },
99
+ stat(b, v, line) {
100
+ const c = $({ ...FILL, padding: pad + "px", justifyContent: "center" }, "", v);
101
+ if (b.kick) kick(b, b.kick, lead, c);
102
+ const row = $({ display: "flex", flexDirection: portrait ? "column" : "row", gap: (short ? 36 : 70) + "px", marginTop: "40px" }, "", c);
103
+ const n = b.stat.length;
104
+ b.stat.forEach((st, i) => {
105
+ const at = lead + (line * i) / n, cell = $({}, "", row);
106
+ const num = $({ fontFamily: `"${FD.display}"`, fontSize: (short ? 112 : 150) + "px", color: P.accent, lineHeight: 1 }, "", cell);
107
+ const show = x => (num.textContent = Math.round(x).toLocaleString(F.language === "id" ? "id-ID" : "en-US") + (st.suffix || ""));
108
+ show(0);
109
+ const o = { x: 0 };
110
+ tl.to(o, { x: st.value, duration: 40 / 30, ease: "none", onUpdate: () => show(o.x) }, b.start + at);
111
+ $(body(30, P.muted, { marginTop: "12px", maxWidth: "560px" }), st.label, cell);
112
+ enter(b, cell, at);
113
+ });
114
+ if (b.title) { const t = title(b, b.title, lead + line * 0.7, 52, { italic }, c); t.style.marginTop = "50px"; }
115
+ const src = b.stat.map(s => s.source).filter(Boolean);
116
+ if (src.length) $({ position: "absolute", left: pad + "px", bottom: (kinetic ? (short ? 400 : portrait ? 520 : 260) : short ? 290 : portrait ? 330 : 70) + "px", ...body(18, P.muted, { opacity: 0.8 }) },
117
+ `${F.labels.source}: ${src.join(" · ")}`, c);
118
+ },
119
+ triptych(b, v, line) {
120
+ const c = $({ ...FILL, padding: pad + "px", justifyContent: "center" }, "", v);
121
+ const cw = portrait ? 270 : 420, ch = portrait ? 420 : 520;
122
+ if (b.kick) kick(b, b.kick, lead, c);
123
+ if (b.title) title(b, b.title, lead + 0.13, 60, {}, c).style.marginTop = "18px";
124
+ const row = $({ display: "flex", gap: "36px", marginTop: "50px" }, "", c);
125
+ b.panels.forEach((pn, i) => {
126
+ const cell = $({}, "", row);
127
+ const fr = $({ width: cw + "px", height: ch + "px", overflow: "hidden", border: `2px solid ${P.accent}` }, `<img src="${pn.img}" data-layout-allow-overflow style="width:100%;height:100%;object-fit:cover">`, cell);
128
+ $({ fontFamily: `"${FD.display}"`, fontSize: "34px", color: P.fg, marginTop: "16px", maxWidth: cw + "px" }, pn.title, cell);
129
+ $(body(22, P.muted, { marginTop: "6px", maxWidth: cw + "px" }), pn.sub || "", cell);
130
+ enter(b, cell, lead + (line * (i + 0.3)) / 3);
131
+ });
132
+ },
133
+ shot(b, v) {
134
+ const c = $({ ...FILL, padding: pad + "px", flexDirection: portrait ? "column" : "row", alignItems: "center", justifyContent: portrait ? "center" : "space-between", gap: "50px" }, "", v);
135
+ textBlock(b, c, portrait ? 900 : 560, 72);
136
+ // fit the screenshot inside its box, whatever its ratio (phone screenshots are tall)
137
+ const r = b.shotRatio || 1.6, mw = portrait ? 880 : 1000, mh = portrait ? 800 : 780, w = Math.min(mw, mh * r), h = w / r;
138
+ const fr = $({ padding: "10px", background: "#F3EDE3", border: `2px solid ${P.accent}` }, `<img src="${b.img}" data-layout-allow-overflow style="width:${w}px;height:${h}px;display:block">`, c);
139
+ enter(b, fr, lead + 0.33);
140
+ tl.fromTo(fr, { scale: 1 }, { scale: 1.04, duration: b.len, ease: "none" }, b.start);
141
+ },
142
+ cta(b, v) {
143
+ const c = F.cta;
144
+ if (b.img) { const g = $({ ...FILL, opacity: 0.35 }, "", v); photo(b, g, S.kenburns); }
145
+ const col = $({ ...FILL, padding: pad + "px", justifyContent: "center", alignItems: portrait ? "center" : "flex-start", textAlign: portrait ? "center" : "left" }, "", v);
146
+ kick(b, c.kick || F.labels.next, lead, col);
147
+ title(b, c.title, lead + 0.2, portrait ? 96 : 86, { center: portrait }, col).style.marginTop = "24px";
148
+ if (c.sub) kick(b, c.sub, lead + 1.0, col).style.marginTop = "40px";
149
+ (c.lines || []).forEach((l, i) => { const e = $(body(38, P.fg, { marginTop: "12px" }), l, col); enter(b, e, lead + 1.2 + i * 0.2); });
150
+ if (c.url) { const e = $(body(34, P.muted, { marginTop: "12px" }), c.url, col); enter(b, e, lead + 1.6); }
151
+ const qr = () => { const e = $({}, `<img src="${F.qr}" style="width:${portrait ? 360 : 330}px;height:${portrait ? 360 : 330}px;border-radius:8px;display:block">`); return e; };
152
+ if (portrait && F.qr) { const e = qr(); col.append(e); e.style.marginTop = "50px"; enter(b, e, lead + 1.8, "center"); }
153
+ const logo = logoFor();
154
+ if (logo) { const e = $({}, `<img src="${logo}" style="width:360px;display:block">`, col); e.style.marginTop = "60px"; enter(b, e, lead + 2.1, "center"); }
155
+ if (!portrait && F.qr) { const e = qr(); Object.assign(e.style, { position: "absolute", right: pad + "px", bottom: "150px" }); v.append(e); enter(b, e, lead + 1.8); }
156
+ },
157
+ media(b, v, line) { // photo | footage | list: the layout depends on the style
158
+ const slot = (parent, style) => {
159
+ const box = $({ overflow: "hidden", ...style }, "", parent);
160
+ if (b.kind === "footage" && b.video) {
161
+ const vid = document.createElement("video"); vid.id = "fv-" + b.n; vid.muted = true; vid.setAttribute("muted", ""); vid.playsInline = true; vid.src = b.video;
162
+ vid.dataset.start = b.start; vid.dataset.duration = b.len; vid.dataset.trackIndex = 8;
163
+ Object.assign(vid.style, { width: "100%", height: "100%", objectFit: "cover", display: "block" }); box.append(vid);
164
+ } else if (b.img) { Object.assign(box.style, { display: "flex", flexDirection: "column" }); photo(b, box, S.kenburns, { position: "relative", flex: 1 }); }
165
+ return box;
166
+ };
167
+ if (S.layout === "window" && !portrait) {
168
+ const c = $({ ...FILL, flexDirection: "row" }, "", v);
169
+ const left = $({ width: "52%", padding: `0 ${pad}px`, display: "flex", flexDirection: "column", justifyContent: "center" }, "", c);
170
+ textBlock(b, left, 820, 76); items(b, left, line);
171
+ const win = slot(c, { width: "40%", margin: "110px 0", border: `2px solid ${P.accent}`, position: "relative" });
172
+ tl.fromTo(win, { clipPath: "inset(50% 0 50% 0)" }, { clipPath: "inset(0% 0 0% 0)", duration: 0.6, ease: "power2.out" }, b.start + lead);
173
+ } else if (S.layout === "card" && !portrait) {
174
+ const c = $({ ...FILL, flexDirection: "row", alignItems: "center" }, "", v);
175
+ const left = $({ width: "50%", padding: `0 ${pad}px` }, "", c);
176
+ textBlock(b, left, 780, 80); items(b, left, line);
177
+ const card = slot(c, { width: "760px", height: "760px", borderRadius: "40px", boxShadow: "0 30px 80px rgba(0,0,0,0.25)", position: "relative" });
178
+ tl.fromTo(card, { rotation: 10, scale: 0.8 }, { rotation: 2, scale: 1, duration: 0.6, ease: "back.out(2)" }, b.start + lead);
179
+ } else {
180
+ const top = portrait && S.captions === "kinetic", bottom = !top && (portrait || b.side !== "left");
181
+ slot(v, { ...FILL, position: "absolute" });
182
+ const g = top ? `linear-gradient(to bottom, ${P.bg}E6 0%, transparent 40%, transparent 55%, ${P.bg}CC 100%)`
183
+ : bottom ? `linear-gradient(to top, ${P.bg}F0 0%, ${P.bg}99 35%, transparent 65%)`
184
+ : `linear-gradient(to right, ${P.bg}F2 0%, ${P.bg}AA 40%, transparent 70%)`;
185
+ $({ ...FILL, background: g }, "", v);
186
+ const c = $({ ...FILL, padding: top ? `200px ${pad}px 0` : bottom ? `0 ${pad}px ${portrait ? 420 : kinetic ? 260 : 150}px` : `0 ${pad}px`,
187
+ justifyContent: top ? "flex-start" : bottom ? "flex-end" : "center" }, "", v);
188
+ textBlock(b, c, bottom ? 1400 : 900, portrait ? 92 : bottom ? 92 : 80); items(b, c, line);
189
+ }
190
+ },
191
+ };
192
+
193
+ // Beats: wrapper with the style's transition, then the kind's content.
194
+ F.beats.forEach((b, i) => {
195
+ const line = b.dur || b.len, end = b.start + b.len, T = S.transition;
196
+ const v = $({ ...FILL, opacity: 0, background: b.kind === "question" ? P.accentBg : "transparent" }, "", document.getElementById("beats"));
197
+ (KINDS[b.kind] || KINDS.media)(b, v, line);
198
+ const out = { opacity: 0, duration: f(10), ease: "none" };
199
+ if (T === "wipe") tl.fromTo(v, { opacity: 1, clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: f(14), ease: "power2.out" }, b.start);
200
+ else if (T === "punch") tl.fromTo(v, { opacity: 0, scale: 1.12 }, { opacity: 1, scale: 1, duration: f(8), ease: "power2.out" }, b.start);
201
+ else tl.fromTo(v, { opacity: 0 }, { opacity: 1, duration: f(10), ease: "none" }, b.start);
202
+ if (b.len > f(20)) tl.to(v, out, end - f(10));
203
+ });
204
+
205
+ // Grade: vignette, light leak on every cut, drifting grain.
206
+ const G = S.grade, grade = document.getElementById("grade");
207
+ if (G.vignette > 0) $({ ...FILL, background: `radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,${G.vignette}) 100%)` }, "", grade);
208
+ if (G.leak) F.beats.forEach((b, i) => {
209
+ const x = 20 + ((i * 37) % 60), e = $({ ...FILL, opacity: 0, mixBlendMode: "screen", background: `radial-gradient(circle at ${x}% 30%, rgba(255,190,120,0.55) 0%, transparent 55%)` }, "", grade);
210
+ tl.fromTo(e, { opacity: 0 }, { opacity: 1, duration: f(8), ease: "none" }, b.start).to(e, { opacity: 0, duration: f(18), ease: "none" }, b.start + f(8));
211
+ });
212
+ if (G.grain > 0) {
213
+ const g = $({ position: "absolute", inset: "-60px", opacity: G.grain, mixBlendMode: "overlay" },
214
+ `<svg width="${W + 120}" height="${H + 120}"><filter id="g"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7"/></filter><rect width="100%" height="100%" filter="url(#g)"/></svg>`, grade);
215
+ for (let k = 0; k < F.total * 8; k++) tl.set(g, { x: (k * 53) % 60 - 30, y: (k * 29) % 60 - 30 }, k / 8);
216
+ }
217
+
218
+ // Captions from the voice's word timings (make_film.py cut them into groups).
219
+ const cap = document.getElementById("captions");
220
+ F.captions.forEach(c => {
221
+ const subtitle = S.captions === "subtitle";
222
+ const box = $({ ...FILL, opacity: 0, justifyContent: "flex-end", alignItems: "center", pointerEvents: "none",
223
+ paddingBottom: (subtitle ? (portrait ? 220 : 56) : (portrait ? 300 : 120)) + "px" }, "", cap);
224
+ let inner;
225
+ if (subtitle) {
226
+ inner = $({ ...body(portrait ? 40 : 32, light ? P.fg : "#FFFFFF"), textAlign: "center", background: light ? "rgba(255,255,255,0.75)" : "rgba(0,0,0,0.45)",
227
+ padding: "8px 18px", borderRadius: "6px", maxWidth: W - 120 + "px" }, c.words.map(w => w.w).join(" "), box);
228
+ } else {
229
+ inner = $({ display: "flex", flexWrap: "wrap", justifyContent: "center", maxWidth: (portrait ? 940 : 1500) + "px" }, "", box);
230
+ c.words.forEach(w => {
231
+ const s = document.createElement("span"); s.textContent = w.w;
232
+ Object.assign(s.style, { fontFamily: '"Inter"', fontWeight: 900, fontSize: (portrait ? 84 : 72) + "px", lineHeight: 1.15, textTransform: "uppercase", color: "#FFFFFF",
233
+ margin: "0 10px", webkitTextStroke: "3px #000", paintOrder: "stroke", textShadow: "0 6px 18px rgba(0,0,0,0.6)" });
234
+ inner.append(s);
235
+ tl.set(s, { color: P.accent }, w.s).set(s, { color: "#FFFFFF" }, w.e);
236
+ });
237
+ tl.fromTo(inner, { scale: 0.85 }, { scale: 1, duration: f(10), ease: "back.out(2)" }, c.s);
238
+ }
239
+ tl.set(box, { opacity: 1 }, c.s).set(box, { opacity: 0 }, c.e);
240
+ });
241
+
242
+ document.getElementById("root").style.background = P.bg;
243
+ window.__timelines["main"] = tl;
244
+ </script>
245
+ </body>
246
+ </html>
@@ -0,0 +1 @@
1
+ Fonts: "Playfair Display" by Claus Eggers Sørensen, "Inter" by Rasmus Andersson (SIL Open Font License 1.1 (Google Fonts)).
@@ -0,0 +1,2 @@
1
+ # film-cinematic-drama
2
+ Pitch opener, brand film. Dark, full-bleed photos with a slow push-in, rise text, grain and vignette, fades. Quiet music, a hit on the reveal, a riser into the question. Subtitle-bar captions, -16 LUFS. Script: full story arc, 70-110 s. Fields: see `style.json` and `references/presets.md` (Film presets).
@@ -0,0 +1,39 @@
1
+ {
2
+ "palette": {
3
+ "bg": "#14110F",
4
+ "fg": "#F3EDE3",
5
+ "muted": "#B8AFA3",
6
+ "accent": "#C9A24E",
7
+ "accentBg": "#C9A96E"
8
+ },
9
+ "fonts": {
10
+ "display": "Playfair Display",
11
+ "body": "Inter"
12
+ },
13
+ "pace": {
14
+ "lead": 0.8,
15
+ "tail": 1.1
16
+ },
17
+ "text": "rise",
18
+ "transition": "fade",
19
+ "layout": "fullbleed",
20
+ "grade": {
21
+ "grain": 0.07,
22
+ "vignette": 0.55,
23
+ "leak": false
24
+ },
25
+ "captions": "subtitle",
26
+ "music": {
27
+ "base": 0.2,
28
+ "lift": 0.32,
29
+ "track": "music.mp3"
30
+ },
31
+ "sfx": {
32
+ "cut": "",
33
+ "text": "",
34
+ "reveal": "hit",
35
+ "question": "riser"
36
+ },
37
+ "kenburns": 0.08,
38
+ "loudness": -16
39
+ }
@@ -0,0 +1 @@
1
+ Fonts: "Fraunces" by Undercase Type, "Inter" by Rasmus Andersson (SIL Open Font License 1.1 (Google Fonts)).
@@ -0,0 +1,2 @@
1
+ # film-fun-energetic
2
+ Social posts, events. Cream background, rounded photo cards that bounce in (16:9), pop text, light leaks, pops and a chime. Subtitle-bar captions, -14 LUFS. Script: 35-50 s, playful lines. Note: the accent orange on cream is below 3:1 contrast; `hyperframes check` warns, change `brand.palette.accent` for stricter needs.
@@ -0,0 +1,39 @@
1
+ {
2
+ "palette": {
3
+ "bg": "#FFF6E8",
4
+ "fg": "#1E1A17",
5
+ "muted": "#5A524A",
6
+ "accent": "#E8833A",
7
+ "accentBg": "#F4C95D"
8
+ },
9
+ "fonts": {
10
+ "display": "Fraunces",
11
+ "body": "Inter"
12
+ },
13
+ "pace": {
14
+ "lead": 0.2,
15
+ "tail": 0.35
16
+ },
17
+ "text": "pop",
18
+ "transition": "punch",
19
+ "layout": "card",
20
+ "grade": {
21
+ "grain": 0.0,
22
+ "vignette": 0.0,
23
+ "leak": true
24
+ },
25
+ "captions": "subtitle",
26
+ "music": {
27
+ "base": 0.38,
28
+ "lift": 0.5,
29
+ "track": "music.mp3"
30
+ },
31
+ "sfx": {
32
+ "cut": "pop",
33
+ "text": "pop",
34
+ "reveal": "chime",
35
+ "question": "pop"
36
+ },
37
+ "kenburns": 0.1,
38
+ "loudness": -14
39
+ }
@@ -0,0 +1 @@
1
+ Fonts: "Playfair Display" by Claus Eggers Sørensen, "Inter" by Rasmus Andersson (SIL Open Font License 1.1 (Google Fonts)).
@@ -0,0 +1,2 @@
1
+ # film-promo-booth
2
+ Exhibition loop, deck-faithful promo. Photo in a framed window beside the text (16:9; full-bleed in 9:16), wipes, slide-in text, staged builds, whoosh on cuts, tick on titles. Subtitle-bar captions, -16 LUFS. Script: the deck's beats in order, 80-100 s.
@@ -0,0 +1,39 @@
1
+ {
2
+ "palette": {
3
+ "bg": "#14110F",
4
+ "fg": "#F3EDE3",
5
+ "muted": "#B8AFA3",
6
+ "accent": "#C9A24E",
7
+ "accentBg": "#C9A96E"
8
+ },
9
+ "fonts": {
10
+ "display": "Playfair Display",
11
+ "body": "Inter"
12
+ },
13
+ "pace": {
14
+ "lead": 0.45,
15
+ "tail": 0.7
16
+ },
17
+ "text": "slide",
18
+ "transition": "wipe",
19
+ "layout": "window",
20
+ "grade": {
21
+ "grain": 0.04,
22
+ "vignette": 0.35,
23
+ "leak": false
24
+ },
25
+ "captions": "subtitle",
26
+ "music": {
27
+ "base": 0.28,
28
+ "lift": 0.4,
29
+ "track": "music.mp3"
30
+ },
31
+ "sfx": {
32
+ "cut": "whoosh",
33
+ "text": "tick",
34
+ "reveal": "hit",
35
+ "question": "riser"
36
+ },
37
+ "kenburns": 0.05,
38
+ "loudness": -16
39
+ }
@@ -0,0 +1 @@
1
+ Fonts: "Inter" by Rasmus Andersson (SIL Open Font License 1.1 (Google Fonts)).
@@ -0,0 +1,2 @@
1
+ # film-viral-social
2
+ TikTok / Reels / Shorts. Full-bleed, fast punch-in cuts, bold Inter, strong Ken Burns, kinetic 2-3 word captions with the spoken word in the accent colour, whooshes. -14 LUFS. Script: 30 s or less, line 1 is the hook.
@@ -0,0 +1,39 @@
1
+ {
2
+ "palette": {
3
+ "bg": "#0E0C0B",
4
+ "fg": "#FFFFFF",
5
+ "muted": "#D6CEC4",
6
+ "accent": "#F2C14E",
7
+ "accentBg": "#F2C14E"
8
+ },
9
+ "fonts": {
10
+ "display": "Inter",
11
+ "body": "Inter"
12
+ },
13
+ "pace": {
14
+ "lead": 0.05,
15
+ "tail": 0.2
16
+ },
17
+ "text": "pop",
18
+ "transition": "punch",
19
+ "layout": "fullbleed",
20
+ "grade": {
21
+ "grain": 0.03,
22
+ "vignette": 0.45,
23
+ "leak": false
24
+ },
25
+ "captions": "kinetic",
26
+ "music": {
27
+ "base": 0.3,
28
+ "lift": 0.42,
29
+ "track": "music.mp3"
30
+ },
31
+ "sfx": {
32
+ "cut": "whoosh",
33
+ "text": "",
34
+ "reveal": "hit",
35
+ "question": "riser"
36
+ },
37
+ "kenburns": 0.14,
38
+ "loudness": -14
39
+ }
@@ -0,0 +1,3 @@
1
+ Sound effects: synthesized with ffmpeg for this preset, CC0 (shutter, whoosh, pop, tick, ding, stamp, reveal, boing, ending).
2
+
3
+ Fonts: "Montserrat" by Julieta Ulanovsky et al. (Google Fonts, SIL Open Font License 1.1); "Instrument Serif" by The Instrument Serif Project Authors (Google Fonts, SIL Open Font License 1.1); "Space Mono" by The Space Mono Project Authors (Google Fonts, SIL Open Font License 1.1).
@@ -0,0 +1,52 @@
1
+ # TikTok explainer preset
2
+
3
+ A 1080×1920 HyperFrames style for talking-head tips and how-to explainers. It was modeled on a short motion-graphics explainer: a near-black editor look in which the speaker plays inside a clip card, carried by big headlines.
4
+
5
+ ## Look and motion
6
+
7
+ - **Palette:** near-black `#0c0b0a` with two soft diagonal light streaks that drift, cream `#efe6d6` text, orange `#f7a93b` accent, and red `#ef4b3a` for stamps.
8
+ - **Type:** Montserrat 900 headlines with tight tracking, small Instrument Serif setup lines above or below them, and Space Mono for timecodes and chips. A trailing `?`, `!` or `.` on any headline turns orange.
9
+ - **Clip card:** the footage sits in a rounded, cream-framed card (700×930, cropped to the upper face) with a running `● 0:07` timecode chip. It tilts ±1.6° and alternates scale at every jump cut, with a slow push inside each segment.
10
+ - **Hook:** `line1` is the serif setup line, `line2` the huge headline, and `sub` an orange serif line under it. It holds for 3 s above the card.
11
+ - **Captions:** kinetic. Each word pops in as it is spoken, in lowercase Montserrat 900, orange while spoken and cream afterwards. Unspoken words are not shown yet. Captions sit under the card and are hidden while a scene owns the screen (photo cards keep them).
12
+ - **Scenes** (beats):
13
+ - `title`: the card tucks to the top right. With `big` (for example `"1"`), a giant outlined numeral fills up in orange behind it, and the text sits bottom left; without it, the text is centered. Keys: `kicker` (serif, above), `title` (headline), `sub` (orange serif, below), optional `img`, and `dur` (default 1.8 s).
14
+ - `case`: a banner scene. `kicker` (serif) and `sub` (bold headline) sit on top, the card greys out, an orange tape with `title` slaps across it, and an optional red `stamp` (for example `"ZONK"`) slams on with the stamp sound. Optional `note` is a serif line at the bottom. Lasts 3.2 s.
15
+ - `photo`: the clip card is swept off to the left and an image card slides in from the right, with an optional mono `label` chip. Lasts 2.8 s.
16
+ - `item`: a steps list. Each row is an orange mono chip (`label`, default `01`, `02`, …) plus a bold line (`text`). The first item may carry `head`, an orange serif header. The card tucks aside while the list shows. The list leaves at the first jump cut after its last item, at most 6 s later.
17
+ - `end`: the card fades out. `title` (use `\n` for line breaks) stacks in, a giant orange glyph (`big`, default `?`) bounces in on the ending sting and wiggles, and `sub` (for example "jawab di komen ↓") fades in. It holds through `tail`.
18
+ - `mascot`: optional. A corner character from the video's `images/` (`img`, for example `mascot.png`, a transparent PNG) pops in at its word and bobs until the end.
19
+
20
+ ## Audio
21
+
22
+ - **Music:** any shared track. `lights` at about 0.28 suits the reference's calm pace. It is carved around the voice like the other presets.
23
+ - **Voice:** the same chain as the other presets (rumble cut, compressor, gain, −1.5 dBTP ceiling). A test render landed at about −16.8 LUFS integrated with a −1.1 dBFS peak.
24
+ - **Sound effects** in `assets/sfx/`, all synthesized (CC0), with peaks around −7 to −9 dBFS:
25
+ - Automatic: `shutter` (a soft swipe on every `title`, `case` and `photo`), `stamp` (a thud under the stamp), `reveal` (a pop-and-chime on each `item`) and `ending` (a 1.2 s riser into a hit).
26
+ - Reactions: `pop`, `tick`, `ding`, `whoosh`, `boing`.
27
+
28
+ ## Config example
29
+
30
+ ```json
31
+ {
32
+ "preset": "tiktok-explainer",
33
+ "hook": { "line1": "klip lu dibuka pake", "line2": "BASA-BASI?", "sub": "momen serunya baru di tengah" },
34
+ "music": "lights", "music_volume": 0.28,
35
+ "beats": [
36
+ { "kind": "mascot", "word": "klip", "img": "mascot.png" },
37
+ { "kind": "title", "word": "detik", "big": "1", "kicker": "detik satu:", "title": "PUNCHLINE", "sub": "atau pertanyaan?", "dur": 2.6 },
38
+ { "kind": "case", "word": "janji", "kicker": "asal judulnya", "sub": "jangan janjiin yang gak ada.", "title": "GAK NYANGKA ENDINGNYA!!", "stamp": "ZONK" },
39
+ { "kind": "item", "word": "nyapa", "head": "yang lama:", "label": "0:01", "text": "nyapa dulu" },
40
+ { "kind": "end", "word": "terakhir", "title": "momen terbaiknya\ndetik ke", "big": "?", "sub": "jawab di komen ↓" }
41
+ ],
42
+ "tail": 2.5
43
+ }
44
+ ```
45
+
46
+ The other keys (`video`, `words`, `plan`, `fix`, `cold_open`, `reactions`, `crf`) work as in the other presets; see the skill's `examples/config.json`.
47
+
48
+ ## Notes
49
+
50
+ - Cards should not overlap. A `title` with a `big` numeral reads best at a `dur` of 2.4–3 s.
51
+ - Keep `title` and banner text short. Headlines shrink to fit 900 px, but anything past about 14 characters on one line gets small.
52
+ - `hyperframes check` warns about low contrast on the tape text at the moment the banner fades out. That is the fade, not the design.