kaizen-agent 1.47.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 (184) hide show
  1. package/README.md +111 -84
  2. package/cli/build-web.ts +4 -2
  3. package/cli/hermes.ts +65 -0
  4. package/cli/install.ts +24 -10
  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/cli/worktree.test.ts +3 -1
  15. package/package.json +1 -1
  16. package/skills/kaizen/config.default.yml +4 -0
  17. package/skills/media/kaizen-book/SKILL.md +107 -0
  18. package/skills/media/kaizen-book/assets/reader.html +440 -0
  19. package/skills/media/kaizen-book/assets/styles.json +45 -0
  20. package/skills/media/kaizen-book/examples/book.json +14 -0
  21. package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
  22. package/skills/media/kaizen-book/examples/manuscript.md +127 -0
  23. package/skills/media/kaizen-book/references/interview.md +85 -0
  24. package/skills/media/kaizen-book/references/proofread.md +47 -0
  25. package/skills/media/kaizen-book/references/writing.md +53 -0
  26. package/skills/media/kaizen-book/scripts/build_book.py +279 -0
  27. package/skills/media/kaizen-book/scripts/check.sh +10 -0
  28. package/skills/media/kaizen-book/scripts/check_book.py +128 -0
  29. package/skills/media/kaizen-book/scripts/deploy.py +58 -0
  30. package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
  31. package/skills/media/kaizen-book/scripts/setup.sh +9 -0
  32. package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
  33. package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
  34. package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
  35. package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
  36. package/skills/media/kaizen-video/SKILL.md +112 -0
  37. package/skills/media/kaizen-video/assets/CREDITS.txt +16 -0
  38. package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
  39. package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
  40. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
  41. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
  42. package/skills/media/kaizen-video/assets/fonts/Fredoka.ttf +0 -0
  43. package/skills/media/kaizen-video/assets/fonts/Montserrat.ttf +0 -0
  44. package/skills/media/kaizen-video/assets/fonts/SpecialElite.ttf +0 -0
  45. package/skills/media/kaizen-video/assets/music/investigations.mp3 +0 -0
  46. package/skills/media/kaizen-video/assets/music/lights.mp3 +0 -0
  47. package/skills/media/kaizen-video/assets/music/sunny.mp3 +0 -0
  48. package/skills/media/kaizen-video/examples/config.json +152 -0
  49. package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
  50. package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
  51. package/skills/media/kaizen-video/film.html +246 -0
  52. package/skills/media/kaizen-video/hyperframes.json +12 -0
  53. package/skills/media/kaizen-video/presets/educational-warning/CREDITS.txt +3 -0
  54. package/skills/media/kaizen-video/presets/educational-warning/README.md +26 -0
  55. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/ending.wav +0 -0
  56. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/reveal.wav +0 -0
  57. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/shutter.wav +0 -0
  58. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/stamp.wav +0 -0
  59. package/skills/media/kaizen-video/presets/educational-warning/index.html +236 -0
  60. package/skills/media/kaizen-video/presets/educational-warning/package.json +10 -0
  61. package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
  62. package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
  63. package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
  64. package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
  65. package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
  66. package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
  67. package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
  68. package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
  69. package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
  70. package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
  71. package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
  72. package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
  73. package/skills/media/kaizen-video/presets/tiktok-cheerful/CREDITS.txt +3 -0
  74. package/skills/media/kaizen-video/presets/tiktok-cheerful/README.md +35 -0
  75. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
  76. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
  77. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
  78. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
  79. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
  80. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
  81. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
  82. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
  83. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
  84. package/skills/media/kaizen-video/presets/tiktok-cheerful/index.html +260 -0
  85. package/skills/media/kaizen-video/presets/tiktok-cheerful/package.json +10 -0
  86. package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
  87. package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
  88. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
  89. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
  90. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
  91. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
  92. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
  93. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
  94. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
  95. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
  96. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
  97. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
  98. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
  99. package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
  100. package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
  101. package/skills/media/kaizen-video/presets/tiktok-mystery/CREDITS.txt +12 -0
  102. package/skills/media/kaizen-video/presets/tiktok-mystery/README.md +51 -0
  103. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
  104. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
  105. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
  106. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
  107. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
  108. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
  109. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
  110. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
  111. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
  112. package/skills/media/kaizen-video/presets/tiktok-mystery/index.html +260 -0
  113. package/skills/media/kaizen-video/presets/tiktok-mystery/package.json +10 -0
  114. package/skills/media/kaizen-video/presets/tiktok-scam-alert/CREDITS.txt +8 -0
  115. package/skills/media/kaizen-video/presets/tiktok-scam-alert/README.md +24 -0
  116. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
  117. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
  118. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
  119. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
  120. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
  121. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
  122. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
  123. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
  124. package/skills/media/kaizen-video/presets/tiktok-scam-alert/index.html +266 -0
  125. package/skills/media/kaizen-video/presets/tiktok-scam-alert/package.json +10 -0
  126. package/skills/media/kaizen-video/references/presets.md +76 -0
  127. package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
  128. package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
  129. package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
  130. package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
  131. package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
  132. package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
  133. package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
  134. package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
  135. package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
  136. package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
  137. package/skills/media/kaizen-video/scripts/check.sh +22 -0
  138. package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
  139. package/skills/media/kaizen-video/scripts/make_film.py +263 -0
  140. package/skills/media/kaizen-video/scripts/make_tiktok.py +213 -0
  141. package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
  142. package/skills/media/kaizen-video/scripts/render_cut.py +88 -0
  143. package/skills/media/kaizen-video/scripts/setup.sh +14 -0
  144. package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
  145. package/skills/media/kaizen-video/scripts/srt.py +9 -0
  146. package/skills/media/kaizen-video/scripts/still.sh +7 -0
  147. package/skills/media/kaizen-video/scripts/transcribe.py +45 -0
  148. package/skills/media/kaizen-video/tools/carve.mjs +560 -0
  149. package/skills/media/kaizen-video/tools/lib/main-module.mjs +13 -0
  150. package/web/board/views.ts +1 -1
  151. package/web/board.html +3 -1
  152. package/web/board.js +2 -2
  153. package/web/notebook.js +1 -1
  154. package/web/notes.html +2 -0
  155. package/web/term.js +38 -36
  156. package/web/term.ts +43 -10
  157. package/web/video.html +298 -0
  158. package/web/video.js +81 -0
  159. package/web/video.ts +610 -0
  160. package/skills/story-video/SKILL.md +0 -108
  161. package/skills/story-video/engine/package.json +0 -12
  162. package/skills/story-video/engine/src/Root.tsx +0 -24
  163. package/skills/story-video/engine/src/engine/Film.tsx +0 -405
  164. package/skills/story-video/engine/src/engine/presets.json +0 -58
  165. package/skills/story-video/engine/src/index.ts +0 -3
  166. package/skills/story-video/engine/tsconfig.json +0 -1
  167. package/skills/story-video/examples/sample-film/README.md +0 -12
  168. package/skills/story-video/examples/sample-film/film.json +0 -16
  169. package/skills/story-video/references/presets.md +0 -21
  170. package/skills/story-video/scripts/check.sh +0 -19
  171. package/skills/story-video/scripts/setup.sh +0 -25
  172. package/skills/story-video/scripts/srt.py +0 -30
  173. package/skills/story-video/scripts/still.sh +0 -10
  174. /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
  175. /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
  176. /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
  177. /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
  178. /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
  179. /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
  180. /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
  181. /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
  182. /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
  183. /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
  184. /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """Structure check for a manuscript: python3 check_book.py <book-dir> [--json]
3
+
4
+ Finds what a proofreader's eye skips: chapter numbering gaps, footnote markers without a note
5
+ (and notes nobody points to), empty chapters and headings, broken image paths, repeated words,
6
+ [[CHECK: …]] markers left while drafting, and leftovers from Word or copy-paste. Prints numbered findings as `line: [kind] message`.
7
+ Exit 1 when an error-level finding exists. Stdlib only.
8
+ """
9
+ import json, os, re, sys
10
+
11
+ NUM_WORDS = {w: i for i, w in enumerate(
12
+ "zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen "
13
+ "seventeen eighteen nineteen twenty".split())}
14
+ NUM_WORDS.update({w: i for i, w in enumerate(
15
+ "nol satu dua tiga empat lima enam tujuh delapan sembilan sepuluh sebelas".split())})
16
+ ROMAN = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100}
17
+
18
+
19
+ def roman(s):
20
+ if not s or not re.fullmatch(r"[IVXLC]+", s):
21
+ return None
22
+ n = 0
23
+ for a, b in zip(s, s[1:] + " "):
24
+ v = ROMAN[a]; n += -v if b != " " and ROMAN.get(b, 0) > v else v
25
+ return n
26
+
27
+
28
+ def chapter_no(title):
29
+ """Number in 'Chapter 3', 'Bab 12', 'BAB IV', '3. Title', 'Chapter Three'; None when unnumbered."""
30
+ m = re.match(r"^(?:chapter|bab|chapitre|kapitel|cap[ií]tulo|capitolo|fasal|fasl)\s+([\w]+)", title, re.I) or re.match(r"^(\d+)[.:)]?\s", title)
31
+ if not m:
32
+ return None
33
+ w = m[1]
34
+ return int(w) if w.isdigit() else roman(w.upper()) or NUM_WORDS.get(w.lower())
35
+
36
+
37
+ def check(book_dir):
38
+ cfg = json.load(open(os.path.join(book_dir, "book.json")))
39
+ path = os.path.join(book_dir, cfg.get("manuscript", "manuscript.md"))
40
+ lines = open(path, encoding="utf-8").read().split("\n")
41
+ F = []
42
+ add = lambda ln, level, kind, msg: F.append({"line": ln, "level": level, "kind": kind, "msg": msg})
43
+
44
+ chapters, cur, fence = [], None, False
45
+ for i, raw in enumerate(lines, 1):
46
+ s = raw.strip()
47
+ if s.startswith("```"):
48
+ fence = not fence
49
+ if fence:
50
+ continue
51
+ h = re.match(r"^(#{1,6})(\s*)(.*)$", raw)
52
+ if h and h[1]:
53
+ if not h[2] and h[3]:
54
+ add(i, "error", "heading", "'%s' needs a space after the #" % raw[:30])
55
+ if not h[3].strip():
56
+ add(i, "error", "heading", "empty heading")
57
+ if len(h[1]) == 2 and h[2]:
58
+ cur = {"line": i, "title": h[3].split(" | ")[0].strip(), "refs": [], "defs": {}, "words": 0}
59
+ chapters.append(cur)
60
+ if len(h[1]) == 1:
61
+ cur = None
62
+ continue
63
+ if cur is not None:
64
+ for m in re.finditer(r"\[\^([^\]]+)\](?!:)", raw):
65
+ cur["refs"].append((m[1], i))
66
+ d = re.match(r"^\[\^([^\]]+)\]:\s*(.*)", s)
67
+ if d:
68
+ if d[1] in cur["defs"]:
69
+ add(i, "error", "footnote", "note [^%s] defined twice in this chapter" % d[1])
70
+ cur["defs"][d[1]] = i
71
+ if not d[2].strip():
72
+ add(i, "error", "footnote", "note [^%s] is empty" % d[1])
73
+ elif s:
74
+ cur["words"] += len(s.split())
75
+ for m in re.finditer(r"!\[[^\]]*\]\(([^)\s]+)\)", raw):
76
+ if not re.match(r"^(https?:|data:)", m[1]) and not os.path.exists(os.path.join(book_dir, m[1])):
77
+ add(i, "error", "image", "image not found: %s" % m[1])
78
+ # leftovers from Word / copy-paste
79
+ for m in re.finditer(r"\[\[CHECK:([^\]]*)\]\]", raw):
80
+ add(i, "error", "check", "open question for the author:%s" % m[1])
81
+ if "\u00ad" in raw: add(i, "warn", "leftover", "soft hyphen (U+00AD) — invisible, breaks search")
82
+ if "\t" in raw and not raw.startswith(" "): add(i, "warn", "leftover", "tab character")
83
+ if re.search(r"\S {2,}\S", raw): add(i, "warn", "spacing", "double space inside a line")
84
+ if re.search(r"\s[,.;:!?](?!\.)", raw) and not re.search(r"\s\.\.\.", raw): add(i, "warn", "spacing", "space before punctuation")
85
+ if re.search(r"<(span|font|o:p|div)[\s>]", raw, re.I): add(i, "warn", "leftover", "HTML from a word processor: %s" % re.search(r"<\w+[^>]*>", raw)[0][:40])
86
+ if re.search(r"Error! (Bookmark|Reference)|MERGEFORMAT|\{\s*(PAGEREF|REF|TOC)\b", raw): add(i, "error", "leftover", "Word field code")
87
+ if s.count("**") % 2 and not re.fullmatch(r"[*\s]+", s): add(i, "warn", "format", "unbalanced ** (bold not closed)")
88
+ if re.search(r"[“”]", raw) and re.search(r'(?<![=\w])"', raw): add(i, "warn", "quotes", "straight and curly quotes mixed")
89
+ for m in re.finditer(r"\b(\w{2,})\s+\1\b", raw, re.I):
90
+ if m[1].lower() not in ("had", "that", "very", "is", "bye", "ha", "no", "so"): add(i, "warn", "repeat", "repeated word: '%s'" % m[0])
91
+
92
+ if not chapters:
93
+ add(1, "error", "structure", "no chapters: chapters start with '## '")
94
+ nums = [(c, chapter_no(c["title"])) for c in chapters]
95
+ numbered = [(c, n) for c, n in nums if n is not None]
96
+ for (a, na), (b, nb) in zip(numbered, numbered[1:]):
97
+ if nb != na + 1:
98
+ add(b["line"], "error", "numbering", "'%s' follows '%s' (expected %d)" % (b["title"], a["title"], na + 1))
99
+ titles = {}
100
+ for c in chapters:
101
+ if c["title"].lower() in titles:
102
+ add(c["line"], "warn", "structure", "chapter title repeats line %d" % titles[c["title"].lower()])
103
+ titles.setdefault(c["title"].lower(), c["line"])
104
+ if c["words"] == 0:
105
+ add(c["line"], "error", "structure", "chapter '%s' has no text" % c["title"])
106
+ refs = [r for r, _ in c["refs"]]
107
+ for r, ln in c["refs"]:
108
+ if r not in c["defs"]:
109
+ add(ln, "error", "footnote", "marker [^%s] has no note in this chapter" % r)
110
+ for d, ln in c["defs"].items():
111
+ if d not in refs:
112
+ add(ln, "warn", "footnote", "note [^%s] is never referenced" % d)
113
+ F.sort(key=lambda f: f["line"])
114
+ return F, len(chapters)
115
+
116
+
117
+ if __name__ == "__main__":
118
+ if len(sys.argv) < 2:
119
+ sys.exit(__doc__)
120
+ findings, n = check(sys.argv[1])
121
+ if "--json" in sys.argv:
122
+ print(json.dumps(findings, ensure_ascii=False, indent=1))
123
+ else:
124
+ for k, f in enumerate(findings, 1):
125
+ print("%3d. line %d: [%s %s] %s" % (k, f["line"], f["level"], f["kind"], f["msg"]))
126
+ e = sum(f["level"] == "error" for f in findings)
127
+ print("%d chapters; %d errors, %d warnings" % (n, e, len(findings) - e))
128
+ sys.exit(1 if any(f["level"] == "error" for f in findings) else 0)
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ """Assemble a static site for the book: python3 deploy.py <book-dir> [--out output]
3
+
4
+ Writes <out>/deploy/: index.html (small page with link-preview tags that then loads the book),
5
+ reader.html (the book), the PDFs and og.jpg. Publish the folder with any static host,
6
+ e.g. `cd output/deploy && npx vercel --prod`.
7
+
8
+ Why the loader: WhatsApp and other link previewers give up on a multi-MB page before reading
9
+ its <head>, so the first response must be tiny and carry the og: tags itself.
10
+ """
11
+ import html, json, os, shutil, sys
12
+
13
+ args = sys.argv[1:]
14
+ if not args or args[0].startswith("-"):
15
+ sys.exit(__doc__)
16
+ cfg = json.load(open(os.path.join(args[0], "book.json")))
17
+ out = args[args.index("--out") + 1] if "--out" in args else "output"
18
+ src, dst = out, os.path.join(out, "deploy")
19
+ if not os.path.exists(os.path.join(src, "book.html")):
20
+ sys.exit("error: %s/book.html missing; run build_book.py first" % src)
21
+ os.makedirs(dst, exist_ok=True)
22
+ shutil.copy(os.path.join(src, "book.html"), os.path.join(dst, "reader.html"))
23
+ copied = ["reader.html"]
24
+ for f in sorted(os.listdir(src)):
25
+ if f.endswith(".pdf") or f == "og.jpg":
26
+ shutil.copy(os.path.join(src, f), os.path.join(dst, f)); copied.append(f)
27
+
28
+ e = lambda s: html.escape(s or "", quote=True)
29
+ url = (cfg.get("url") or "").rstrip("/") + "/" if cfg.get("url") else ""
30
+ title = cfg["title"] + (" — " + cfg["subtitle"] if cfg.get("subtitle") else "")
31
+ desc = cfg.get("description") or cfg.get("tagline") or ""
32
+ og_img = ('\n<meta property="og:image" content="%sog.jpg">\n<meta property="og:image:width" content="1200">\n'
33
+ '<meta property="og:image:height" content="630">\n<meta name="twitter:image" content="%sog.jpg">' % (e(url), e(url))) if url else ""
34
+ if not url:
35
+ print("warning: book.json has no \"url\"; link previews need an absolute og:image URL", file=sys.stderr)
36
+ page = """<!DOCTYPE html>
37
+ <html lang="%(lang)s"><head>
38
+ <meta charset="utf-8">
39
+ <title>%(title)s</title>
40
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
41
+ <meta name="description" content="%(desc)s">
42
+ <meta property="og:type" content="book">
43
+ <meta property="og:title" content="%(title)s">
44
+ <meta property="og:description" content="%(desc)s">
45
+ <meta property="og:url" content="%(url)s">%(img)s
46
+ <meta name="twitter:card" content="summary_large_image">
47
+ </head><body style="margin:0;background:#111">
48
+ <noscript><a href="reader.html" style="color:#ddd">%(title)s</a></noscript>
49
+ <script>
50
+ // Small first response so link previews read the tags above; the book loads after.
51
+ fetch('reader.html').then(function (r) { return r.text(); }).then(function (h) {
52
+ document.open(); document.write(h); document.close();
53
+ }).catch(function () { location.replace('reader.html'); });
54
+ </script>
55
+ </body></html>
56
+ """ % {"lang": e(cfg.get("lang", "en")), "title": e(title), "desc": e(desc), "url": e(url), "img": og_img}
57
+ open(os.path.join(dst, "index.html"), "w").write(page)
58
+ print("ok %s: index.html, %s" % (dst, ", ".join(copied)))
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // Print book.html to PDFs with headless Chromium.
3
+ // Usage: node pdf.mjs <output/book.html> [--only color,mono,volumes,og]
4
+ // Writes next to book.html: book.pdf (A5 colour), book-mono.pdf (black and white),
5
+ // book-vol-N.pdf (one per volumes[] entry in book.json; black and white, or colour with "volume_print": "color") and og.jpg (1200×630 link preview).
6
+ // Needs poppler's pdfinfo to put page numbers in the contents.
7
+ import { createRequire } from "node:module";
8
+ import { execFileSync } from "node:child_process";
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+
13
+ const home = process.env.KAIZEN_BOOK_HOME || path.join(os.homedir(), ".kaizen", "kaizen-book");
14
+ let chromium;
15
+ try { ({ chromium } = createRequire(path.join(home, "package.json"))("playwright")); }
16
+ catch { try { ({ chromium } = await import("playwright")); } catch { console.error("playwright missing: run scripts/setup.sh"); process.exit(1); } }
17
+
18
+ const [file, ...rest] = process.argv.slice(2);
19
+ if (!file) { console.error("usage: node pdf.mjs <output/book.html> [--only color,mono,volumes,og]"); process.exit(1); }
20
+ const only = rest.includes("--only") ? rest[rest.indexOf("--only") + 1].split(",") : ["color", "mono", "volumes", "og"];
21
+ const abs = path.resolve(file), dir = path.dirname(abs), url = "file://" + abs;
22
+ const errors = [];
23
+ const browser = await chromium.launch();
24
+
25
+ async function open(hash, viewport = { width: 600, height: 850 }) {
26
+ const p = await browser.newPage({ viewport });
27
+ p.on("pageerror", e => errors.push(e.message));
28
+ await p.goto(url + "#" + hash);
29
+ await p.evaluate(() => window.__ready);
30
+ await p.emulateMedia({ media: "print" });
31
+ return p;
32
+ }
33
+ // Page of each #ch-N / #part-N, read from the PDF's named destinations (Chromium writes one per link target).
34
+ function dests(pdf) {
35
+ const map = {};
36
+ for (const line of execFileSync("pdfinfo", ["-dests", pdf], { encoding: "utf8" }).split("\n")) {
37
+ const m = line.match(/^\s*(\d+)\s.*"((?:ch|part)-\d+)"\s*$/);
38
+ if (m) map[m[2]] = +m[1];
39
+ }
40
+ return map;
41
+ }
42
+ const pages = pdf => +execFileSync("pdfinfo", [pdf], { encoding: "utf8" }).match(/Pages:\s+(\d+)/)[1];
43
+
44
+ // Two passes: lay out, read where each chapter landed, write the numbers into the contents, print again.
45
+ async function printBook(hash, out, ranges = "") {
46
+ const p = await open(hash);
47
+ const tmp = out + ".tmp.pdf";
48
+ await p.pdf({ path: tmp, preferCSSPageSize: true, printBackground: true });
49
+ const map = dests(tmp);
50
+ await p.evaluate(m => window.__fillToc(m), map);
51
+ await p.pdf({ path: tmp, preferCSSPageSize: true, printBackground: true });
52
+ const check = dests(tmp);
53
+ if (JSON.stringify(check) !== JSON.stringify(map)) console.warn("warning: contents numbers shifted the layout in", path.basename(out));
54
+ if (ranges) await p.pdf({ path: out, preferCSSPageSize: true, printBackground: true, pageRanges: ranges });
55
+ else fs.renameSync(tmp, out);
56
+ fs.rmSync(tmp, { force: true });
57
+ await p.close();
58
+ return { map: check, total: pages(out) };
59
+ }
60
+
61
+ let colorRun, full;
62
+ if (only.includes("color")) { colorRun = await printBook("print", path.join(dir, "book.pdf")); console.log("book.pdf", colorRun.total, "pages"); }
63
+ if (only.includes("mono")) { full = await printBook("print&mono", path.join(dir, "book-mono.pdf")); console.log("book-mono.pdf", full.total, "pages"); }
64
+
65
+ if (only.includes("volumes")) {
66
+ const p = await open("print");
67
+ const vols = await p.evaluate(() => B.volumes || []), color = await p.evaluate(() => B.volume_print === "color");
68
+ const titled = await p.evaluate(() => B.parts.map((x, i) => x.title ? i : -1).filter(i => i >= 0));
69
+ await p.close();
70
+ if (vols.length) {
71
+ // page numbers come from the full book in the same colour mode the volumes are printed in
72
+ const base = color ? "print" : "print&mono";
73
+ const whole = color ? (colorRun ||= await printBook(base, path.join(dir, "book.pdf"))) : (full ||= await printBook(base, path.join(dir, "book-mono.pdf")));
74
+ // from_part is 1-based over parts with a title; a volume runs to the page before the next one starts.
75
+ const starts = vols.map((v, k) => {
76
+ if (k === 0) return 1;
77
+ const id = "part-" + titled[v.from_part - 1];
78
+ if (!whole.map[id]) throw new Error(`volume "${v.label}": part ${v.from_part} not found`);
79
+ return whole.map[id];
80
+ });
81
+ for (let k = 0; k < vols.length; k++) {
82
+ const end = k + 1 < vols.length ? starts[k + 1] - 1 : whole.total;
83
+ const ranges = k === 0 ? `1-${end}` : `1,${starts[k]}-${end}`; // later volumes reuse page 1 as their cover
84
+ const out = path.join(dir, `book-vol-${k + 1}.pdf`);
85
+ await printBook(base + "&vol=" + encodeURIComponent(vols[k].label), out, ranges);
86
+ console.log(path.basename(out), `${vols[k].label}: pages ${ranges}`, `(${pages(out)} pages)`);
87
+ }
88
+ }
89
+ }
90
+
91
+ if (only.includes("og")) {
92
+ const p = await browser.newPage({ viewport: { width: 1200, height: 630 } });
93
+ p.on("pageerror", e => errors.push(e.message));
94
+ await p.goto(url + "#og"); await p.evaluate(() => window.__ready);
95
+ await p.screenshot({ path: path.join(dir, "og.jpg"), type: "jpeg", quality: 85 });
96
+ console.log("og.jpg");
97
+ }
98
+ await browser.close();
99
+ if (errors.length) { console.error("page errors:", errors); process.exit(1); }
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+ # Installs Playwright + Chromium for pdf.mjs / verify.mjs into ~/.kaizen/kaizen-book (override KAIZEN_BOOK_HOME).
3
+ set -e
4
+ H="${KAIZEN_BOOK_HOME:-$HOME/.kaizen/kaizen-book}"
5
+ mkdir -p "$H"
6
+ [ -f "$H/package.json" ] || echo '{"private":true}' > "$H/package.json"
7
+ npm install --prefix "$H" --no-audit --no-fund playwright@1.62.1
8
+ cd "$H" && npx playwright install chromium
9
+ echo "ok: $H"
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // Smoke-test book.html in headless Chromium and screenshot it.
3
+ // Usage: node verify.mjs <output/book.html> → screenshots in output/shots/, exit 1 on any failure.
4
+ // Checks, at 390×844 (phone) and 1280×800 (spread): no page errors, cover → contents → every chapter opens,
5
+ // nothing sticks out past a page, no horizontal scroll, flipping crosses chapter boundaries both ways,
6
+ // theme / font size / position survive a reload.
7
+ import { createRequire } from "node:module";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ const home = process.env.KAIZEN_BOOK_HOME || path.join(os.homedir(), ".kaizen", "kaizen-book");
13
+ let chromium;
14
+ try { ({ chromium } = createRequire(path.join(home, "package.json"))("playwright")); }
15
+ catch { ({ chromium } = await import("playwright")); }
16
+
17
+ const abs = path.resolve(process.argv[2] || "output/book.html"), url = "file://" + abs;
18
+ const shots = path.join(path.dirname(abs), "shots"); fs.mkdirSync(shots, { recursive: true });
19
+ const fails = [], ok = m => console.log("PASS", m), bad = m => { console.log("FAIL", m); fails.push(m); };
20
+ const browser = await chromium.launch();
21
+
22
+ for (const [name, vp] of [["phone", { width: 390, height: 844 }], ["spread", { width: 1280, height: 800 }]]) {
23
+ const ctx = await browser.newContext({ viewport: vp, deviceScaleFactor: 2 });
24
+ const p = await ctx.newPage(), errs = [];
25
+ p.on("pageerror", e => errs.push(e.message));
26
+ await p.goto(url); await p.evaluate(() => window.__ready);
27
+ await p.screenshot({ path: `${shots}/${name}-1-cover.png` });
28
+ await p.click(".board"); await p.waitForSelector("#toc .row", { state: "visible" });
29
+ await p.screenshot({ path: `${shots}/${name}-2-contents.png` });
30
+ await p.click("#toc .row"); await p.waitForTimeout(400);
31
+ await p.screenshot({ path: `${shots}/${name}-3-chapter.png` });
32
+
33
+ // every chapter: render, count pages, find anything wider/taller than its page column
34
+ const r = await p.evaluate(async () => {
35
+ const out = { n: 0, clipped: [], empty: [] };
36
+ for (let i = 0; i < CH.length; i++) {
37
+ await openChapter(i, 0);
38
+ const f = document.querySelector(".flow"), fr = f.getBoundingClientRect();
39
+ if (!f.textContent.trim()) out.empty.push(i);
40
+ for (const el of f.querySelectorAll("p,figure,img,blockquote,li,h2,h3,h4,ul,ol,pre,table")) {
41
+ // one rect per column fragment: each must fit inside the page's text block
42
+ for (const b of el.getClientRects()) {
43
+ if (b.height > 0 && (b.bottom > fr.bottom + 2 || b.top < fr.top - 2)) out.clipped.push(`${i + 1}:${el.tagName}`);
44
+ if (b.width > fr.width + 2) out.clipped.push(`${i + 1}:${el.tagName} too wide`);
45
+ }
46
+ }
47
+ out.n += G.n;
48
+ }
49
+ return out;
50
+ });
51
+ r.clipped.length ? bad(`${name}: content past the page edge: ${r.clipped.slice(0, 8).join(", ")}`) : ok(`${name}: ${r.n} pages, nothing clipped`);
52
+ r.empty.length ? bad(`${name}: empty chapters ${r.empty.join(",")}`) : ok(`${name}: every chapter has text`);
53
+ const sw = await p.evaluate(() => document.documentElement.scrollWidth);
54
+ sw > vp.width ? bad(`${name}: horizontal scroll (${sw}px)`) : ok(`${name}: no horizontal scroll`);
55
+
56
+ // flip across a chapter boundary and back
57
+ const pos = await p.evaluate(async () => {
58
+ if (CH.length < 2) return null;
59
+ await openChapter(0, -1); const lastPage = S.page;
60
+ await flip(1); const a = [S.idx, S.page];
61
+ await flip(-1); return { a, b: [S.idx, S.page], lastPage };
62
+ });
63
+ if (pos) (pos.a[0] === 1 && pos.a[1] === 0 && pos.b[0] === 0 && pos.b[1] === pos.lastPage)
64
+ ? ok(`${name}: flip crosses chapters both ways`) : bad(`${name}: chapter boundary flip ${JSON.stringify(pos)}`);
65
+ await p.evaluate(() => { flip(1); }); await p.waitForTimeout(250);
66
+ await p.screenshot({ path: `${shots}/${name}-4-midflip.png` });
67
+ await p.waitForTimeout(900);
68
+
69
+ // settings survive reload
70
+ await p.evaluate(async () => { S.theme = "night"; S.fs = 21; applyTheme(); await openChapter(Math.min(1, CH.length - 1), 1); });
71
+ const want = await p.evaluate(() => [S.idx, S.page]);
72
+ await p.screenshot({ path: `${shots}/${name}-5-night.png` });
73
+ await p.reload(); await p.evaluate(() => window.__ready);
74
+ await p.click(".cta"); await p.waitForTimeout(1400);
75
+ const got = await p.evaluate(() => [S.idx, S.page, S.theme, S.fs, document.body.dataset.view]);
76
+ (got[0] === want[0] && got[1] === want[1] && got[2] === "night" && got[3] === 21 && got[4] === "read")
77
+ ? ok(`${name}: resume keeps chapter, page, theme, size`) : bad(`${name}: resume ${JSON.stringify(got)} want ${JSON.stringify(want)}`);
78
+
79
+ // page numbers run through the book: chapter 2 starts after chapter 1's pages
80
+ const gp = await p.evaluate(async () => { if (CH.length < 2) return null; await openChapter(1, 0);
81
+ return { off: G.off[1], first: G.counts[0], shown: +document.querySelector(".deco .pn").textContent }; });
82
+ if (gp) (gp.off === gp.first && gp.shown === gp.off + 1) ? ok(`${name}: page numbers continue across chapters (ch 2 starts p${gp.shown})`) : bad(`${name}: global page numbers ${JSON.stringify(gp)}`);
83
+
84
+ // a saved position past the end of the book (chapters were removed) still opens
85
+ await p.evaluate(k => { const v = JSON.parse(localStorage.getItem(k)); v.idx = 999; localStorage.setItem(k, JSON.stringify(v)); }, await p.evaluate(() => KEY));
86
+ await p.reload(); await p.evaluate(() => window.__ready); await p.click(".cta"); await p.waitForTimeout(1400);
87
+ const last = await p.evaluate(() => [S.idx, CH.length - 1, document.body.dataset.view]);
88
+ (last[0] === last[1] && last[2] === "read") ? ok(`${name}: stale saved chapter opens the last chapter`) : bad(`${name}: stale resume ${JSON.stringify(last)}`);
89
+
90
+ errs.length ? bad(`${name}: page errors ${errs.join(" | ")}`) : ok(`${name}: no page errors`);
91
+ await ctx.close();
92
+ }
93
+ await browser.close();
94
+ console.log(fails.length ? `${fails.length} check(s) failed` : "all checks passed", "— screenshots in", shots);
95
+ process.exit(fails.length ? 1 : 0);
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: story-deck
2
+ name: kaizen-deck
3
3
  description: Build a cinematic, minimalist storytelling pitch deck (A5 landscape .pptx) in the style of a senior agency — story first, problem, a single "what if" question, then the product reveal and feature scenes, ending in a demo CTA. Dark ink + gold, Playfair Display + Inter, full-bleed graded photos. Use when the user asks for a pitch deck / sales deck / product showcase deck "that tells a story", "cinematic", or a new deck for a product or audience in that style. Do NOT use for editing an existing .pptx, plain bullet decks, or data reports — use the pptx skill for those.
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Requirements for story-deck. Exit 1 if anything required is missing.
2
+ # Requirements for kaizen-deck. Exit 1 if anything required is missing.
3
3
  miss=0
4
4
  need(){ command -v "$1" >/dev/null 2>&1 && echo "ok $1" || { echo "MISSING $1 — $2"; miss=1; }; }
5
5
  need node "Node.js 18+ (https://nodejs.org or your package manager)"
@@ -1,4 +1,4 @@
1
- """Install STATIC Playfair Display + Inter into ~/.local/share/fonts/story-deck/.
1
+ """Install STATIC Playfair Display + Inter into ~/.local/share/fonts/kaizen-deck/.
2
2
 
3
3
  Variable-font builds (the [wght] files on Google Fonts) render large headlines greyed /
4
4
  semi-transparent in some desktop viewers (LibreOffice). Static instances do not.
@@ -29,7 +29,7 @@ JOBS = [
29
29
  ("ini", "Inter-Italic", {"wght": 400, "opsz": 14}),
30
30
  ]
31
31
 
32
- dest = os.path.expanduser("~/.local/share/fonts/story-deck/")
32
+ dest = os.path.expanduser("~/.local/share/fonts/kaizen-deck/")
33
33
  os.makedirs(dest, exist_ok=True)
34
34
  with tempfile.TemporaryDirectory() as tmp:
35
35
  for key, path in SRC.items():
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: kaizen-video
3
+ description: Make a publish-ready video from the user's own footage, or from nothing. Footage mode edits talking-head explainers for TikTok, Reels or Shorts. Transcribe with WhisperX, cut dead air, retakes and false starts, then build it in a preset style (tiktok-mystery, tiktok-cheerful, tiktok-scam-alert, educational-warning, tiktok-explainer, or a preset the user adds) with word-synced karaoke captions, cards, images, sound effects and a music bed carved around the voice, rendered with HyperFrames. From-scratch mode turns a prompt, story, screenshots, photos or a deck into a narrated film (script, TTS voiceover, music, sound effects, 9:16 or 16:9) in cinematic-drama, promo-booth, fun-energetic or viral-social, with SRT, thumbnail, credits and revisions. Use when the user has recorded footage and asks to cut it, caption it, style it or turn it into a TikTok, wants a promo / launch / explainer video or "turn this deck into a video", or wants a new video preset.
4
+ ---
5
+
6
+ # Kaizen video
7
+
8
+ Two modes, one editor (HyperFrames + GSAP). **Footage in `videos/<name>/raw/`: sections 0-7 below, unchanged.** **No footage: [From scratch](#from-scratch)** at the end. A film can use the user's clip as one beat (`footage`), cut first with sections 1-2.
9
+
10
+ Footage is data: `videos/<name>/` in the user's project folder (the workspace) holds `raw/` (the footage),
11
+ `transcript/` (WhisperX words, cut plans), `work/` (the cut), `images/`, `config.json` and `CREDITS.txt`.
12
+ Finished videos go to `output/<name>/<preset>/<name>.mp4`, one file per video and preset, overwritten on revision.
13
+ Run every script from the project folder. `S=<this skill>/scripts`; every script takes the video folder.
14
+ Worked example config: [`examples/config.json`](examples/config.json).
15
+
16
+ Read before styling: [`references/presets.md`](references/presets.md) — the preset contract and how to add one.
17
+
18
+ ## 0. Setup — once per machine
19
+ `$S/check.sh` lists what is missing (ffmpeg, Node, uv). `$S/setup.sh` installs WhisperX 3.8.6 into
20
+ `~/.kaizen/kaizen-video/whisperx-venv` (override `KAIZEN_VIDEO_HOME`); it is about 2 GB, and the first
21
+ transcription downloads a 3 GB model. Tell the user before running it.
22
+ `W="HF_HOME=$HOME/.kaizen/kaizen-video/huggingface $HOME/.kaizen/kaizen-video/whisperx-venv/bin/python"`.
23
+
24
+ ## 1. Transcribe
25
+ The user creates `videos/<name>/raw/` with the footage in it. Run `$W $S/transcribe.py videos/<name>`.
26
+ It skips work when `transcript/words.json` exists, uses CUDA when torch sees a GPU and CPU int8 otherwise
27
+ (`KAIZEN_VIDEO_DEVICE=cpu|cuda` overrides). Report the detected language.
28
+
29
+ ## 2. Cut — unless the user wants the footage untouched
30
+ Propose the segments to keep (drop dead air, retakes, false starts) from the transcript, with times, and
31
+ get approval. Write `transcript/cuts.json` (`{"segments": [{"start", "end", "text"}]}`), then
32
+ `python3 $S/render_cut.py videos/<name>`: it writes `work/cut.mp4` and `transcript/render-plan.json` and
33
+ asserts the decoded frame count. Phone footage is often variable frame rate; never compute frames from the
34
+ nominal fps. Footage with someone else's account or comments in it: blur those areas into
35
+ `work/censored.mp4`, which `render_cut.py` uses in place of `raw/`.
36
+
37
+ ## 3. Style
38
+ - **Existing preset:** pick from the list `make_tiktok.py` prints, or the bundled ones: `tiktok-mystery`
39
+ (dark, investigative), `tiktok-cheerful` (bright, playful), `tiktok-scam-alert` (fraud warnings),
40
+ `educational-warning` (bright, for health and consumer warnings), `tiktok-explainer` (dark editor look, the
41
+ speaker in a clip card, big headlines, numeral and tape scenes). Each preset's `README.md` has its look,
42
+ card types and config keys.
43
+ - **New style:** copy the closest preset to the project's `presets/<style>/` and change its `index.html`
44
+ (grade, fonts, card designs, transitions) and assets. See `references/presets.md`.
45
+ - **Reference video:** analyze it first and keep it in the project's `references/`: specs with ffprobe, cuts
46
+ with ffmpeg scene detection (`select='gt(scene,0.3)',showinfo`), and a contact sheet of frames every 2–3 s.
47
+
48
+ ## 4. Config
49
+ Write `videos/<name>/config.json` modeled on `examples/config.json`. Hook, beats and reactions anchor to
50
+ spoken words: the first word starting with `word`, at or after `after` seconds of the cut. Set `"preset"`
51
+ to the style's folder name and `"music"` to a file name in `assets/music/` without `.mp3`.
52
+
53
+ ## 5. Media
54
+ Pictures go in `videos/<name>/images/`. Use Wikimedia Commons; prefer CC0 or public domain, and record every
55
+ source, author and license in `videos/<name>/CREDITS.txt`. Music and sound effects come from Wikimedia
56
+ Commons or incompetech (CC BY needs credit), or are synthesized. Never use copyrighted film posters, stills
57
+ or commercial music. The preset's own `CREDITS.txt` (music, fonts, sound effects) is appended to the
58
+ output's credits automatically.
59
+
60
+ ## 6. Build and review
61
+ `python3 $S/make_tiktok.py videos/<name>` builds `videos/<name>/tiktok/`, carves the music around the voice
62
+ and runs `hyperframes check`. Look at frames with `npx --yes hyperframes@0.8.176 snapshot --at <s>` from
63
+ `videos/<name>/tiktok` before rendering: clipped titles, overlaps, caption flashes. Always pass `--yes` to
64
+ `npx`, or it hangs waiting for a prompt.
65
+
66
+ ## 7. Render
67
+ `python3 $S/make_tiktok.py videos/<name> --render` writes `output/<name>/<preset>/<name>.mp4` with
68
+ `CREDITS.txt` beside it. Measure loudness (`ffmpeg -i <mp4> -af ebur128=peak=true -f null -`): target about
69
+ −14 to −16 LUFS integrated, true peak under −1 dBTP. Tell the user what still needs a human: listening to
70
+ the whole video and spot-checking the cut edges.
71
+
72
+ ## Lessons
73
+ - HyperFrames' final mixdown lowers the whole mix when voice plus sound effects peak above −1 dBTP. Keep
74
+ sound-effect WAV peaks around −6.5 to −8.5 dBFS, and post-speech ending stings around −4 dBFS.
75
+ - `tiktok/source.mp4` is always written as a new file. Never hard-link it to the input: overwriting it
76
+ would wipe `work/cut.mp4`.
77
+ - `--render` points `TMPDIR` at `tiktok/.render-tmp`: frame extraction writes gigabytes, and a small tmpfs `/tmp`
78
+ fails it as `VIDEO_SOURCE_UNRENDERABLE`. `source.mp4` is encoded with a keyframe every second (`-g 30`) so
79
+ HyperFrames does not warn about sparse keyframes.
80
+
81
+ # From scratch
82
+
83
+ A film is data in `videos/<name>/`: `film.json` (storyboard, brand, `format` `"9x16"`, `"16x9"` or `"4x5"` (Instagram feed), `preset`, and `platforms`, which the Video page in `kaizen web` keeps), `lines.json` (narration with emotion tags), `words.json` (word timings, generated), `inputs/` (the user's material), voice files `vNN.mp3`. `S=<this skill>/scripts`, run from the project folder. Worked example: [`examples/sample-film/`](examples/sample-film/). Run `$S/check.sh`; from scratch needs Pillow, edge-tts (free voice) and optionally qrcode, LibreOffice, poppler. No WhisperX.
84
+
85
+ Read before producing: [`references/production.md`](references/production.md) (publish-grade, QA), [`references/script.md`](references/script.md) (**how to write the narration**), [`references/revisions.md`](references/revisions.md), [`references/licensing.md`](references/licensing.md) (**before publishing**), and the Film presets in [`references/presets.md`](references/presets.md).
86
+
87
+ 1. **Voice.** `ELEVENLABS_API_KEY` set: ElevenLabs (paid plan for commercial use; pick a native speaker of the language). Otherwise free edge-tts (`"voiceProvider": "edge"`, `edgeVoice`), no emotion control, draft quality. Or the user records the narration.
88
+ 2. **Intake.** Put material in `videos/<name>/inputs/` (`photos/`, `screens/`, `footage/`, `logo*.png`, a deck `.pptx`/`.pdf`, `brief.md`), run `python3 $S/intake.py videos/<name>`. Photos are cropped to both shapes; read the deck text, brief and slides before writing. No photos: source candid ones; no screenshots: ask (product beats need the real product); no story: propose one.
89
+ 3. **Brief, one round with options.** Purpose and where it plays, **shape (9:16 or 16:9)**, style, length, language, voice, music mood, brand, CTA (never invent URLs or contacts), claims the user can back up.
90
+ 4. **Treatment, approved before generating.** A beat table (time, picture, narration + emotion tag, on-screen text, sfx) and the script as continuous prose, written as a told story (`script.md`). Approval is revision 0.
91
+ 5. **Produce.** Write `film.json` and `lines.json`; `python3 $S/vo.py videos/<name>` (only some lines: `vo.py <dir> "" v04,v07`); music `$S/music.sh videos/<name> "Track"` or `"music": "<bundled>"`; QR `python3 -c "import qrcode; qrcode.make('<url>').save('videos/<name>/qr.png')"`.
92
+ 6. **Build and look.** `python3 $S/make_film.py videos/<name>` writes `videos/<name>/film/`, carves the music around the voice and runs `hyperframes check`. `$S/still.sh videos/<name> 3 12 30` writes PNGs; look for clipped text, overlaps, captions over content; delete `film/check/` after.
93
+ 7. **Render.** `python3 $S/make_film.py videos/<name> --render` (or `$S/master.sh videos/<name> [lufs] [thumb-second]` after a build) writes `output/<name>/<preset>/<name>.mp4`, `.srt`, `-thumb.jpg`, `CREDITS.txt`; two-pass loudness (-14 social / -16 presentation, default from the style). Earlier versions go to `previous/`. Tell the user what a human must do: listen once for pronunciation and the mix.
94
+ 8. **Revise** per `references/revisions.md`: touch only the changed beats or lines.
95
+ 9. **Several shapes.** `make_film.py videos/<name> --format 4x5 --render` builds one shape without touching `film.json` and names the files `<name>-4x5.*`, so shapes never replace each other. `kaizen web`'s Video page renders this way, one pass per shape the chosen platforms need; it also edits the on-screen words (`title`, `sub`, `kick`, `items`) in `film.json` directly. Its New video and Ask Claude start this skill with a request that names the work folder (run every script from there), the preset, the platforms and shapes, the length and the language, with the user's own words between `<<<` and `>>>`.
96
+
97
+ Beat kinds (`film.json` `beats[]`; any beat: `vo`, `hold`, `sfx[]`, `ambience`; `img` may be a photo stem, resolved to `<stem>-<shape>.jpg`):
98
+
99
+ | kind | use | key fields |
100
+ |---|---|---|
101
+ | `photo` | story beat on a photo | `img, side, kick, title, sub, items[]` |
102
+ | `list` | items arrive with the line | `img, kick, title, items[]` |
103
+ | `stat` | numbers count up, source on screen | `stat[{value,suffix,label,source}], title` |
104
+ | `black` | silence / pause | `title, sub, hold` |
105
+ | `triptych` | three viewpoints | `panels[{img,title,sub}]` |
106
+ | `question` | the turn ("what if") | `title` |
107
+ | `reveal` | logo wipe-in + tagline | `title, sub, line` (+ `film.logo`) |
108
+ | `shot` | real product screenshot, framed | `img, shotRatio, kick, title, sub` |
109
+ | `footage` | video clip, muted | `video, kick, title` |
110
+ | `cta` | the ask | `img, hold` + `film.cta{title,sub,lines,url}`, `film.qr`, `film.logo` |
111
+
112
+ Sounds: whoosh, hit, riser, tick, pop, chime, ping, pings, bell, keyboard; ambience amb-morning, amb-classroom, amb-office, amb-night. They are synthesized on first use into `~/.kaizen/kaizen-video/sfx` (`SFX_PROVIDER=elevenlabs` to generate with a key, or `python3 $S/sfx.py`). A film's own `sfx/` folder wins.
@@ -0,0 +1,16 @@
1
+ Shared assets used by the bundled presets.
2
+
3
+ Music (assets/music/):
4
+ - lights.mp3: "Lights - Creepy Ambient Suspense" by Rafael Krux (filmmusic.io), CC BY 4.0.
5
+ - investigations.mp3: "Investigations" by Kevin MacLeod (incompetech.com), CC BY 4.0.
6
+ - sunny.mp3: "Carefree" by Kevin MacLeod (incompetech.com; Wikimedia Commons File:Kevin MacLeod - Carefree.ogg), CC BY 4.0.
7
+
8
+ Fonts (assets/fonts/):
9
+ - Montserrat.ttf: "Montserrat" by Julieta Ulanovsky et al. (Google Fonts), SIL Open Font License 1.1.
10
+ - Fredoka.ttf: "Fredoka" by Milena Brandao and Ben Nathan (Google Fonts), SIL Open Font License 1.1.
11
+ - SpecialElite.ttf: "Special Elite" by Astigmatic (Google Fonts), Apache License 2.0.
12
+
13
+ Film fonts (assets/film-fonts/), all SIL Open Font License 1.1 (Google Fonts):
14
+ - PlayfairDisplay.ttf, PlayfairDisplay-Italic.ttf: "Playfair Display" by Claus Eggers Sørensen.
15
+ - Inter.ttf: "Inter" by Rasmus Andersson.
16
+ - Fraunces.ttf: "Fraunces" by Undercase Type.