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,127 @@
1
+ # Part One: The Keeper
2
+
3
+ The first part follows Ilse through the long quiet.
4
+
5
+ ## The Long Quiet | In which nothing happens, carefully
6
+
7
+ Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. She had stopped trying to reply to them; it felt like speaking to photographs. The station hummed the way old machines hum, as if remembering a song nobody had taught it. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her.
8
+
9
+ Sometimes the relay crackled with fragments of voices from stations that no longer answered. When the alarm finally sounded, she did not believe it for almost a full minute. The station hummed the way old machines hum, as if remembering a song nobody had taught it.
10
+
11
+ The station hummed the way old machines hum, as if remembering a song nobody had taught it. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. She had stopped trying to reply to them; it felt like speaking to photographs. She had stopped trying to reply to them; it felt like speaking to photographs.
12
+
13
+ She wrote her reports anyway, in the careful hand her mother had used for letters home. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her.
14
+
15
+ The station hummed the way old machines hum, as if remembering a song nobody had taught it. When the alarm finally sounded, she did not believe it for almost a full minute. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. She wrote her reports anyway, in the careful hand her mother had used for letters home. When the alarm finally sounded, she did not believe it for almost a full minute. The station hummed the way old machines hum, as if remembering a song nobody had taught it.
16
+
17
+ She kept a list on the wall of the galley:
18
+
19
+ - Check the lamp.
20
+ - Water the garden.
21
+ - Write the report, even if no one reads it.
22
+
23
+ The station hummed the way old machines hum, as if remembering a song nobody had taught it. She wrote her reports anyway, in the careful hand her mother had used for letters home. The station hummed the way old machines hum, as if remembering a song nobody had taught it. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right.
24
+
25
+ Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. When the alarm finally sounded, she did not believe it for almost a full minute. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her.
26
+
27
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. When the alarm finally sounded, she did not believe it for almost a full minute. When the alarm finally sounded, she did not believe it for almost a full minute. She wrote her reports anyway, in the careful hand her mother had used for letters home.
28
+
29
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. When the alarm finally sounded, she did not believe it for almost a full minute. The station hummed the way old machines hum, as if remembering a song nobody had taught it.
30
+
31
+ > Keep the light, and the light will keep you.
32
+ > — inscription above the hatch
33
+
34
+ The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. She had stopped trying to reply to them; it felt like speaking to photographs. Sometimes the relay crackled with fragments of voices from stations that no longer answered.
35
+
36
+ When the alarm finally sounded, she did not believe it for almost a full minute. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Sometimes the relay crackled with fragments of voices from stations that no longer answered. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. She wrote her reports anyway, in the careful hand her mother had used for letters home. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again.
37
+
38
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. When the alarm finally sounded, she did not believe it for almost a full minute. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her.
39
+
40
+ Sometimes the relay crackled with fragments of voices from stations that no longer answered. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. When the alarm finally sounded, she did not believe it for almost a full minute. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years.
41
+
42
+ Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. Sometimes the relay crackled with fragments of voices from stations that no longer answered. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. She had stopped trying to reply to them; it felt like speaking to photographs. The station hummed the way old machines hum, as if remembering a song nobody had taught it.[^1]
43
+
44
+ ***
45
+
46
+ Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. When the alarm finally sounded, she did not believe it for almost a full minute. Sometimes the relay crackled with fragments of voices from stations that no longer answered.
47
+
48
+ Sometimes the relay crackled with fragments of voices from stations that no longer answered. When the alarm finally sounded, she did not believe it for almost a full minute. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. When the alarm finally sounded, she did not believe it for almost a full minute. The garden in Module Four needed water, and the water needed the pump, and the pump needed her.
49
+
50
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. The garden in Module Four needed water, and the water needed the pump, and the pump needed her.
51
+
52
+ The station hummed the way old machines hum, as if remembering a song nobody had taught it. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. When the alarm finally sounded, she did not believe it for almost a full minute.
53
+
54
+ [^1]: The phrase comes from the station's founding charter, signed in 2211.
55
+
56
+ ## Signals | A voice where no voice should be
57
+
58
+ There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. She had stopped trying to reply to them; it felt like speaking to photographs. Sometimes the relay crackled with fragments of voices from stations that no longer answered. The station hummed the way old machines hum, as if remembering a song nobody had taught it. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Sometimes the relay crackled with fragments of voices from stations that no longer answered.
59
+
60
+ When the alarm finally sounded, she did not believe it for almost a full minute. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. The station hummed the way old machines hum, as if remembering a song nobody had taught it.
61
+
62
+ There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. She wrote her reports anyway, in the careful hand her mother had used for letters home. She had stopped trying to reply to them; it felt like speaking to photographs.
63
+
64
+ The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. She had stopped trying to reply to them; it felt like speaking to photographs. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her.
65
+
66
+ Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. She had stopped trying to reply to them; it felt like speaking to photographs. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. She had stopped trying to reply to them; it felt like speaking to photographs.
67
+
68
+ She had stopped trying to reply to them; it felt like speaking to photographs. She wrote her reports anyway, in the careful hand her mother had used for letters home. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again.
69
+
70
+ ![The Reach at shift change](images/reach.svg)
71
+
72
+ She wrote her reports anyway, in the careful hand her mother had used for letters home. She wrote her reports anyway, in the careful hand her mother had used for letters home. The station hummed the way old machines hum, as if remembering a song nobody had taught it. The garden in Module Four needed water, and the water needed the pump, and the pump needed her.
73
+
74
+ There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. The station hummed the way old machines hum, as if remembering a song nobody had taught it. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again.
75
+
76
+ Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. Sometimes the relay crackled with fragments of voices from stations that no longer answered. When the alarm finally sounded, she did not believe it for almost a full minute. When the alarm finally sounded, she did not believe it for almost a full minute. Sometimes the relay crackled with fragments of voices from stations that no longer answered. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again.
77
+
78
+ The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. She had stopped trying to reply to them; it felt like speaking to photographs.
79
+
80
+ She had stopped trying to reply to them; it felt like speaking to photographs. She had stopped trying to reply to them; it felt like speaking to photographs. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. She had stopped trying to reply to them; it felt like speaking to photographs. The station hummed the way old machines hum, as if remembering a song nobody had taught it.
81
+
82
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. She wrote her reports anyway, in the careful hand her mother had used for letters home. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again.
83
+
84
+ # Part Two: The Ship
85
+
86
+ ## Contact
87
+
88
+ Sometimes the relay crackled with fragments of voices from stations that no longer answered. When the alarm finally sounded, she did not believe it for almost a full minute. The station hummed the way old machines hum, as if remembering a song nobody had taught it.
89
+
90
+ The station hummed the way old machines hum, as if remembering a song nobody had taught it. When the alarm finally sounded, she did not believe it for almost a full minute. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again.
91
+
92
+ Sometimes the relay crackled with fragments of voices from stations that no longer answered. When the alarm finally sounded, she did not believe it for almost a full minute. The station hummed the way old machines hum, as if remembering a song nobody had taught it.
93
+
94
+ She wrote her reports anyway, in the careful hand her mother had used for letters home. When the alarm finally sounded, she did not believe it for almost a full minute. She had stopped trying to reply to them; it felt like speaking to photographs.
95
+
96
+ There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. Sometimes the relay crackled with fragments of voices from stations that no longer answered. When the alarm finally sounded, she did not believe it for almost a full minute. Sometimes the relay crackled with fragments of voices from stations that no longer answered.
97
+
98
+ ### The Approach
99
+
100
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. The garden in Module Four needed water, and the water needed the pump, and the pump needed her.
101
+
102
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. Sometimes the relay crackled with fragments of voices from stations that no longer answered. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right.
103
+
104
+ Outside, the dust of the Reach moved in slow rivers, catching the beam and letting it go again. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. The station hummed the way old machines hum, as if remembering a song nobody had taught it. She wrote her reports anyway, in the careful hand her mother had used for letters home. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. Sometimes the relay crackled with fragments of voices from stations that no longer answered.
105
+
106
+ Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. The station hummed the way old machines hum, as if remembering a song nobody had taught it. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right.
107
+
108
+ There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. Sometimes the relay crackled with fragments of voices from stations that no longer answered.[^a] And then it was there.[^b]
109
+
110
+ [^a]: Ships of this class were retired a century earlier.
111
+ [^b]: Ilse's log for this hour is a single line: *It came.*
112
+
113
+ ## The Last Light
114
+
115
+ Sometimes the relay crackled with fragments of voices from stations that no longer answered. She wrote her reports anyway, in the careful hand her mother had used for letters home. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her.
116
+
117
+ She wrote her reports anyway, in the careful hand her mother had used for letters home. When the alarm finally sounded, she did not believe it for almost a full minute. She wrote her reports anyway, in the careful hand her mother had used for letters home. She wrote her reports anyway, in the careful hand her mother had used for letters home. She had stopped trying to reply to them; it felt like speaking to photographs.
118
+
119
+ She wrote her reports anyway, in the careful hand her mother had used for letters home. Every system on Kepler Reach leaned on another, and all of them, in the end, leaned on her. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Sometimes the relay crackled with fragments of voices from stations that no longer answered.
120
+
121
+ The station hummed the way old machines hum, as if remembering a song nobody had taught it. There was a comfort in procedure, in the small certainty that one thing, at least, was being done right. The garden in Module Four needed water, and the water needed the pump, and the pump needed her.
122
+
123
+ She wrote her reports anyway, in the careful hand her mother had used for letters home. When the alarm finally sounded, she did not believe it for almost a full minute. Sometimes the relay crackled with fragments of voices from stations that no longer answered. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. Sometimes the relay crackled with fragments of voices from stations that no longer answered.
124
+
125
+ Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. She wrote her reports anyway, in the careful hand her mother had used for letters home. Ilse checked the lamp twice each shift, although the logs had not recorded a passing ship in eleven years. She wrote her reports anyway, in the careful hand her mother had used for letters home. The garden in Module Four needed water, and the water needed the pump, and the pump needed her.
126
+
127
+ Sometimes the relay crackled with fragments of voices from stations that no longer answered. She wrote her reports anyway, in the careful hand her mother had used for letters home. The garden in Module Four needed water, and the water needed the pump, and the pump needed her. When the alarm finally sounded, she did not believe it for almost a full minute.
@@ -0,0 +1,85 @@
1
+ # The interview: before a word of the book is written
2
+
3
+ A new book starts with an interview, never with a draft. The answers become `book/brief.md`,
4
+ which the author approves before the outline is drafted. Skip only the questions the author has already answered
5
+ in their request, and say which ones you took from it.
6
+
7
+ ## How to ask
8
+
9
+ - One round at a time, with AskUserQuestion, at most 4 questions per round, 2–4 concrete options
10
+ each. Free text is always available, so never add an "other" option.
11
+ - Put the recommended or most common choice first. Fill options from what is already known: a
12
+ romance author is offered romance tropes, not hard-sci-fi subgenres.
13
+ - Ask in the author's language. If they wrote in Indonesian, every question and option is in Indonesian.
14
+ - Four rounds is normal, and six is the limit. Once the brief is good enough to outline from, stop asking and write it.
15
+ - An answer that changes the shape of the book (fiction to memoir, novel to short-story collection)
16
+ restarts the round it belongs to, not the whole interview.
17
+
18
+ ## Round 1 — what kind of book
19
+
20
+ | Question | Typical options |
21
+ |---|---|
22
+ | What kind of book? | Novel / novella · short-story collection · memoir or biography · non-fiction (history, essays, religious, self-help, how-to) · children's book |
23
+ | Genre (fiction) | science fiction · romance · fantasy · mystery/thriller · horror · literary · historical · young adult |
24
+ | Subject (non-fiction) | Ask for the subject in one line, then: argument · narrative history · practical guide · collected essays/articles |
25
+ | Who reads it? | Age band (children 6–9 · middle grade · young adult · adult), and general readers or specialists |
26
+
27
+ ## Round 2 — size and voice
28
+
29
+ | Question | Typical options |
30
+ |---|---|
31
+ | Language | The author's language first; bilingual front matter if asked |
32
+ | Length | Short (~15k words, 6–10 chapters) · novella (~30k, 10–15) · novel (~70k, 20–30) · long (100k+, consider volumes) |
33
+ | Point of view (fiction) | first person · close third, one character · third, several characters · omniscient |
34
+ | Tense (fiction) | past · present |
35
+ | Tone | Offer 3–4 tones that fit the genre: romance → warm & witty / slow-burn & aching / steamy / sweet & clean; thriller → cold & clipped / paranoid / procedural |
36
+ | Voice reference | "Like which book or author?" Use the answer for voice only, and never imitate the text itself |
37
+
38
+ ## Round 3 — the story or the argument
39
+
40
+ Fiction:
41
+ - Premise in one or two sentences: who wants what, what stands in the way, and what is at stake.
42
+ - The protagonist: name, age, the flaw, and what they want versus what they need.
43
+ - Two to four other main characters, each with their role in one line.
44
+ - Setting: place, time, and the one rule of this world that matters (magic system, tech, social order).
45
+ - Ending: happy / bittersweet / tragic / open. For a romance, HEA (happily ever after) or HFN (happy for now) is a genre promise; ask, don't assume.
46
+ - Content limits: violence, sex, language. What is on the page, implied, or off limits.
47
+
48
+ Non-fiction:
49
+ - Thesis or promise: what the reader knows or can do after the last page.
50
+ - Sources the author has: notes, articles, interviews, earlier writing. Existing text is the author's
51
+ material and is used as it is; it is never rewritten without asking.
52
+ - Structure: chronological · thematic · problem→solution · collected pieces grouped into parts.
53
+ - Facts and citations: footnotes or endnotes · a bibliography · none.
54
+
55
+ ## Round 4 — the object
56
+
57
+ | Question | Options |
58
+ |---|---|
59
+ | Look (`style` in book.json) | classic · romance · scifi · fantasy · thriller · literary · children. Name the one that matches the genre first, with its one-line note from `assets/styles.json` |
60
+ | Cover | Typeset cover in the style's colours (default) · their own image (ask for the file; 2:3 portrait, at least 1200×1800) |
61
+ | Parts and volumes | One volume · parts (`#` headings) · several printed volumes (very long books) |
62
+ | Front matter | Dedication · copyright line · preface or foreword (who writes it) |
63
+ | Where it goes | Just the files · a website with link previews (ask for the domain) · print only |
64
+
65
+ ## The brief
66
+
67
+ Write `book/brief.md`. The author approves it, and it is the scope for everything after:
68
+
69
+ ```markdown
70
+ # Brief — <working title>
71
+ - Kind / genre: … - Readers: …
72
+ - Language: … - Length: … words, … chapters
73
+ - POV / tense: … - Tone: … (voice like: …)
74
+ - Premise / thesis: …
75
+ - Characters / sources: …
76
+ - Setting / structure: …
77
+ - Ending / promise: …
78
+ - Content limits: …
79
+ - Look: style `…`, cover: …, parts: …, volumes: …
80
+ - Front matter: …
81
+ - Delivery: …
82
+ ```
83
+
84
+ Then create `book/book.json` from it (title, author, lang, style, and so on) and move on to the outline in
85
+ [`writing.md`](writing.md).
@@ -0,0 +1,47 @@
1
+ # Proofreading
2
+
3
+ There are two passes. The script checks structure and you read for language. Nothing is changed until the
4
+ author picks which items to accept.
5
+
6
+ ## 1. Structure: `python3 $S/check_book.py book`
7
+
8
+ It reports chapter numbering gaps, footnote markers without notes and notes nobody cites, empty chapters
9
+ and headings, `##Heading` without a space, broken image paths, Word field codes and HTML leftovers,
10
+ soft hyphens, double spaces, space before punctuation, unclosed bold, mixed straight and curly quotes, and
11
+ repeated words. Errors stop the build; warnings are judgement calls. Fix errors that are mechanical
12
+ (a missing space after `##`) directly and list them. Anything that changes wording goes in the
13
+ change list below.
14
+
15
+ Also search for `[[CHECK:` markers left while drafting. Every one is an open question for the author.
16
+
17
+ ## 2. Language: `book/proofread.md`
18
+
19
+ Read chapter by chapter, against `book/style.md`. Write numbered items, one per change:
20
+
21
+ ```markdown
22
+ # Proofread — <title>, <date>
23
+ | # | Where | Original | Correction | Reason |
24
+ |---|---|---|---|---|
25
+ | 1 | Bab 3, ¶4 | "di karenakan" | "dikarenakan" | KBBI: prefix written together |
26
+ | 2 | Ch 7, ¶12 | "Marta" | "Martha" | style sheet spelling |
27
+ ```
28
+
29
+ Look for:
30
+ - spelling and grammar in the book's language and standard: for Indonesian, KBBI and EYD (prefixes *di-*/*ke-* versus
31
+ prepositions *di*/*ke*, capitalisation, italics for foreign words); for English, the US or UK choice in the style sheet;
32
+ - consistency with the style sheet: names, terms, transliterations, numbers, dates;
33
+ - continuity (fiction): a character who knows something before they learn it, timelines, a changed eye colour;
34
+ - facts (non-fiction): dates, names and quotes that disagree within the book;
35
+ - punctuation: dialogue quotes, dashes, ellipses, and missing or doubled full stops;
36
+ - footnote numbering and placement (the marker comes after the punctuation, unless the language's
37
+ convention says otherwise).
38
+
39
+ Do not "improve" the author's style in a proofread. Changes of voice, wording or structure are a
40
+ separate edit, and only with the author's say-so. When a passage reads badly enough that you would rewrite it, list it as
41
+ a query ("Q:"), not a correction.
42
+
43
+ ## 3. Apply
44
+
45
+ The author answers with the numbers to accept ("all except 4, 9", "1–20"). Apply exactly those, and
46
+ mark each item `✓ applied` or `✗ declined` in `proofread.md`, which becomes the change record (like an
47
+ editor's change list). Then run `check_book.py` again and rebuild.
@@ -0,0 +1,53 @@
1
+ # Writing the book
2
+
3
+ The order is: brief (approved) → outline (approved) → chapters in batches → proofread → build. Every
4
+ step leaves a file in `book/`, so another session or another tool can pick the book up where it stopped.
5
+
6
+ ## Outline: `book/outline.md`
7
+
8
+ - Fiction: one entry per chapter, with title, POV character, what happens, what changes by the end of the chapter, and
9
+ the hook into the next one. Mark the turning points: inciting incident (~10%), first turn (~25%),
10
+ midpoint (~50%), crisis (~75%), climax and resolution.
11
+ - Non-fiction: one entry per chapter, with its claim or question, its sources, and what the reader takes away.
12
+ - Group the chapters into parts when the brief asks for parts. A part becomes `# Part title` in the manuscript.
13
+ - Show the outline and wait for approval. A rejected outline is revised, never "fixed while drafting".
14
+
15
+ ## The style sheet: `book/style.md`
16
+
17
+ Start it with the outline and update it after every batch of chapters. This is what keeps chapter 20 consistent with chapter 2:
18
+
19
+ - Names and spellings: characters, places, invented words, titles and honorifics, transliterations (one
20
+ system, used everywhere).
21
+ - Facts established so far: ages, eye colours, dates, who knows what and when.
22
+ - Voice rules: POV, tense, dialogue punctuation, how numbers are written, the spelling standard
23
+ (KBBI/EYD for Indonesian, US or UK spelling for English).
24
+ - Words to avoid: the author's dislikes, anachronisms, and your own tics (see below).
25
+
26
+ ## Drafting
27
+
28
+ - Write into `book/manuscript.md` in the format `build_book.py` reads:
29
+ `# Part`, `## Chapter title | optional subtitle`, `### Section`, blank line between paragraphs,
30
+ `> quote`, `- list`, `***` scene break, `![caption](images/file.jpg)`, footnote `[^1]` with
31
+ `[^1]: note text` later in the same chapter.
32
+ - Write in batches of three to five chapters. After each batch, show a short summary of each chapter and what
33
+ you changed in the style sheet, and stop for the author. They may want to steer before the next batch.
34
+ - Read `brief.md`, `style.md`, the outline entry, and the end of the previous chapter before writing a chapter.
35
+ Don't reread the whole manuscript.
36
+ - Aim for the brief's chapter length. Report the word count per chapter and the running total.
37
+ - Prose: concrete over abstract, scenes over summary (fiction), one idea per paragraph (non-fiction).
38
+ Vary sentence length. Dialogue carries subtext, and people rarely say exactly what they mean.
39
+ - Avoid the habits that mark machine prose: stacked adjectives, "a testament to", "tapestry",
40
+ "delve", "in the realm of", "it's not X, it's Y", every chapter ending on a reflective one-liner,
41
+ everyone "letting out a breath they didn't know they were holding". Put any you catch in the style sheet's
42
+ avoid list.
43
+ - Facts in non-fiction: never invent a source, a quote, a date or a statistic. Anything the author's material
44
+ does not support is marked `[[CHECK: …]]` for the author, and the structure check reports these
45
+ markers until they are resolved.
46
+ - The author's own text is theirs. Edit it only as the brief allows (light copyedit, restructure, or
47
+ rewrite), and say what kind of edit you made.
48
+
49
+ ## Existing manuscripts
50
+
51
+ If the author already has a text, convert it with
52
+ `pandoc in.docx -t gfm --wrap=none --extract-media=book/images -o book/manuscript.md`. Then fix
53
+ the headings so parts are `#` and chapters are `##`, and go straight to proofreading.
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env python3
2
+ """Build a single-file e-book reader from book/manuscript.md + book/book.json.
3
+
4
+ Usage: python3 build_book.py <book-dir> [--out output] [--no-fonts]
5
+ Writes <out>/book.html. Stdlib only. Run pdf.mjs afterwards for the PDFs.
6
+ """
7
+ import base64, html, json, mimetypes, os, re, sys, urllib.request
8
+
9
+ SKILL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10
+
11
+ LABELS = {
12
+ "id": {"start": "Mulai Membaca", "resume": "Lanjutkan", "contents": "Daftar Isi", "cover": "Sampul",
13
+ "tap": "Ketuk untuk membuka", "settings": "Pengaturan tampilan", "theme": "Tema", "size": "Ukuran huruf",
14
+ "light": "Kertas", "sepia": "Sepia", "night": "Malam", "print": "Cetak / PDF", "notes": "Catatan",
15
+ "page": "hlm.", "by": "oleh", "close": "Tutup", "prev": "Sebelumnya", "next": "Berikutnya"},
16
+ "en": {"start": "Start Reading", "resume": "Continue", "contents": "Contents", "cover": "Cover",
17
+ "tap": "Tap to open", "settings": "Display settings", "theme": "Theme", "size": "Text size",
18
+ "light": "Paper", "sepia": "Sepia", "night": "Night", "print": "Print / PDF", "notes": "Notes",
19
+ "page": "p.", "by": "by", "close": "Close", "prev": "Previous", "next": "Next"},
20
+ }
21
+
22
+ # ---------------------------------------------------------------- Markdown
23
+
24
+ def inline(s, ch):
25
+ """Inline Markdown to HTML. `ch` collects footnote refs for this chapter."""
26
+ out, pos = [], 0
27
+ code = re.compile(r"`([^`]+)`")
28
+ parts = code.split(s)
29
+ for i, p in enumerate(parts):
30
+ if i % 2:
31
+ out.append("<code>%s</code>" % html.escape(p))
32
+ continue
33
+ p = html.escape(p, quote=False)
34
+ p = re.sub(r"!\[([^\]]*)\]\(([^)\s]+)\)", lambda m: '<img alt="%s" src="%s">' % (m[1], ch["img"](m[2])), p)
35
+ p = re.sub(r"\[\^([^\]]+)\]", lambda m: ch["ref"](m[1]), p)
36
+ p = re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", r'<a href="\2" target="_blank" rel="noopener">\1</a>', p)
37
+ p = re.sub(r"\*\*(.+?)\*\*|__(.+?)__", lambda m: "<strong>%s</strong>" % (m[1] or m[2]), p)
38
+ p = re.sub(r"(?<![\w*])\*(?!\s)(.+?)(?<!\s)\*(?!\w)|(?<![\w_])_(?!\s)(.+?)(?<!\s)_(?!\w)",
39
+ lambda m: "<em>%s</em>" % (m[1] or m[2]), p)
40
+ p = re.sub(r"( |\\)\n", "<br>", p)
41
+ out.append(p)
42
+ return "".join(out)
43
+
44
+
45
+ def parse(text, book_dir):
46
+ """Returns {"intro": html, "parts": [{"title", "intro", "chs": [{"t", "sub", "html"}]}]}."""
47
+ text = text.replace("\r\n", "\n").lstrip("")
48
+ lines = text.split("\n")
49
+ parts = [{"title": None, "intro": "", "chs": []}]
50
+ cur = {"buf": [], "notes": {}, "order": []}
51
+
52
+ def img(src):
53
+ if re.match(r"^(https?:|data:)", src):
54
+ return src
55
+ path = os.path.join(book_dir, src)
56
+ if not os.path.exists(path):
57
+ print("warning: image not found: %s" % src, file=sys.stderr)
58
+ return src
59
+ if os.path.getsize(path) > 1_500_000:
60
+ print("warning: %s is %.1f MB; resize it to keep the book light" % (src, os.path.getsize(path) / 1e6), file=sys.stderr)
61
+ mime = mimetypes.guess_type(path)[0] or "image/jpeg"
62
+ return "data:%s;base64,%s" % (mime, base64.b64encode(open(path, "rb").read()).decode())
63
+
64
+ chap_no = [0]
65
+
66
+ def ref(key):
67
+ if key not in cur["order"]:
68
+ cur["order"].append(key)
69
+ n = cur["order"].index(key) + 1
70
+ return '<sup class="fn"><a href="#" data-fn="%d">%d</a></sup>' % (n, n)
71
+
72
+ ctx = {"img": img, "ref": ref}
73
+
74
+ def flush():
75
+ """Render the buffered block lines into the current chapter (or part intro)."""
76
+ blocks, block, code = [], [], None
77
+ for ln in cur["buf"]:
78
+ if code is not None: # inside ``` … ```: keep every line, blank ones too
79
+ if ln.strip().startswith("```"):
80
+ blocks.append(code); code = None
81
+ else:
82
+ code.append(ln)
83
+ elif ln.strip().startswith("```"):
84
+ if block:
85
+ blocks.append(block); block = []
86
+ code = ["\0code"]
87
+ elif ln.strip() == "":
88
+ if block:
89
+ blocks.append(block); block = []
90
+ else:
91
+ block.append(ln)
92
+ for b in (block, code):
93
+ if b:
94
+ blocks.append(b)
95
+ out = []
96
+ for b in blocks:
97
+ if b[0] == "\0code":
98
+ out.append("<pre><code>%s</code></pre>" % html.escape("\n".join(b[1:])))
99
+ continue
100
+ first = b[0].strip()
101
+ joined = "\n".join(x.strip() for x in b)
102
+ if re.match(r"^\[\^[^\]]+\]:", first):
103
+ key = None
104
+ for x in b:
105
+ m = re.match(r"^\[\^([^\]]+)\]:\s*(.*)", x.strip())
106
+ if m:
107
+ key = m[1]; cur["notes"][key] = m[2]
108
+ elif key:
109
+ cur["notes"][key] += " " + x.strip()
110
+ continue
111
+ if re.match(r"^(\*\s*\*\s*\*|-{3,}|\*{3,}|⁂)$", first) and len(b) == 1:
112
+ out.append('<p class="scene">⁂</p>')
113
+ elif first.startswith("### ") or first.startswith("#### "):
114
+ lvl = 3 if first.startswith("### ") else 4
115
+ out.append("<h%d>%s</h%d>" % (lvl, inline(first[lvl + 1:].strip(), ctx), lvl))
116
+ rest = b[1:]
117
+ if rest:
118
+ out.append('<p dir="auto">%s</p>' % inline("\n".join(rest), ctx))
119
+ elif first.startswith(">"):
120
+ inner = "\n".join(re.sub(r"^>\s?", "", x.strip()) for x in b)
121
+ paras = [p for p in re.split(r"\n\s*\n", inner) if p.strip()]
122
+ out.append("<blockquote>%s</blockquote>" % "".join('<p dir="auto">%s</p>' % inline(p, ctx) for p in paras))
123
+ elif re.match(r"^([-*+]|\d+[.)])\s", first):
124
+ ordered = bool(re.match(r"^\d", first))
125
+ items, it = [], None
126
+ for x in b:
127
+ m = re.match(r"^\s*([-*+]|\d+[.)])\s+(.*)", x)
128
+ if m:
129
+ it = [m[2]]; items.append(it)
130
+ elif it is not None:
131
+ it.append(x.strip())
132
+ tag = "ol" if ordered else "ul"
133
+ out.append("<%s>%s</%s>" % (tag, "".join('<li dir="auto">%s</li>' % inline("\n".join(i), ctx) for i in items), tag))
134
+ elif re.match(r"^!\[[^\]]*\]\([^)]+\)$", joined):
135
+ m = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)$", joined)
136
+ cap = '<figcaption>%s</figcaption>' % inline(m[1], ctx) if m[1] else ""
137
+ out.append('<figure><img alt="%s" src="%s">%s</figure>' % (html.escape(m[1]), img(m[2]), cap))
138
+ elif first.startswith("<"):
139
+ out.append(joined)
140
+ else:
141
+ out.append('<p dir="auto">%s</p>' % inline("\n".join(x.rstrip(" ") if not x.endswith(" ") else x for x in b), ctx))
142
+ cur["buf"] = []
143
+ if cur["order"]:
144
+ notes = "".join('<li data-n="%d">%s</li>' % (i + 1, inline(cur["notes"].get(k, "<em>?</em>"), ctx))
145
+ for i, k in enumerate(cur["order"]))
146
+ out.append('<ol class="notes">%s</ol>' % notes)
147
+ return "".join(out)
148
+
149
+ target = ("intro", parts[0])
150
+ in_fence = False
151
+
152
+ def close():
153
+ body = flush()
154
+ kind, obj = target
155
+ if kind == "intro":
156
+ obj["intro"] += body
157
+ else:
158
+ obj["html"] += body
159
+ cur["notes"], cur["order"] = {}, []
160
+
161
+ for ln in lines:
162
+ if ln.strip().startswith("```"):
163
+ in_fence = not in_fence
164
+ cur["buf"].append(ln)
165
+ continue
166
+ m = re.match(r"^(#{1,2})\s+(.*)$", ln) if not in_fence else None
167
+ if m:
168
+ close()
169
+ title = m[2].strip()
170
+ if m[1] == "#":
171
+ p = {"title": title, "intro": "", "chs": []}
172
+ if parts[-1]["title"] is None and not parts[-1]["chs"] and not parts[-1]["intro"]:
173
+ parts[-1] = p
174
+ else:
175
+ parts.append(p)
176
+ target = ("intro", p)
177
+ else:
178
+ t, _, sub = title.partition(" | ")
179
+ c = {"t": t.strip(), "sub": sub.strip(), "html": ""}
180
+ parts[-1]["chs"].append(c)
181
+ target = ("ch", c)
182
+ continue
183
+ cur["buf"].append(ln)
184
+ close()
185
+ return [p for p in parts if p["title"] or p["chs"] or p["intro"]]
186
+
187
+ # ---------------------------------------------------------------- Fonts
188
+
189
+ def embed_fonts(families, cache):
190
+ """Google Fonts CSS with latin/latin-ext woff2 inlined, cached on disk. '' on failure (falls back to <link>)."""
191
+ os.makedirs(cache, exist_ok=True)
192
+ key = re.sub(r"\W+", "_", "-".join(families)) + ".css"
193
+ path = os.path.join(cache, key)
194
+ if os.path.exists(path):
195
+ return open(path).read()
196
+ q = "&".join("family=" + f.replace(" ", "+") + ":ital,wght@0,400;0,600;0,700;1,400" for f in families)
197
+ url = "https://fonts.googleapis.com/css2?%s&display=swap" % q
198
+ ua = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/130 Safari/537.36"}
199
+ try:
200
+ try:
201
+ css = urllib.request.urlopen(urllib.request.Request(url, headers=ua), timeout=20).read().decode()
202
+ except Exception: # a family without italics or weight 600/700
203
+ q = "&".join("family=" + f.replace(" ", "+") for f in families)
204
+ url = "https://fonts.googleapis.com/css2?%s&display=swap" % q
205
+ css = urllib.request.urlopen(urllib.request.Request(url, headers=ua), timeout=20).read().decode()
206
+ keep = []
207
+ for sub, block in re.findall(r"/\* ([\w-]+) \*/\s*(@font-face\s*{[^}]*})", css):
208
+ if sub not in ("latin", "latin-ext"):
209
+ continue
210
+ def inl(m):
211
+ data = urllib.request.urlopen(urllib.request.Request(m[1], headers=ua), timeout=30).read()
212
+ return "url(data:font/woff2;base64,%s)" % base64.b64encode(data).decode()
213
+ keep.append(re.sub(r"url\((https://[^)]+)\)", inl, block))
214
+ out = "\n".join(keep)
215
+ open(path, "w").write(out)
216
+ return out
217
+ except Exception as e:
218
+ print("warning: fonts not embedded (%s); the book loads them online instead" % e, file=sys.stderr)
219
+ return ""
220
+
221
+ # ---------------------------------------------------------------- Build
222
+
223
+ def main():
224
+ args = sys.argv[1:]
225
+ if not args or args[0].startswith("-"):
226
+ sys.exit(__doc__)
227
+ book_dir = args[0]
228
+ out_dir = args[args.index("--out") + 1] if "--out" in args else "output"
229
+ cfg = json.load(open(os.path.join(book_dir, "book.json")))
230
+ styles = json.load(open(os.path.join(SKILL, "assets", "styles.json")))
231
+ style = dict(styles.get(cfg.get("style", "classic"), styles["classic"]))
232
+ for k, v in cfg.get("theme", {}).items(): # per-book overrides
233
+ style[k] = {**style[k], **v} if isinstance(v, dict) and isinstance(style.get(k), dict) else v
234
+ lang = cfg.get("lang", "en")
235
+ labels = {**LABELS.get(lang, LABELS["en"]), **cfg.get("labels", {})}
236
+
237
+ parts = parse(open(os.path.join(book_dir, cfg.get("manuscript", "manuscript.md"))).read(), book_dir)
238
+ if not any(p["chs"] for p in parts):
239
+ sys.exit("error: no chapters found; chapters start with '## '")
240
+
241
+ cover = None
242
+ if cfg.get("cover"):
243
+ p = os.path.join(book_dir, cfg["cover"])
244
+ if os.path.exists(p):
245
+ cover = "data:%s;base64,%s" % (mimetypes.guess_type(p)[0] or "image/jpeg", base64.b64encode(open(p, "rb").read()).decode())
246
+ else:
247
+ print("warning: cover not found: %s; using a typeset cover" % cfg["cover"], file=sys.stderr)
248
+
249
+ fams = list(dict.fromkeys(style["fonts"].values()))
250
+ font_css = "" if "--no-fonts" in args else embed_fonts(fams, os.path.join(os.path.expanduser("~"), ".kaizen", "kaizen-book", "fonts"))
251
+ font_link = "" if font_css else '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?%s&display=swap">' % "&".join(
252
+ "family=" + f.replace(" ", "+") for f in fams)
253
+
254
+ data = {
255
+ "title": cfg["title"], "subtitle": cfg.get("subtitle", ""), "author": cfg.get("author", ""),
256
+ "tagline": cfg.get("tagline", ""), "edition": cfg.get("edition", ""), "lang": lang,
257
+ "key": cfg.get("id") or re.sub(r"\W+", "-", cfg["title"].lower()).strip("-"),
258
+ "cover": cover, "style": style, "labels": labels, "parts": parts,
259
+ "dedication": cfg.get("dedication", ""), "copyright": cfg.get("copyright", ""),
260
+ "volumes": cfg.get("volumes", []),
261
+ "volume_print": cfg.get("volume_print", "mono"),
262
+ }
263
+ tpl = open(os.path.join(SKILL, "assets", "reader.html")).read()
264
+ js = json.dumps(data, ensure_ascii=False).replace("</", "<\\/")
265
+ out = (tpl.replace("/*@@FONTS@@*/", font_css)
266
+ .replace("<!--@@FONTLINK@@-->", font_link)
267
+ .replace("@@LANG@@", html.escape(lang))
268
+ .replace("@@TITLE@@", html.escape(cfg["title"]))
269
+ .replace("@@DESC@@", html.escape(cfg.get("description") or cfg.get("tagline", "")))
270
+ .replace('"@@BOOK@@"', js))
271
+ os.makedirs(out_dir, exist_ok=True)
272
+ dest = os.path.join(out_dir, "book.html")
273
+ open(dest, "w").write(out)
274
+ n = sum(len(p["chs"]) for p in parts)
275
+ print("ok %s — %d parts, %d chapters, %.1f MB" % (dest, len([p for p in parts if p["title"]]), n, len(out.encode()) / 1e6))
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ # Lists what kaizen-book needs and whether it is here. Exit 1 if anything is missing.
3
+ H="${KAIZEN_BOOK_HOME:-$HOME/.kaizen/kaizen-book}"; miss=0
4
+ for c in python3 node npm pdfinfo; do
5
+ if command -v $c >/dev/null 2>&1; then echo "ok $c"; else echo "MISSING $c"; miss=1; fi
6
+ done
7
+ command -v pdfinfo >/dev/null 2>&1 || echo " pdfinfo comes with poppler (poppler-utils): contents page numbers need it"
8
+ if [ -d "$H/node_modules/playwright" ]; then echo "ok playwright ($H)"; else echo "MISSING playwright: run scripts/setup.sh"; miss=1; fi
9
+ command -v pandoc >/dev/null 2>&1 && echo "ok pandoc (optional, .docx import)" || echo "- pandoc not found (optional, only for .docx manuscripts)"
10
+ exit $miss