kasaframe 0.1.1 → 0.1.4

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 (1179) hide show
  1. package/NOTICE +10 -8
  2. package/README.md +16 -1
  3. package/app/server/engine.ts +133 -15
  4. package/app/server/main.ts +7 -3
  5. package/app/web/main.ts +218 -36
  6. package/app/web/style.css +37 -4
  7. package/bin/hooks.mjs +14 -1
  8. package/bin/kf.mjs +20 -2
  9. package/docs/AGENTS.md +64 -16
  10. package/music/README.md +31 -7
  11. package/package.json +2 -2
  12. package/packages/adapters/src/project.ts +5 -1
  13. package/packages/adapters/src/texture.ts +94 -0
  14. package/packages/check/src/check.ts +10 -3
  15. package/packages/check/src/rules/assets.ts +4 -1
  16. package/packages/check/src/rules/frames.ts +2 -1
  17. package/packages/check/src/rules/layout.ts +30 -6
  18. package/packages/cli/package.json +2 -1
  19. package/packages/cli/src/blur.ts +15 -3
  20. package/packages/cli/src/cells.ts +227 -0
  21. package/packages/cli/src/commands/bench.ts +123 -0
  22. package/packages/cli/src/commands/blender.ts +72 -49
  23. package/packages/cli/src/commands/direct.ts +210 -0
  24. package/packages/cli/src/commands/doctor.ts +9 -1
  25. package/packages/cli/src/commands/look.ts +9 -2
  26. package/packages/cli/src/commands/lyrics.ts +47 -1
  27. package/packages/cli/src/commands/make.ts +797 -150
  28. package/packages/cli/src/commands/music.ts +78 -0
  29. package/packages/cli/src/commands/narrate.ts +238 -0
  30. package/packages/cli/src/commands/new.ts +15 -6
  31. package/packages/cli/src/commands/render.ts +94 -60
  32. package/packages/cli/src/commands/series.ts +110 -0
  33. package/packages/cli/src/commands/setup.ts +42 -3
  34. package/packages/cli/src/commands/theme.ts +222 -4
  35. package/packages/cli/src/deps.ts +6 -6
  36. package/packages/cli/src/kf.ts +14 -3
  37. package/packages/cli/src/lock.ts +8 -6
  38. package/packages/cli/src/mix.ts +131 -0
  39. package/packages/cli/src/pick.ts +110 -0
  40. package/packages/cli/src/pool.ts +34 -0
  41. package/packages/cli/src/segments.ts +402 -0
  42. package/packages/cli/src/vary.ts +124 -0
  43. package/packages/core/src/index.ts +1 -1
  44. package/packages/core/src/project.ts +26 -0
  45. package/packages/director/package.json +23 -0
  46. package/packages/director/src/assemble.ts +364 -0
  47. package/packages/director/src/brief.ts +122 -0
  48. package/packages/director/src/catalog.ts +49 -0
  49. package/packages/director/src/content.ts +343 -0
  50. package/packages/director/src/fallback.ts +58 -0
  51. package/packages/director/src/index.ts +13 -0
  52. package/packages/director/src/pattern.ts +90 -0
  53. package/packages/director/src/patterns/fill/cells.ts +115 -0
  54. package/packages/director/src/patterns/formula/stack.ts +93 -0
  55. package/packages/director/src/patterns/util.ts +58 -0
  56. package/packages/director/src/scene.ts +62 -0
  57. package/packages/director/src/select.ts +163 -0
  58. package/packages/director/src/transitions.ts +115 -0
  59. package/packages/gen/PROVIDERS.md +39 -4
  60. package/packages/gen/src/ask.ts +132 -0
  61. package/packages/gen/src/audio.ts +91 -0
  62. package/packages/gen/src/blender.ts +54 -0
  63. package/packages/gen/src/capabilities.ts +151 -0
  64. package/packages/gen/src/generate.ts +4 -2
  65. package/packages/gen/src/image/codex.ts +117 -0
  66. package/packages/gen/src/index.ts +10 -2
  67. package/packages/gen/src/keys.ts +26 -0
  68. package/packages/gen/src/music/elevenlabs.ts +41 -0
  69. package/packages/gen/src/music/index.ts +120 -0
  70. package/packages/gen/src/music/library.ts +73 -0
  71. package/packages/gen/src/music/lyria.ts +52 -0
  72. package/packages/gen/src/music/types.ts +28 -0
  73. package/packages/gen/src/registry.ts +6 -3
  74. package/packages/gen/src/tts/dashscope.ts +64 -0
  75. package/packages/gen/src/tts/elevenlabs.ts +84 -0
  76. package/packages/gen/src/tts/gemini.ts +73 -0
  77. package/packages/gen/src/tts/index.ts +241 -0
  78. package/packages/gen/src/tts/local.ts +42 -0
  79. package/packages/gen/src/tts/types.ts +44 -0
  80. package/packages/gen/src/types.ts +4 -0
  81. package/packages/gen/src/video/h3.ts +5 -0
  82. package/packages/gen/src/video/http.ts +1 -1
  83. package/packages/jizura/src/index.ts +2 -2
  84. package/packages/jizura/src/install.ts +198 -155
  85. package/packages/jizura/src/layer.ts +75 -32
  86. package/packages/jizura/src/pin.ts +4 -1
  87. package/packages/jizura/src/vendor.ts +140 -0
  88. package/packages/jizura/src/vite.ts +34 -7
  89. package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
  90. package/packages/jizura/vendor/NOTICE +43 -0
  91. package/packages/jizura/vendor/engine.json +47 -0
  92. package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
  93. package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
  94. package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
  95. package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
  96. package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  97. package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
  98. package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
  99. package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
  100. package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
  101. package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
  102. package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
  103. package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
  104. package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
  105. package/packages/jizura/vendor/fonts/fonts.json +52 -0
  106. package/packages/jizura/vendor/jizura_engine.js +35024 -0
  107. package/packages/motion/README.md +11 -0
  108. package/packages/motion/package.json +1 -0
  109. package/packages/motion/src/captions.ts +32 -24
  110. package/packages/motion/src/moves/brushWipe.ts +14 -1
  111. package/packages/motion/src/moves/dress.ts +191 -0
  112. package/packages/motion/src/moves/echoStack.ts +3 -3
  113. package/packages/motion/src/moves/fit.ts +36 -0
  114. package/packages/motion/src/moves/ghostStack.ts +14 -5
  115. package/packages/motion/src/moves/imageSwarm.ts +8 -4
  116. package/packages/motion/src/moves/index.ts +23 -4
  117. package/packages/motion/src/moves/karaokeLine.ts +17 -3
  118. package/packages/motion/src/moves/menu.ts +139 -0
  119. package/packages/motion/src/moves/neonSign.ts +2 -1
  120. package/packages/motion/src/moves/slamWords.ts +14 -7
  121. package/packages/motion/src/moves/sparkTrail.ts +16 -0
  122. package/packages/motion/src/moves/splitFlap.ts +4 -2
  123. package/packages/motion/src/moves/stepWords.ts +19 -8
  124. package/packages/motion/src/moves/tickerRows.ts +13 -9
  125. package/packages/motion/src/moves/tokenPicker.ts +4 -3
  126. package/packages/motion/src/moves/wordWall.ts +23 -21
  127. package/packages/motion/src/moves/worldRoll.ts +2 -1
  128. package/packages/motion/src/transition3d/index.ts +10 -3
  129. package/packages/motion/src/transition3d/rig.ts +107 -9
  130. package/packages/motion/src/transition3d/shatter.ts +5 -2
  131. package/packages/motion/src/transition3d/space.ts +173 -0
  132. package/packages/motion/src/transition3d/whip.ts +19 -6
  133. package/packages/motion/src/transition3d/world.ts +54 -0
  134. package/packages/motion/src/transitions2d/catalog.ts +1 -1
  135. package/packages/patterns/package.json +15 -0
  136. package/packages/patterns/src/bars/rank.ts +119 -0
  137. package/packages/patterns/src/before-after/shrink.ts +127 -0
  138. package/packages/patterns/src/before-after/strike.ts +125 -0
  139. package/packages/patterns/src/code/terminal.ts +133 -0
  140. package/packages/patterns/src/compare/bars.ts +142 -0
  141. package/packages/patterns/src/compare/duel.ts +103 -0
  142. package/packages/patterns/src/end/lockup.ts +85 -0
  143. package/packages/patterns/src/end/slam.ts +91 -0
  144. package/packages/patterns/src/flow/graph.ts +250 -0
  145. package/packages/patterns/src/hook-number/counter.ts +101 -0
  146. package/packages/patterns/src/hook-number/roll.ts +179 -0
  147. package/packages/patterns/src/hook-number/slam.ts +91 -0
  148. package/packages/patterns/src/index.ts +38 -0
  149. package/packages/patterns/src/kit.ts +450 -0
  150. package/packages/patterns/src/limit/gauge.ts +117 -0
  151. package/packages/patterns/src/list/cards.ts +131 -0
  152. package/packages/patterns/src/list/cycle.ts +22 -0
  153. package/packages/patterns/src/media/panels.ts +100 -0
  154. package/packages/patterns/src/objects.ts +234 -0
  155. package/packages/patterns/src/quote/karaoke.ts +126 -0
  156. package/packages/patterns/src/quote/slam.ts +22 -0
  157. package/packages/patterns/src/screen/focus.ts +85 -0
  158. package/packages/patterns/src/steps/path.ts +137 -0
  159. package/packages/patterns/src/steps/stack.ts +111 -0
  160. package/packages/patterns/src/term-reveal/chips.ts +92 -0
  161. package/packages/patterns/src/term-reveal/expand.ts +127 -0
  162. package/packages/patterns/src/timeline/axis.ts +144 -0
  163. package/packages/patterns/src/title/mask.ts +85 -0
  164. package/packages/patterns/src/title/slam.ts +23 -0
  165. package/packages/patterns/src/toggle-sim/switch.ts +92 -0
  166. package/packages/patterns/src/trend/columns.ts +108 -0
  167. package/packages/patterns/src/trend/draw.ts +140 -0
  168. package/packages/patterns/src/types.ts +67 -0
  169. package/packages/patterns/src/words/moves.ts +42 -0
  170. package/packages/patterns/src/words/slam.ts +4 -0
  171. package/packages/plan/src/asks.ts +265 -0
  172. package/packages/plan/src/brief.ts +136 -7
  173. package/packages/plan/src/captions.ts +5 -2
  174. package/packages/plan/src/index.ts +7 -3
  175. package/packages/plan/src/llm.ts +64 -2
  176. package/packages/plan/src/look.ts +3 -1
  177. package/packages/plan/src/plan.ts +70 -14
  178. package/packages/plan/src/series.ts +76 -0
  179. package/packages/plan/src/themes.ts +65 -9
  180. package/packages/plan/src/types.ts +56 -2
  181. package/packages/themes/kit/THEME-MAKE.md +1 -0
  182. package/packages/themes/kit/fonts.json +743 -0
  183. package/packages/themes/package.json +10 -2
  184. package/packages/themes/src/color.ts +152 -0
  185. package/packages/themes/src/fx.ts +4 -2
  186. package/packages/themes/src/index.ts +74 -1
  187. package/packages/themes/src/look.ts +223 -0
  188. package/packages/themes/src/name.ts +9 -0
  189. package/packages/themes/src/palette.ts +194 -0
  190. package/packages/themes/src/slotkit.ts +92 -0
  191. package/packages/themes/src/slots.ts +61 -0
  192. package/packages/themes/src/stage3d.ts +20 -0
  193. package/packages/themes/src/types.ts +84 -0
  194. package/packages/themes/src/vary.ts +290 -0
  195. package/series/og-shorts.json +14 -0
  196. package/skills/kasaframe/SKILL.md +4 -3
  197. package/templates/_shared/project/lib/stage.ts +70 -50
  198. package/templates/_shared/scenes/default.ts +10 -7
  199. package/templates/_shared/scenes/drop.ts +11 -8
  200. package/templates/_shared/scenes/end.ts +9 -6
  201. package/templates/_shared/scenes/hook.ts +8 -6
  202. package/templates/_shared/scenes/step.ts +13 -4
  203. package/templates/ad/template.json +20 -1
  204. package/templates/demo/template.json +11 -0
  205. package/templates/explainer/template.json +54 -3
  206. package/templates/launch/template.json +34 -1
  207. package/templates/mv/scenes/default.ts +11 -8
  208. package/templates/mv/template.json +4 -2
  209. package/templates/promo/template.json +42 -1
  210. package/templates/shorts/scenes/hook.ts +8 -5
  211. package/templates/shorts/template.json +72 -3
  212. package/templates/square/template.json +21 -1
  213. package/themes/blueprint/THEME.md +64 -0
  214. package/themes/blueprint/effects.ts +570 -0
  215. package/themes/blueprint/sample/assets/og/logo.svg +12 -0
  216. package/themes/blueprint/sample/beats.json +1005 -0
  217. package/themes/blueprint/sample/index.html +11 -0
  218. package/themes/blueprint/sample/kf.json +59 -0
  219. package/themes/blueprint/sample/lib/facts.ts +29 -0
  220. package/themes/blueprint/sample/lib/sheet.ts +55 -0
  221. package/themes/blueprint/sample/main.ts +9 -0
  222. package/themes/blueprint/sample/scenes/end.ts +91 -0
  223. package/themes/blueprint/sample/scenes/gather.ts +75 -0
  224. package/themes/blueprint/sample/scenes/models.ts +117 -0
  225. package/themes/blueprint/sample/scenes/price.ts +94 -0
  226. package/themes/blueprint/sample/style.json +114 -0
  227. package/themes/blueprint/slots.ts +88 -0
  228. package/themes/blueprint/still.png +0 -0
  229. package/themes/blueprint/style.json +114 -0
  230. package/themes/blueprint/theme.json +137 -0
  231. package/themes/blueprint/variants/deep-rose-2.json +90 -0
  232. package/themes/blueprint/variants/deep-rose-suit.json +117 -0
  233. package/themes/blueprint/variants/deep-rose.json +117 -0
  234. package/themes/blueprint/variants/navy-coral-2.json +118 -0
  235. package/themes/blueprint/variants/navy-coral-suit.json +118 -0
  236. package/themes/blueprint/variants/navy-coral.json +128 -0
  237. package/themes/blueprint/variants/navy-orange-2.json +118 -0
  238. package/themes/blueprint/variants/navy-orange-3.json +91 -0
  239. package/themes/blueprint/variants/navy-orange-4.json +128 -0
  240. package/themes/blueprint/variants/navy-orange-mono.json +128 -0
  241. package/themes/blueprint/variants/navy-orange-plex.json +128 -0
  242. package/themes/blueprint/variants/navy-orange-suit.json +118 -0
  243. package/themes/blueprint/variants/navy-orange.json +118 -0
  244. package/themes/blueprint/variants/navy-rose-plex.json +127 -0
  245. package/themes/blueprint/variants/navy-rose.json +127 -0
  246. package/themes/cartography/THEME.md +61 -0
  247. package/themes/cartography/effects.ts +845 -0
  248. package/themes/cartography/sample/assets/og/logo.svg +12 -0
  249. package/themes/cartography/sample/beats.json +1005 -0
  250. package/themes/cartography/sample/index.html +11 -0
  251. package/themes/cartography/sample/kf.json +59 -0
  252. package/themes/cartography/sample/lib/facts.ts +29 -0
  253. package/themes/cartography/sample/lib/map.ts +39 -0
  254. package/themes/cartography/sample/main.ts +9 -0
  255. package/themes/cartography/sample/scenes/end.ts +56 -0
  256. package/themes/cartography/sample/scenes/gather.ts +46 -0
  257. package/themes/cartography/sample/scenes/models.ts +43 -0
  258. package/themes/cartography/sample/scenes/price.ts +73 -0
  259. package/themes/cartography/sample/style.json +103 -0
  260. package/themes/cartography/slots.ts +87 -0
  261. package/themes/cartography/still.png +0 -0
  262. package/themes/cartography/style.json +103 -0
  263. package/themes/cartography/theme.json +160 -0
  264. package/themes/cartography/variants/blush-amber.json +128 -0
  265. package/themes/cartography/variants/blush-indigo.json +128 -0
  266. package/themes/cartography/variants/blush-pink.json +136 -0
  267. package/themes/cartography/variants/chalk-plum.json +137 -0
  268. package/themes/cartography/variants/chalk-sky.json +100 -0
  269. package/themes/cartography/variants/cocoa-amber.json +128 -0
  270. package/themes/cartography/variants/deep-plum.json +125 -0
  271. package/themes/cartography/variants/dusk-coral.json +128 -0
  272. package/themes/cartography/variants/forest-lime.json +131 -0
  273. package/themes/cartography/variants/frost-teal.json +128 -0
  274. package/themes/cartography/variants/navy-green.json +131 -0
  275. package/themes/cartography/variants/navy-indigo.json +138 -0
  276. package/themes/cartography/variants/navy-sky.json +100 -0
  277. package/themes/cartography/variants/sage-green.json +127 -0
  278. package/themes/cartography/variants/wine-teal.json +128 -0
  279. package/themes/circuit-board/THEME.md +61 -0
  280. package/themes/circuit-board/effects.ts +576 -0
  281. package/themes/circuit-board/pcb.ts +103 -0
  282. package/themes/circuit-board/sample/assets/og/logo.svg +12 -0
  283. package/themes/circuit-board/sample/beats.json +1005 -0
  284. package/themes/circuit-board/sample/index.html +11 -0
  285. package/themes/circuit-board/sample/kf.json +59 -0
  286. package/themes/circuit-board/sample/lib/facts.ts +29 -0
  287. package/themes/circuit-board/sample/lib/kit.ts +45 -0
  288. package/themes/circuit-board/sample/main.ts +9 -0
  289. package/themes/circuit-board/sample/scenes/end.ts +102 -0
  290. package/themes/circuit-board/sample/scenes/gather.ts +76 -0
  291. package/themes/circuit-board/sample/scenes/models.ts +101 -0
  292. package/themes/circuit-board/sample/scenes/price.ts +109 -0
  293. package/themes/circuit-board/sample/style.json +104 -0
  294. package/themes/circuit-board/slots.ts +108 -0
  295. package/themes/circuit-board/still.png +0 -0
  296. package/themes/circuit-board/style.json +104 -0
  297. package/themes/circuit-board/theme.json +149 -0
  298. package/themes/circuit-board/variants/deep-gold-plex.json +133 -0
  299. package/themes/circuit-board/variants/deep-gold.json +123 -0
  300. package/themes/circuit-board/variants/dusk-gold-grain.json +138 -0
  301. package/themes/circuit-board/variants/dusk-gold-pretendard.json +118 -0
  302. package/themes/circuit-board/variants/dusk-gold.json +97 -0
  303. package/themes/circuit-board/variants/forest-gold-2.json +96 -0
  304. package/themes/circuit-board/variants/forest-gold-grain.json +129 -0
  305. package/themes/circuit-board/variants/forest-gold-mono.json +124 -0
  306. package/themes/circuit-board/variants/forest-gold.json +128 -0
  307. package/themes/circuit-board/variants/navy-gold-grain.json +124 -0
  308. package/themes/circuit-board/variants/navy-gold.json +101 -0
  309. package/themes/circuit-board/variants/wine-gold-grain.json +128 -0
  310. package/themes/circuit-board/variants/wine-gold-plex.json +133 -0
  311. package/themes/circuit-board/variants/wine-gold-pretendard.json +118 -0
  312. package/themes/circuit-board/variants/wine-gold.json +128 -0
  313. package/themes/clay-toy/THEME.md +73 -0
  314. package/themes/clay-toy/effects.ts +643 -0
  315. package/themes/clay-toy/glyphs.ts +32 -0
  316. package/themes/clay-toy/sample/assets/og/logo.svg +12 -0
  317. package/themes/clay-toy/sample/beats.json +1005 -0
  318. package/themes/clay-toy/sample/index.html +11 -0
  319. package/themes/clay-toy/sample/kf.json +59 -0
  320. package/themes/clay-toy/sample/lib/clay.ts +20 -0
  321. package/themes/clay-toy/sample/lib/facts.ts +29 -0
  322. package/themes/clay-toy/sample/main.ts +9 -0
  323. package/themes/clay-toy/sample/scenes/end.ts +81 -0
  324. package/themes/clay-toy/sample/scenes/gather.ts +53 -0
  325. package/themes/clay-toy/sample/scenes/models.ts +77 -0
  326. package/themes/clay-toy/sample/scenes/price.ts +91 -0
  327. package/themes/clay-toy/sample/style.json +101 -0
  328. package/themes/clay-toy/slots.ts +132 -0
  329. package/themes/clay-toy/still.png +0 -0
  330. package/themes/clay-toy/style.json +101 -0
  331. package/themes/clay-toy/theme.json +146 -0
  332. package/themes/clay-toy/variants/blush-coral-grain.json +126 -0
  333. package/themes/clay-toy/variants/blush-coral.json +123 -0
  334. package/themes/clay-toy/variants/chalk-indigo.json +97 -0
  335. package/themes/clay-toy/variants/chalk-jade.json +125 -0
  336. package/themes/clay-toy/variants/chalk-teal.json +120 -0
  337. package/themes/clay-toy/variants/frost-orange.json +125 -0
  338. package/themes/clay-toy/variants/frost-sky-grain.json +126 -0
  339. package/themes/clay-toy/variants/frost-sky.json +121 -0
  340. package/themes/clay-toy/variants/frost-teal.json +120 -0
  341. package/themes/clay-toy/variants/lilac-green.json +120 -0
  342. package/themes/clay-toy/variants/lilac-plum-round.json +120 -0
  343. package/themes/clay-toy/variants/lilac-plum.json +124 -0
  344. package/themes/clay-toy/variants/sage-indigo.json +125 -0
  345. package/themes/clay-toy/variants/sage-magenta.json +119 -0
  346. package/themes/clay-toy/variants/sand-amber.json +125 -0
  347. package/themes/conveyor-factory/THEME.md +71 -0
  348. package/themes/conveyor-factory/effects.ts +744 -0
  349. package/themes/conveyor-factory/sample/assets/og/logo.svg +12 -0
  350. package/themes/conveyor-factory/sample/beats.json +1005 -0
  351. package/themes/conveyor-factory/sample/index.html +11 -0
  352. package/themes/conveyor-factory/sample/kf.json +59 -0
  353. package/themes/conveyor-factory/sample/lib/factory.ts +48 -0
  354. package/themes/conveyor-factory/sample/lib/facts.ts +29 -0
  355. package/themes/conveyor-factory/sample/main.ts +9 -0
  356. package/themes/conveyor-factory/sample/scenes/end.ts +44 -0
  357. package/themes/conveyor-factory/sample/scenes/gather.ts +47 -0
  358. package/themes/conveyor-factory/sample/scenes/models.ts +91 -0
  359. package/themes/conveyor-factory/sample/scenes/price.ts +77 -0
  360. package/themes/conveyor-factory/sample/style.json +107 -0
  361. package/themes/conveyor-factory/slots.ts +121 -0
  362. package/themes/conveyor-factory/still.png +0 -0
  363. package/themes/conveyor-factory/style.json +107 -0
  364. package/themes/conveyor-factory/theme.json +142 -0
  365. package/themes/conveyor-factory/variants/cocoa-gold.json +125 -0
  366. package/themes/conveyor-factory/variants/dusk-gold.json +135 -0
  367. package/themes/conveyor-factory/variants/forest-gold.json +135 -0
  368. package/themes/conveyor-factory/variants/navy-amber.json +93 -0
  369. package/themes/conveyor-factory/variants/navy-gold.json +120 -0
  370. package/themes/conveyor-factory/variants/slate-amber-2.json +120 -0
  371. package/themes/conveyor-factory/variants/slate-amber-grain.json +125 -0
  372. package/themes/conveyor-factory/variants/slate-amber-suit.json +120 -0
  373. package/themes/conveyor-factory/variants/slate-amber.json +125 -0
  374. package/themes/conveyor-factory/variants/slate-gold-bold.json +120 -0
  375. package/themes/conveyor-factory/variants/slate-gold.json +120 -0
  376. package/themes/data-journal/THEME.md +74 -0
  377. package/themes/data-journal/effects.ts +570 -0
  378. package/themes/data-journal/sample/assets/og/logo.svg +12 -0
  379. package/themes/data-journal/sample/beats.json +1005 -0
  380. package/themes/data-journal/sample/index.html +11 -0
  381. package/themes/data-journal/sample/kf.json +59 -0
  382. package/themes/data-journal/sample/lib/facts.ts +29 -0
  383. package/themes/data-journal/sample/lib/page.ts +25 -0
  384. package/themes/data-journal/sample/main.ts +9 -0
  385. package/themes/data-journal/sample/scenes/end.ts +69 -0
  386. package/themes/data-journal/sample/scenes/gather.ts +58 -0
  387. package/themes/data-journal/sample/scenes/models.ts +51 -0
  388. package/themes/data-journal/sample/scenes/price.ts +73 -0
  389. package/themes/data-journal/sample/style.json +114 -0
  390. package/themes/data-journal/slots.ts +119 -0
  391. package/themes/data-journal/still.png +0 -0
  392. package/themes/data-journal/style.json +114 -0
  393. package/themes/data-journal/theme.json +140 -0
  394. package/themes/data-journal/variants/blush-green.json +129 -0
  395. package/themes/data-journal/variants/chalk-magenta.json +138 -0
  396. package/themes/data-journal/variants/cocoa-amber.json +140 -0
  397. package/themes/data-journal/variants/cocoa-sky.json +130 -0
  398. package/themes/data-journal/variants/deep-coral.json +135 -0
  399. package/themes/data-journal/variants/dusk-plum.json +132 -0
  400. package/themes/data-journal/variants/forest-green.json +138 -0
  401. package/themes/data-journal/variants/frost-indigo.json +141 -0
  402. package/themes/data-journal/variants/lilac-amber.json +141 -0
  403. package/themes/data-journal/variants/navy-indigo.json +131 -0
  404. package/themes/data-journal/variants/navy-lime.json +128 -0
  405. package/themes/data-journal/variants/snow-sky.json +125 -0
  406. package/themes/data-journal/variants/wine-indigo.json +129 -0
  407. package/themes/data-journal/variants/wine-magenta.json +138 -0
  408. package/themes/data-journal/variants/wine-teal.json +97 -0
  409. package/themes/debug-hud/slots.ts +99 -0
  410. package/themes/debug-hud/theme.json +56 -0
  411. package/themes/debug-hud/variants/cocoa-gold.json +113 -0
  412. package/themes/debug-hud/variants/cocoa-teal.json +119 -0
  413. package/themes/debug-hud/variants/deep-cyan.json +85 -0
  414. package/themes/debug-hud/variants/deep-teal-scan.json +118 -0
  415. package/themes/debug-hud/variants/deep-teal.json +112 -0
  416. package/themes/debug-hud/variants/dusk-plum.json +116 -0
  417. package/themes/debug-hud/variants/dusk-violet.json +112 -0
  418. package/themes/debug-hud/variants/forest-amber.json +86 -0
  419. package/themes/debug-hud/variants/forest-lime.json +117 -0
  420. package/themes/debug-hud/variants/forest-magenta.json +111 -0
  421. package/themes/debug-hud/variants/navy-jade.json +79 -0
  422. package/themes/debug-hud/variants/slate-green.json +112 -0
  423. package/themes/debug-hud/variants/wine-gold.json +119 -0
  424. package/themes/debug-hud/variants/wine-jade.json +83 -0
  425. package/themes/debug-hud/variants/wine-sky.json +112 -0
  426. package/themes/depth-flight/THEME.md +54 -0
  427. package/themes/depth-flight/effects.ts +396 -0
  428. package/themes/depth-flight/sample/assets/og/logo.svg +12 -0
  429. package/themes/depth-flight/sample/beats.json +1005 -0
  430. package/themes/depth-flight/sample/index.html +11 -0
  431. package/themes/depth-flight/sample/kf.json +59 -0
  432. package/themes/depth-flight/sample/lib/facts.ts +29 -0
  433. package/themes/depth-flight/sample/lib/fly.ts +15 -0
  434. package/themes/depth-flight/sample/main.ts +9 -0
  435. package/themes/depth-flight/sample/scenes/end.ts +51 -0
  436. package/themes/depth-flight/sample/scenes/gather.ts +40 -0
  437. package/themes/depth-flight/sample/scenes/models.ts +75 -0
  438. package/themes/depth-flight/sample/scenes/price.ts +50 -0
  439. package/themes/depth-flight/sample/style.json +94 -0
  440. package/themes/depth-flight/slots.ts +196 -0
  441. package/themes/depth-flight/still.png +0 -0
  442. package/themes/depth-flight/style.json +94 -0
  443. package/themes/depth-flight/theme.json +119 -0
  444. package/themes/depth-flight/variants/cocoa-cyan.json +121 -0
  445. package/themes/depth-flight/variants/cocoa-indigo.json +121 -0
  446. package/themes/depth-flight/variants/cocoa-violet.json +122 -0
  447. package/themes/depth-flight/variants/deep-sky.json +126 -0
  448. package/themes/depth-flight/variants/dusk-violet-bold.json +117 -0
  449. package/themes/depth-flight/variants/dusk-violet.json +132 -0
  450. package/themes/depth-flight/variants/forest-blue.json +116 -0
  451. package/themes/depth-flight/variants/forest-cyan.json +122 -0
  452. package/themes/depth-flight/variants/navy-sky-grain.json +122 -0
  453. package/themes/depth-flight/variants/navy-sky.json +116 -0
  454. package/themes/depth-flight/variants/night-teal.json +116 -0
  455. package/themes/depth-flight/variants/slate-cyan.json +127 -0
  456. package/themes/depth-flight/variants/slate-indigo.json +116 -0
  457. package/themes/depth-flight/variants/slate-sky.json +94 -0
  458. package/themes/depth-flight/variants/slate-violet.json +121 -0
  459. package/themes/film-reel/THEME.md +60 -0
  460. package/themes/film-reel/effects.ts +477 -0
  461. package/themes/film-reel/sample/assets/og/logo.svg +12 -0
  462. package/themes/film-reel/sample/beats.json +1005 -0
  463. package/themes/film-reel/sample/index.html +11 -0
  464. package/themes/film-reel/sample/kf.json +59 -0
  465. package/themes/film-reel/sample/lib/facts.ts +29 -0
  466. package/themes/film-reel/sample/lib/reel.ts +9 -0
  467. package/themes/film-reel/sample/main.ts +9 -0
  468. package/themes/film-reel/sample/scenes/end.ts +31 -0
  469. package/themes/film-reel/sample/scenes/gather.ts +40 -0
  470. package/themes/film-reel/sample/scenes/models.ts +37 -0
  471. package/themes/film-reel/sample/scenes/price.ts +63 -0
  472. package/themes/film-reel/sample/style.json +103 -0
  473. package/themes/film-reel/slots.ts +75 -0
  474. package/themes/film-reel/still.png +0 -0
  475. package/themes/film-reel/style.json +103 -0
  476. package/themes/film-reel/theme.json +142 -0
  477. package/themes/film-reel/variants/chalk-amber.json +124 -0
  478. package/themes/film-reel/variants/chalk-pink.json +123 -0
  479. package/themes/film-reel/variants/cream-violet.json +121 -0
  480. package/themes/film-reel/variants/frost-blue.json +126 -0
  481. package/themes/film-reel/variants/frost-green.json +123 -0
  482. package/themes/film-reel/variants/frost-indigo.json +121 -0
  483. package/themes/film-reel/variants/frost-jade.json +120 -0
  484. package/themes/film-reel/variants/frost-plum.json +120 -0
  485. package/themes/film-reel/variants/frost-teal.json +93 -0
  486. package/themes/film-reel/variants/lilac-gold.json +120 -0
  487. package/themes/film-reel/variants/lilac-jade.json +123 -0
  488. package/themes/film-reel/variants/lilac-magenta.json +125 -0
  489. package/themes/film-reel/variants/lilac-violet.json +122 -0
  490. package/themes/film-reel/variants/sand-cyan.json +124 -0
  491. package/themes/film-reel/variants/sand-lime.json +96 -0
  492. package/themes/glass-light/THEME.md +55 -0
  493. package/themes/glass-light/effects.ts +433 -0
  494. package/themes/glass-light/sample/assets/og/logo.svg +12 -0
  495. package/themes/glass-light/sample/beats.json +1005 -0
  496. package/themes/glass-light/sample/index.html +11 -0
  497. package/themes/glass-light/sample/kf.json +59 -0
  498. package/themes/glass-light/sample/lib/facts.ts +29 -0
  499. package/themes/glass-light/sample/lib/glass.ts +81 -0
  500. package/themes/glass-light/sample/main.ts +9 -0
  501. package/themes/glass-light/sample/scenes/end.ts +43 -0
  502. package/themes/glass-light/sample/scenes/gather.ts +38 -0
  503. package/themes/glass-light/sample/scenes/models.ts +46 -0
  504. package/themes/glass-light/sample/scenes/price.ts +46 -0
  505. package/themes/glass-light/sample/style.json +94 -0
  506. package/themes/glass-light/slots.ts +198 -0
  507. package/themes/glass-light/still.png +0 -0
  508. package/themes/glass-light/style.json +94 -0
  509. package/themes/glass-light/theme.json +119 -0
  510. package/themes/glass-light/variants/blush-coral.json +116 -0
  511. package/themes/glass-light/variants/blush-orange.json +115 -0
  512. package/themes/glass-light/variants/blush-pink.json +119 -0
  513. package/themes/glass-light/variants/blush-plum.json +115 -0
  514. package/themes/glass-light/variants/blush-teal.json +115 -0
  515. package/themes/glass-light/variants/cream-blue.json +125 -0
  516. package/themes/glass-light/variants/cream-pink.json +87 -0
  517. package/themes/glass-light/variants/cream-violet.json +119 -0
  518. package/themes/glass-light/variants/frost-green.json +115 -0
  519. package/themes/glass-light/variants/frost-sky-suit.json +115 -0
  520. package/themes/glass-light/variants/frost-sky.json +121 -0
  521. package/themes/glass-light/variants/lilac-gold.json +126 -0
  522. package/themes/glass-light/variants/lilac-plum.json +114 -0
  523. package/themes/glass-light/variants/sage-green.json +119 -0
  524. package/themes/glass-light/variants/sage-rose.json +114 -0
  525. package/themes/ink-wash/THEME.md +59 -0
  526. package/themes/ink-wash/effects.ts +574 -0
  527. package/themes/ink-wash/sample/assets/og/logo.svg +12 -0
  528. package/themes/ink-wash/sample/beats.json +1005 -0
  529. package/themes/ink-wash/sample/index.html +11 -0
  530. package/themes/ink-wash/sample/kf.json +59 -0
  531. package/themes/ink-wash/sample/lib/facts.ts +29 -0
  532. package/themes/ink-wash/sample/lib/ink.ts +35 -0
  533. package/themes/ink-wash/sample/main.ts +9 -0
  534. package/themes/ink-wash/sample/scenes/end.ts +72 -0
  535. package/themes/ink-wash/sample/scenes/gather.ts +53 -0
  536. package/themes/ink-wash/sample/scenes/models.ts +58 -0
  537. package/themes/ink-wash/sample/scenes/price.ts +85 -0
  538. package/themes/ink-wash/sample/style.json +108 -0
  539. package/themes/ink-wash/slots.ts +150 -0
  540. package/themes/ink-wash/still.png +0 -0
  541. package/themes/ink-wash/style.json +108 -0
  542. package/themes/ink-wash/theme.json +131 -0
  543. package/themes/ink-wash/variants/blush-orange.json +125 -0
  544. package/themes/ink-wash/variants/blush-rose.json +124 -0
  545. package/themes/ink-wash/variants/chalk-coral-grain.json +125 -0
  546. package/themes/ink-wash/variants/chalk-coral-serif.json +118 -0
  547. package/themes/ink-wash/variants/chalk-coral.json +120 -0
  548. package/themes/ink-wash/variants/chalk-orange-grain.json +97 -0
  549. package/themes/ink-wash/variants/chalk-orange-serif.json +120 -0
  550. package/themes/ink-wash/variants/chalk-orange.json +120 -0
  551. package/themes/ink-wash/variants/frost-coral.json +119 -0
  552. package/themes/ink-wash/variants/frost-rose.json +91 -0
  553. package/themes/ink-wash/variants/lilac-coral.json +96 -0
  554. package/themes/ink-wash/variants/sage-coral.json +119 -0
  555. package/themes/ink-wash/variants/sage-rose.json +119 -0
  556. package/themes/ink-wash/variants/sand-coral.json +125 -0
  557. package/themes/ink-wash/variants/sand-orange.json +120 -0
  558. package/themes/iso-diorama/THEME.md +54 -0
  559. package/themes/iso-diorama/effects.ts +374 -0
  560. package/themes/iso-diorama/sample/assets/og/logo.svg +12 -0
  561. package/themes/iso-diorama/sample/beats.json +1005 -0
  562. package/themes/iso-diorama/sample/index.html +11 -0
  563. package/themes/iso-diorama/sample/kf.json +59 -0
  564. package/themes/iso-diorama/sample/lib/facts.ts +29 -0
  565. package/themes/iso-diorama/sample/lib/iso.ts +22 -0
  566. package/themes/iso-diorama/sample/main.ts +9 -0
  567. package/themes/iso-diorama/sample/scenes/end.ts +41 -0
  568. package/themes/iso-diorama/sample/scenes/gather.ts +39 -0
  569. package/themes/iso-diorama/sample/scenes/models.ts +55 -0
  570. package/themes/iso-diorama/sample/scenes/price.ts +49 -0
  571. package/themes/iso-diorama/sample/style.json +98 -0
  572. package/themes/iso-diorama/slots.ts +222 -0
  573. package/themes/iso-diorama/still.png +0 -0
  574. package/themes/iso-diorama/style.json +98 -0
  575. package/themes/iso-diorama/theme.json +131 -0
  576. package/themes/iso-diorama/variants/chalk-plum.json +121 -0
  577. package/themes/iso-diorama/variants/cocoa-gold.json +93 -0
  578. package/themes/iso-diorama/variants/cocoa-indigo.json +122 -0
  579. package/themes/iso-diorama/variants/cocoa-rose.json +125 -0
  580. package/themes/iso-diorama/variants/deep-green.json +125 -0
  581. package/themes/iso-diorama/variants/deep-plum.json +120 -0
  582. package/themes/iso-diorama/variants/dusk-blue.json +126 -0
  583. package/themes/iso-diorama/variants/dusk-coral.json +122 -0
  584. package/themes/iso-diorama/variants/dusk-jade.json +121 -0
  585. package/themes/iso-diorama/variants/forest-cyan.json +127 -0
  586. package/themes/iso-diorama/variants/frost-blue.json +126 -0
  587. package/themes/iso-diorama/variants/frost-green.json +126 -0
  588. package/themes/iso-diorama/variants/frost-orange.json +125 -0
  589. package/themes/iso-diorama/variants/frost-teal.json +123 -0
  590. package/themes/iso-diorama/variants/navy-amber.json +125 -0
  591. package/themes/isotype/THEME.md +58 -0
  592. package/themes/isotype/effects.ts +297 -0
  593. package/themes/isotype/kit.ts +102 -0
  594. package/themes/isotype/sample/assets/og/logo.svg +12 -0
  595. package/themes/isotype/sample/beats.json +1005 -0
  596. package/themes/isotype/sample/index.html +11 -0
  597. package/themes/isotype/sample/kf.json +59 -0
  598. package/themes/isotype/sample/lib/chart.ts +47 -0
  599. package/themes/isotype/sample/lib/facts.ts +29 -0
  600. package/themes/isotype/sample/main.ts +9 -0
  601. package/themes/isotype/sample/scenes/end.ts +80 -0
  602. package/themes/isotype/sample/scenes/gather.ts +65 -0
  603. package/themes/isotype/sample/scenes/models.ts +75 -0
  604. package/themes/isotype/sample/scenes/price.ts +90 -0
  605. package/themes/isotype/sample/style.json +101 -0
  606. package/themes/isotype/slots.ts +74 -0
  607. package/themes/isotype/still.png +0 -0
  608. package/themes/isotype/style.json +101 -0
  609. package/themes/isotype/theme.json +140 -0
  610. package/themes/isotype/variants/blush-rose.json +127 -0
  611. package/themes/isotype/variants/chalk-amber.json +123 -0
  612. package/themes/isotype/variants/chalk-coral-bold.json +118 -0
  613. package/themes/isotype/variants/chalk-coral.json +117 -0
  614. package/themes/isotype/variants/chalk-orange.json +128 -0
  615. package/themes/isotype/variants/frost-amber-plex.json +128 -0
  616. package/themes/isotype/variants/frost-amber.json +123 -0
  617. package/themes/isotype/variants/frost-coral-grain.json +95 -0
  618. package/themes/isotype/variants/frost-coral.json +123 -0
  619. package/themes/isotype/variants/frost-orange.json +123 -0
  620. package/themes/isotype/variants/lilac-rose.json +127 -0
  621. package/themes/isotype/variants/sage-coral.json +90 -0
  622. package/themes/isotype/variants/sand-amber.json +118 -0
  623. package/themes/isotype/variants/sand-orange.json +128 -0
  624. package/themes/kawaii-sticker/THEME.md +44 -0
  625. package/themes/kawaii-sticker/effects.ts +412 -0
  626. package/themes/kawaii-sticker/sample/assets/stickers/bolt.png +0 -0
  627. package/themes/kawaii-sticker/sample/assets/stickers/bubble.png +0 -0
  628. package/themes/kawaii-sticker/sample/assets/stickers/clock.png +0 -0
  629. package/themes/kawaii-sticker/sample/assets/stickers/coin.png +0 -0
  630. package/themes/kawaii-sticker/sample/assets/stickers/moon.png +0 -0
  631. package/themes/kawaii-sticker/sample/assets/stickers/og-logo.svg +12 -0
  632. package/themes/kawaii-sticker/sample/assets/stickers/server.png +0 -0
  633. package/themes/kawaii-sticker/sample/beats.json +1 -0
  634. package/themes/kawaii-sticker/sample/index.html +11 -0
  635. package/themes/kawaii-sticker/sample/kf.json +66 -0
  636. package/themes/kawaii-sticker/sample/lib/og.ts +54 -0
  637. package/themes/kawaii-sticker/sample/main.ts +9 -0
  638. package/themes/kawaii-sticker/sample/scenes/end.ts +37 -0
  639. package/themes/kawaii-sticker/sample/scenes/gather.ts +50 -0
  640. package/themes/kawaii-sticker/sample/scenes/models.ts +43 -0
  641. package/themes/kawaii-sticker/sample/scenes/one.ts +45 -0
  642. package/themes/kawaii-sticker/sample/scenes/price.ts +47 -0
  643. package/themes/kawaii-sticker/sample/style.json +99 -0
  644. package/themes/kawaii-sticker/slots.ts +95 -0
  645. package/themes/kawaii-sticker/still.png +0 -0
  646. package/themes/kawaii-sticker/style.json +99 -0
  647. package/themes/kawaii-sticker/theme.json +145 -0
  648. package/themes/kawaii-sticker/variants/blush-coral.json +134 -0
  649. package/themes/kawaii-sticker/variants/blush-teal.json +129 -0
  650. package/themes/kawaii-sticker/variants/blush-violet.json +134 -0
  651. package/themes/kawaii-sticker/variants/cream-gold.json +135 -0
  652. package/themes/kawaii-sticker/variants/deep-jade.json +134 -0
  653. package/themes/kawaii-sticker/variants/dusk-green.json +134 -0
  654. package/themes/kawaii-sticker/variants/dusk-indigo.json +130 -0
  655. package/themes/kawaii-sticker/variants/dusk-sky.json +135 -0
  656. package/themes/kawaii-sticker/variants/dusk-teal.json +134 -0
  657. package/themes/kawaii-sticker/variants/frost-blue.json +135 -0
  658. package/themes/kawaii-sticker/variants/frost-plum.json +133 -0
  659. package/themes/kawaii-sticker/variants/lilac-violet.json +133 -0
  660. package/themes/kawaii-sticker/variants/navy-gold.json +134 -0
  661. package/themes/kawaii-sticker/variants/navy-plum.json +134 -0
  662. package/themes/kawaii-sticker/variants/sage-sky.json +107 -0
  663. package/themes/lab-notebook/THEME.md +56 -0
  664. package/themes/lab-notebook/effects.ts +423 -0
  665. package/themes/lab-notebook/kit.ts +84 -0
  666. package/themes/lab-notebook/sample/assets/og/logo.svg +12 -0
  667. package/themes/lab-notebook/sample/beats.json +1005 -0
  668. package/themes/lab-notebook/sample/index.html +11 -0
  669. package/themes/lab-notebook/sample/kf.json +59 -0
  670. package/themes/lab-notebook/sample/lib/facts.ts +29 -0
  671. package/themes/lab-notebook/sample/lib/note.ts +53 -0
  672. package/themes/lab-notebook/sample/main.ts +9 -0
  673. package/themes/lab-notebook/sample/scenes/end.ts +127 -0
  674. package/themes/lab-notebook/sample/scenes/gather.ts +83 -0
  675. package/themes/lab-notebook/sample/scenes/models.ts +73 -0
  676. package/themes/lab-notebook/sample/scenes/price.ts +89 -0
  677. package/themes/lab-notebook/sample/style.json +109 -0
  678. package/themes/lab-notebook/slots.ts +75 -0
  679. package/themes/lab-notebook/still.png +0 -0
  680. package/themes/lab-notebook/style.json +109 -0
  681. package/themes/lab-notebook/theme.json +141 -0
  682. package/themes/lab-notebook/variants/blush-orange.json +125 -0
  683. package/themes/lab-notebook/variants/chalk-coral.json +126 -0
  684. package/themes/lab-notebook/variants/chalk-orange.json +129 -0
  685. package/themes/lab-notebook/variants/chalk-rose-2.json +96 -0
  686. package/themes/lab-notebook/variants/chalk-rose.json +125 -0
  687. package/themes/lab-notebook/variants/frost-orange.json +101 -0
  688. package/themes/lab-notebook/variants/frost-rose-grain.json +101 -0
  689. package/themes/lab-notebook/variants/frost-rose.json +96 -0
  690. package/themes/lab-notebook/variants/lilac-rose-2.json +96 -0
  691. package/themes/lab-notebook/variants/lilac-rose.json +128 -0
  692. package/themes/lab-notebook/variants/sage-rose.json +124 -0
  693. package/themes/lab-notebook/variants/sand-orange-grain.json +101 -0
  694. package/themes/lab-notebook/variants/sand-orange.json +129 -0
  695. package/themes/lab-notebook/variants/snow-orange.json +129 -0
  696. package/themes/lab-notebook/variants/snow-rose.json +124 -0
  697. package/themes/manga-panel/THEME.md +60 -0
  698. package/themes/manga-panel/effects.ts +791 -0
  699. package/themes/manga-panel/sample/assets/og/logo.svg +12 -0
  700. package/themes/manga-panel/sample/beats.json +1005 -0
  701. package/themes/manga-panel/sample/index.html +11 -0
  702. package/themes/manga-panel/sample/kf.json +59 -0
  703. package/themes/manga-panel/sample/lib/facts.ts +29 -0
  704. package/themes/manga-panel/sample/lib/manga.ts +58 -0
  705. package/themes/manga-panel/sample/main.ts +9 -0
  706. package/themes/manga-panel/sample/scenes/end.ts +96 -0
  707. package/themes/manga-panel/sample/scenes/gather.ts +100 -0
  708. package/themes/manga-panel/sample/scenes/models.ts +96 -0
  709. package/themes/manga-panel/sample/scenes/price.ts +154 -0
  710. package/themes/manga-panel/sample/style.json +105 -0
  711. package/themes/manga-panel/slots.ts +127 -0
  712. package/themes/manga-panel/still.png +0 -0
  713. package/themes/manga-panel/style.json +105 -0
  714. package/themes/manga-panel/theme.json +148 -0
  715. package/themes/manga-panel/variants/chalk-cyan-dots.json +122 -0
  716. package/themes/manga-panel/variants/chalk-cyan.json +122 -0
  717. package/themes/manga-panel/variants/chalk-magenta.json +121 -0
  718. package/themes/manga-panel/variants/chalk-orange-bold.json +117 -0
  719. package/themes/manga-panel/variants/chalk-orange.json +122 -0
  720. package/themes/manga-panel/variants/chalk-rose-dots.json +121 -0
  721. package/themes/manga-panel/variants/chalk-rose.json +116 -0
  722. package/themes/manga-panel/variants/chalk-sky-bold.json +117 -0
  723. package/themes/manga-panel/variants/chalk-sky.json +117 -0
  724. package/themes/manga-panel/variants/snow-cyan.json +117 -0
  725. package/themes/manga-panel/variants/snow-magenta-dots.json +121 -0
  726. package/themes/manga-panel/variants/snow-magenta.json +116 -0
  727. package/themes/manga-panel/variants/snow-rose-dots.json +121 -0
  728. package/themes/manga-panel/variants/snow-rose.json +116 -0
  729. package/themes/manga-panel/variants/snow-sky.json +95 -0
  730. package/themes/marble-run/THEME.md +73 -0
  731. package/themes/marble-run/effects.ts +678 -0
  732. package/themes/marble-run/sample/assets/og/logo.svg +12 -0
  733. package/themes/marble-run/sample/beats.json +1005 -0
  734. package/themes/marble-run/sample/index.html +11 -0
  735. package/themes/marble-run/sample/kf.json +59 -0
  736. package/themes/marble-run/sample/lib/facts.ts +29 -0
  737. package/themes/marble-run/sample/lib/run.ts +30 -0
  738. package/themes/marble-run/sample/main.ts +9 -0
  739. package/themes/marble-run/sample/scenes/end.ts +58 -0
  740. package/themes/marble-run/sample/scenes/gather.ts +51 -0
  741. package/themes/marble-run/sample/scenes/models.ts +73 -0
  742. package/themes/marble-run/sample/scenes/price.ts +51 -0
  743. package/themes/marble-run/sample/style.json +109 -0
  744. package/themes/marble-run/slots.ts +116 -0
  745. package/themes/marble-run/still.png +0 -0
  746. package/themes/marble-run/style.json +109 -0
  747. package/themes/marble-run/theme.json +145 -0
  748. package/themes/marble-run/variants/cocoa-amber.json +128 -0
  749. package/themes/marble-run/variants/cocoa-orange.json +142 -0
  750. package/themes/marble-run/variants/dusk-orange.json +133 -0
  751. package/themes/marble-run/variants/forest-gold.json +128 -0
  752. package/themes/marble-run/variants/navy-gold.json +137 -0
  753. package/themes/marble-run/variants/night-gold-grain.json +105 -0
  754. package/themes/marble-run/variants/night-gold.json +137 -0
  755. package/themes/marble-run/variants/slate-gold-grain.json +133 -0
  756. package/themes/marble-run/variants/slate-gold.json +133 -0
  757. package/themes/marble-run/variants/slate-orange-grain.json +105 -0
  758. package/themes/marble-run/variants/slate-orange-mono.json +128 -0
  759. package/themes/marble-run/variants/slate-orange-serif.json +128 -0
  760. package/themes/marble-run/variants/slate-orange.json +131 -0
  761. package/themes/neon-sign/THEME.md +59 -0
  762. package/themes/neon-sign/effects.ts +424 -0
  763. package/themes/neon-sign/sample/assets/og/logo.svg +12 -0
  764. package/themes/neon-sign/sample/beats.json +1005 -0
  765. package/themes/neon-sign/sample/index.html +11 -0
  766. package/themes/neon-sign/sample/kf.json +59 -0
  767. package/themes/neon-sign/sample/lib/facts.ts +29 -0
  768. package/themes/neon-sign/sample/lib/neon.ts +50 -0
  769. package/themes/neon-sign/sample/main.ts +9 -0
  770. package/themes/neon-sign/sample/scenes/end.ts +62 -0
  771. package/themes/neon-sign/sample/scenes/gather.ts +59 -0
  772. package/themes/neon-sign/sample/scenes/models.ts +63 -0
  773. package/themes/neon-sign/sample/scenes/price.ts +74 -0
  774. package/themes/neon-sign/sample/style.json +104 -0
  775. package/themes/neon-sign/slots.ts +110 -0
  776. package/themes/neon-sign/still.png +0 -0
  777. package/themes/neon-sign/style.json +104 -0
  778. package/themes/neon-sign/theme.json +136 -0
  779. package/themes/neon-sign/variants/deep-plum.json +121 -0
  780. package/themes/neon-sign/variants/deep-violet.json +121 -0
  781. package/themes/neon-sign/variants/dusk-plum.json +121 -0
  782. package/themes/neon-sign/variants/forest-magenta.json +126 -0
  783. package/themes/neon-sign/variants/forest-plum.json +98 -0
  784. package/themes/neon-sign/variants/night-coral.json +122 -0
  785. package/themes/neon-sign/variants/night-magenta-round.json +121 -0
  786. package/themes/neon-sign/variants/night-magenta.json +126 -0
  787. package/themes/neon-sign/variants/night-pink.json +125 -0
  788. package/themes/neon-sign/variants/night-violet.json +126 -0
  789. package/themes/neon-sign/variants/slate-plum.json +120 -0
  790. package/themes/neon-sign/variants/slate-violet.json +127 -0
  791. package/themes/neon-sign/variants/wine-coral.json +127 -0
  792. package/themes/neon-sign/variants/wine-pink.json +125 -0
  793. package/themes/neon-sign/variants/wine-plum.json +126 -0
  794. package/themes/newspaper/THEME.md +67 -0
  795. package/themes/newspaper/effects.ts +347 -0
  796. package/themes/newspaper/sample/assets/og/logo.svg +12 -0
  797. package/themes/newspaper/sample/beats.json +1005 -0
  798. package/themes/newspaper/sample/index.html +11 -0
  799. package/themes/newspaper/sample/kf.json +59 -0
  800. package/themes/newspaper/sample/lib/facts.ts +29 -0
  801. package/themes/newspaper/sample/lib/page.ts +27 -0
  802. package/themes/newspaper/sample/main.ts +9 -0
  803. package/themes/newspaper/sample/scenes/end.ts +62 -0
  804. package/themes/newspaper/sample/scenes/gather.ts +66 -0
  805. package/themes/newspaper/sample/scenes/models.ts +61 -0
  806. package/themes/newspaper/sample/scenes/price.ts +103 -0
  807. package/themes/newspaper/sample/style.json +100 -0
  808. package/themes/newspaper/slots.ts +73 -0
  809. package/themes/newspaper/still.png +0 -0
  810. package/themes/newspaper/style.json +100 -0
  811. package/themes/newspaper/theme.json +140 -0
  812. package/themes/newspaper/variants/blush-amber.json +123 -0
  813. package/themes/newspaper/variants/blush-orange.json +125 -0
  814. package/themes/newspaper/variants/chalk-cyan.json +119 -0
  815. package/themes/newspaper/variants/chalk-indigo.json +124 -0
  816. package/themes/newspaper/variants/chalk-jade.json +95 -0
  817. package/themes/newspaper/variants/chalk-magenta.json +127 -0
  818. package/themes/newspaper/variants/frost-plum.json +123 -0
  819. package/themes/newspaper/variants/frost-sky.json +129 -0
  820. package/themes/newspaper/variants/frost-violet.json +96 -0
  821. package/themes/newspaper/variants/lilac-coral.json +124 -0
  822. package/themes/newspaper/variants/lilac-green.json +127 -0
  823. package/themes/newspaper/variants/sage-blue.json +124 -0
  824. package/themes/newspaper/variants/sage-lime.json +117 -0
  825. package/themes/newspaper/variants/sand-gold.json +124 -0
  826. package/themes/newspaper/variants/sand-jade.json +123 -0
  827. package/themes/og-measure/slots.ts +88 -0
  828. package/themes/og-measure/theme.json +62 -0
  829. package/themes/og-measure/variants/chalk-orange.json +102 -0
  830. package/themes/og-measure/variants/cocoa-amber.json +138 -0
  831. package/themes/og-measure/variants/cocoa-cyan.json +138 -0
  832. package/themes/og-measure/variants/cocoa-gold.json +129 -0
  833. package/themes/og-measure/variants/cream-lime.json +137 -0
  834. package/themes/og-measure/variants/deep-jade.json +100 -0
  835. package/themes/og-measure/variants/dusk-violet.json +133 -0
  836. package/themes/og-measure/variants/forest-cyan.json +123 -0
  837. package/themes/og-measure/variants/frost-amber.json +129 -0
  838. package/themes/og-measure/variants/frost-cyan.json +133 -0
  839. package/themes/og-measure/variants/lilac-plum.json +127 -0
  840. package/themes/og-measure/variants/lilac-violet.json +139 -0
  841. package/themes/og-measure/variants/sage-green.json +137 -0
  842. package/themes/og-measure/variants/wine-jade.json +127 -0
  843. package/themes/og-measure/variants/wine-plum.json +127 -0
  844. package/themes/orbit-system/THEME.md +77 -0
  845. package/themes/orbit-system/effects.ts +665 -0
  846. package/themes/orbit-system/sample/assets/og/logo.svg +12 -0
  847. package/themes/orbit-system/sample/beats.json +1005 -0
  848. package/themes/orbit-system/sample/index.html +11 -0
  849. package/themes/orbit-system/sample/kf.json +59 -0
  850. package/themes/orbit-system/sample/lib/facts.ts +29 -0
  851. package/themes/orbit-system/sample/lib/orbit.ts +84 -0
  852. package/themes/orbit-system/sample/main.ts +9 -0
  853. package/themes/orbit-system/sample/scenes/end.ts +50 -0
  854. package/themes/orbit-system/sample/scenes/gather.ts +35 -0
  855. package/themes/orbit-system/sample/scenes/models.ts +49 -0
  856. package/themes/orbit-system/sample/scenes/price.ts +57 -0
  857. package/themes/orbit-system/sample/style.json +108 -0
  858. package/themes/orbit-system/slots.ts +174 -0
  859. package/themes/orbit-system/still.png +0 -0
  860. package/themes/orbit-system/style.json +108 -0
  861. package/themes/orbit-system/theme.json +142 -0
  862. package/themes/orbit-system/variants/cocoa-amber.json +122 -0
  863. package/themes/orbit-system/variants/cocoa-coral.json +127 -0
  864. package/themes/orbit-system/variants/cocoa-cyan.json +127 -0
  865. package/themes/orbit-system/variants/deep-cyan.json +132 -0
  866. package/themes/orbit-system/variants/deep-sky.json +122 -0
  867. package/themes/orbit-system/variants/dusk-magenta.json +121 -0
  868. package/themes/orbit-system/variants/dusk-plum-pretendard.json +121 -0
  869. package/themes/orbit-system/variants/dusk-plum.json +127 -0
  870. package/themes/orbit-system/variants/dusk-violet.json +122 -0
  871. package/themes/orbit-system/variants/forest-coral.json +126 -0
  872. package/themes/orbit-system/variants/forest-plum.json +94 -0
  873. package/themes/orbit-system/variants/navy-amber.json +127 -0
  874. package/themes/orbit-system/variants/wine-amber.json +127 -0
  875. package/themes/orbit-system/variants/wine-coral.json +123 -0
  876. package/themes/orbit-system/variants/wine-magenta.json +126 -0
  877. package/themes/origami-fold/THEME.md +74 -0
  878. package/themes/origami-fold/effects.ts +535 -0
  879. package/themes/origami-fold/sample/assets/og/logo.svg +12 -0
  880. package/themes/origami-fold/sample/beats.json +1005 -0
  881. package/themes/origami-fold/sample/index.html +11 -0
  882. package/themes/origami-fold/sample/kf.json +59 -0
  883. package/themes/origami-fold/sample/lib/facts.ts +29 -0
  884. package/themes/origami-fold/sample/lib/fold.ts +53 -0
  885. package/themes/origami-fold/sample/main.ts +9 -0
  886. package/themes/origami-fold/sample/scenes/end.ts +51 -0
  887. package/themes/origami-fold/sample/scenes/gather.ts +37 -0
  888. package/themes/origami-fold/sample/scenes/models.ts +46 -0
  889. package/themes/origami-fold/sample/scenes/price.ts +54 -0
  890. package/themes/origami-fold/sample/style.json +108 -0
  891. package/themes/origami-fold/slots.ts +176 -0
  892. package/themes/origami-fold/still.png +0 -0
  893. package/themes/origami-fold/style.json +108 -0
  894. package/themes/origami-fold/theme.json +123 -0
  895. package/themes/origami-fold/variants/deep-amber-sunflower.json +122 -0
  896. package/themes/origami-fold/variants/deep-amber.json +122 -0
  897. package/themes/origami-fold/variants/deep-orange.json +128 -0
  898. package/themes/origami-fold/variants/forest-coral.json +127 -0
  899. package/themes/origami-fold/variants/forest-pink.json +123 -0
  900. package/themes/origami-fold/variants/forest-rose.json +100 -0
  901. package/themes/origami-fold/variants/frost-amber-grain.json +127 -0
  902. package/themes/origami-fold/variants/frost-amber-paper.json +128 -0
  903. package/themes/origami-fold/variants/frost-amber.json +95 -0
  904. package/themes/origami-fold/variants/sage-coral-grain.json +100 -0
  905. package/themes/origami-fold/variants/sage-coral-sunflower.json +122 -0
  906. package/themes/origami-fold/variants/sage-coral.json +128 -0
  907. package/themes/origami-fold/variants/sage-pink-grain.json +127 -0
  908. package/themes/origami-fold/variants/sage-pink-round.json +124 -0
  909. package/themes/origami-fold/variants/sage-pink.json +127 -0
  910. package/themes/paper-cutout/THEME.md +63 -0
  911. package/themes/paper-cutout/effects.ts +609 -0
  912. package/themes/paper-cutout/sample/assets/og/logo.svg +12 -0
  913. package/themes/paper-cutout/sample/beats.json +1005 -0
  914. package/themes/paper-cutout/sample/index.html +11 -0
  915. package/themes/paper-cutout/sample/kf.json +59 -0
  916. package/themes/paper-cutout/sample/lib/facts.ts +29 -0
  917. package/themes/paper-cutout/sample/lib/paper.ts +91 -0
  918. package/themes/paper-cutout/sample/main.ts +9 -0
  919. package/themes/paper-cutout/sample/scenes/end.ts +54 -0
  920. package/themes/paper-cutout/sample/scenes/gather.ts +63 -0
  921. package/themes/paper-cutout/sample/scenes/models.ts +93 -0
  922. package/themes/paper-cutout/sample/scenes/price.ts +100 -0
  923. package/themes/paper-cutout/sample/style.json +104 -0
  924. package/themes/paper-cutout/slots.ts +87 -0
  925. package/themes/paper-cutout/still.png +0 -0
  926. package/themes/paper-cutout/style.json +104 -0
  927. package/themes/paper-cutout/theme.json +144 -0
  928. package/themes/paper-cutout/variants/blush-coral.json +122 -0
  929. package/themes/paper-cutout/variants/blush-violet.json +122 -0
  930. package/themes/paper-cutout/variants/chalk-teal.json +122 -0
  931. package/themes/paper-cutout/variants/cream-gold.json +122 -0
  932. package/themes/paper-cutout/variants/frost-blue.json +123 -0
  933. package/themes/paper-cutout/variants/frost-lime.json +121 -0
  934. package/themes/paper-cutout/variants/frost-plum.json +122 -0
  935. package/themes/paper-cutout/variants/lilac-blue.json +123 -0
  936. package/themes/paper-cutout/variants/lilac-gold.json +122 -0
  937. package/themes/paper-cutout/variants/sage-cyan.json +122 -0
  938. package/themes/paper-cutout/variants/sage-green.json +122 -0
  939. package/themes/paper-cutout/variants/sage-pink.json +122 -0
  940. package/themes/paper-cutout/variants/sand-blue.json +123 -0
  941. package/themes/paper-cutout/variants/sand-green.json +121 -0
  942. package/themes/paper-cutout/variants/sand-plum.json +120 -0
  943. package/themes/pixel-quest/THEME.md +57 -0
  944. package/themes/pixel-quest/effects.ts +491 -0
  945. package/themes/pixel-quest/kit.ts +232 -0
  946. package/themes/pixel-quest/sample/assets/og/logo.svg +12 -0
  947. package/themes/pixel-quest/sample/beats.json +1005 -0
  948. package/themes/pixel-quest/sample/index.html +11 -0
  949. package/themes/pixel-quest/sample/kf.json +59 -0
  950. package/themes/pixel-quest/sample/lib/facts.ts +29 -0
  951. package/themes/pixel-quest/sample/lib/game.ts +71 -0
  952. package/themes/pixel-quest/sample/lib/logo.ts +51 -0
  953. package/themes/pixel-quest/sample/main.ts +9 -0
  954. package/themes/pixel-quest/sample/scenes/end.ts +75 -0
  955. package/themes/pixel-quest/sample/scenes/gather.ts +65 -0
  956. package/themes/pixel-quest/sample/scenes/models.ts +104 -0
  957. package/themes/pixel-quest/sample/scenes/price.ts +118 -0
  958. package/themes/pixel-quest/sample/style.json +115 -0
  959. package/themes/pixel-quest/slots.ts +94 -0
  960. package/themes/pixel-quest/still.png +0 -0
  961. package/themes/pixel-quest/style.json +115 -0
  962. package/themes/pixel-quest/theme.json +143 -0
  963. package/themes/pixel-quest/variants/navy-gold-2.json +98 -0
  964. package/themes/pixel-quest/variants/navy-gold-3.json +127 -0
  965. package/themes/pixel-quest/variants/navy-gold-4.json +95 -0
  966. package/themes/pixel-quest/variants/navy-gold-pixel.json +132 -0
  967. package/themes/pixel-quest/variants/navy-gold-scan.json +99 -0
  968. package/themes/pixel-quest/variants/navy-gold.json +131 -0
  969. package/themes/pixel-quest/variants/navy-lime-2.json +93 -0
  970. package/themes/pixel-quest/variants/navy-lime-3.json +124 -0
  971. package/themes/pixel-quest/variants/navy-lime-pixel.json +126 -0
  972. package/themes/pixel-quest/variants/navy-lime-scan.json +97 -0
  973. package/themes/pixel-quest/variants/navy-lime.json +130 -0
  974. package/themes/receipt/THEME.md +72 -0
  975. package/themes/receipt/effects.ts +257 -0
  976. package/themes/receipt/sample/assets/og/logo.svg +12 -0
  977. package/themes/receipt/sample/beats.json +1005 -0
  978. package/themes/receipt/sample/index.html +11 -0
  979. package/themes/receipt/sample/kf.json +59 -0
  980. package/themes/receipt/sample/lib/facts.ts +29 -0
  981. package/themes/receipt/sample/lib/sheet.ts +52 -0
  982. package/themes/receipt/sample/main.ts +9 -0
  983. package/themes/receipt/sample/scenes/end.ts +44 -0
  984. package/themes/receipt/sample/scenes/gather.ts +57 -0
  985. package/themes/receipt/sample/scenes/models.ts +48 -0
  986. package/themes/receipt/sample/scenes/price.ts +64 -0
  987. package/themes/receipt/sample/style.json +104 -0
  988. package/themes/receipt/slots.ts +89 -0
  989. package/themes/receipt/still.png +0 -0
  990. package/themes/receipt/style.json +104 -0
  991. package/themes/receipt/theme.json +133 -0
  992. package/themes/receipt/variants/blush-coral.json +89 -0
  993. package/themes/receipt/variants/sand-coral-2.json +89 -0
  994. package/themes/receipt/variants/sand-coral-3.json +89 -0
  995. package/themes/receipt/variants/sand-coral-4.json +90 -0
  996. package/themes/receipt/variants/sand-coral-5.json +89 -0
  997. package/themes/receipt/variants/sand-coral-6.json +88 -0
  998. package/themes/receipt/variants/sand-coral-7.json +88 -0
  999. package/themes/receipt/variants/sand-coral-8.json +89 -0
  1000. package/themes/receipt/variants/sand-coral-9.json +89 -0
  1001. package/themes/receipt/variants/sand-coral.json +88 -0
  1002. package/themes/receipt/variants/sand-orange-2.json +89 -0
  1003. package/themes/receipt/variants/sand-orange-3.json +90 -0
  1004. package/themes/receipt/variants/sand-orange-4.json +89 -0
  1005. package/themes/receipt/variants/sand-orange-5.json +89 -0
  1006. package/themes/receipt/variants/sand-orange.json +89 -0
  1007. package/themes/riso-zine/slots.ts +81 -0
  1008. package/themes/riso-zine/theme.json +66 -0
  1009. package/themes/riso-zine/variants/blush-coral.json +120 -0
  1010. package/themes/riso-zine/variants/blush-gold.json +120 -0
  1011. package/themes/riso-zine/variants/blush-pink.json +115 -0
  1012. package/themes/riso-zine/variants/chalk-cyan.json +120 -0
  1013. package/themes/riso-zine/variants/chalk-lime.json +119 -0
  1014. package/themes/riso-zine/variants/cream-amber.json +89 -0
  1015. package/themes/riso-zine/variants/frost-blue.json +119 -0
  1016. package/themes/riso-zine/variants/frost-coral.json +118 -0
  1017. package/themes/riso-zine/variants/frost-cyan.json +120 -0
  1018. package/themes/riso-zine/variants/frost-green.json +120 -0
  1019. package/themes/riso-zine/variants/lilac-plum.json +118 -0
  1020. package/themes/riso-zine/variants/lilac-violet.json +123 -0
  1021. package/themes/riso-zine/variants/sage-green.json +119 -0
  1022. package/themes/riso-zine/variants/sand-jade.json +120 -0
  1023. package/themes/riso-zine/variants/sand-plum.json +113 -0
  1024. package/themes/server-hall/THEME.md +58 -0
  1025. package/themes/server-hall/effects.ts +455 -0
  1026. package/themes/server-hall/kit.ts +138 -0
  1027. package/themes/server-hall/sample/assets/og/logo.svg +12 -0
  1028. package/themes/server-hall/sample/beats.json +1005 -0
  1029. package/themes/server-hall/sample/index.html +11 -0
  1030. package/themes/server-hall/sample/kf.json +59 -0
  1031. package/themes/server-hall/sample/lib/facts.ts +29 -0
  1032. package/themes/server-hall/sample/lib/hall.ts +81 -0
  1033. package/themes/server-hall/sample/main.ts +9 -0
  1034. package/themes/server-hall/sample/scenes/end.ts +57 -0
  1035. package/themes/server-hall/sample/scenes/gather.ts +38 -0
  1036. package/themes/server-hall/sample/scenes/models.ts +49 -0
  1037. package/themes/server-hall/sample/scenes/price.ts +65 -0
  1038. package/themes/server-hall/sample/style.json +107 -0
  1039. package/themes/server-hall/slots.ts +106 -0
  1040. package/themes/server-hall/still.png +0 -0
  1041. package/themes/server-hall/style.json +107 -0
  1042. package/themes/server-hall/theme.json +143 -0
  1043. package/themes/server-hall/variants/navy-indigo.json +140 -0
  1044. package/themes/server-hall/variants/night-indigo.json +120 -0
  1045. package/themes/server-hall/variants/wine-plum.json +130 -0
  1046. package/themes/server-hall/variants/wine-violet.json +103 -0
  1047. package/themes/subway-map/THEME.md +64 -0
  1048. package/themes/subway-map/effects.ts +662 -0
  1049. package/themes/subway-map/sample/assets/og/logo.svg +12 -0
  1050. package/themes/subway-map/sample/beats.json +1005 -0
  1051. package/themes/subway-map/sample/index.html +11 -0
  1052. package/themes/subway-map/sample/kf.json +59 -0
  1053. package/themes/subway-map/sample/lib/facts.ts +29 -0
  1054. package/themes/subway-map/sample/lib/map.ts +58 -0
  1055. package/themes/subway-map/sample/main.ts +9 -0
  1056. package/themes/subway-map/sample/scenes/end.ts +94 -0
  1057. package/themes/subway-map/sample/scenes/gather.ts +64 -0
  1058. package/themes/subway-map/sample/scenes/models.ts +117 -0
  1059. package/themes/subway-map/sample/scenes/price.ts +119 -0
  1060. package/themes/subway-map/sample/style.json +108 -0
  1061. package/themes/subway-map/slots.ts +66 -0
  1062. package/themes/subway-map/still.png +0 -0
  1063. package/themes/subway-map/style.json +108 -0
  1064. package/themes/subway-map/theme.json +161 -0
  1065. package/themes/subway-map/variants/blush-jade.json +131 -0
  1066. package/themes/subway-map/variants/chalk-jade.json +141 -0
  1067. package/themes/subway-map/variants/deep-green.json +131 -0
  1068. package/themes/subway-map/variants/deep-teal.json +132 -0
  1069. package/themes/subway-map/variants/dusk-teal-bold.json +132 -0
  1070. package/themes/subway-map/variants/dusk-teal.json +105 -0
  1071. package/themes/subway-map/variants/forest-green.json +141 -0
  1072. package/themes/subway-map/variants/forest-jade.json +131 -0
  1073. package/themes/subway-map/variants/frost-jade.json +131 -0
  1074. package/themes/subway-map/variants/frost-teal.json +137 -0
  1075. package/themes/subway-map/variants/sage-teal.json +132 -0
  1076. package/themes/subway-map/variants/slate-green.json +136 -0
  1077. package/themes/subway-map/variants/slate-jade.json +131 -0
  1078. package/themes/subway-map/variants/snow-jade.json +136 -0
  1079. package/themes/subway-map/variants/wine-green.json +104 -0
  1080. package/themes/swiss-poster/THEME.md +59 -0
  1081. package/themes/swiss-poster/effects.ts +339 -0
  1082. package/themes/swiss-poster/grid.ts +35 -0
  1083. package/themes/swiss-poster/sample/assets/og/logo.svg +12 -0
  1084. package/themes/swiss-poster/sample/beats.json +1005 -0
  1085. package/themes/swiss-poster/sample/index.html +11 -0
  1086. package/themes/swiss-poster/sample/kf.json +59 -0
  1087. package/themes/swiss-poster/sample/lib/facts.ts +29 -0
  1088. package/themes/swiss-poster/sample/lib/poster.ts +36 -0
  1089. package/themes/swiss-poster/sample/main.ts +9 -0
  1090. package/themes/swiss-poster/sample/scenes/end.ts +70 -0
  1091. package/themes/swiss-poster/sample/scenes/gather.ts +68 -0
  1092. package/themes/swiss-poster/sample/scenes/models.ts +63 -0
  1093. package/themes/swiss-poster/sample/scenes/price.ts +74 -0
  1094. package/themes/swiss-poster/sample/style.json +100 -0
  1095. package/themes/swiss-poster/slots.ts +69 -0
  1096. package/themes/swiss-poster/still.png +0 -0
  1097. package/themes/swiss-poster/style.json +100 -0
  1098. package/themes/swiss-poster/theme.json +131 -0
  1099. package/themes/swiss-poster/variants/blush-green.json +116 -0
  1100. package/themes/swiss-poster/variants/chalk-plum.json +121 -0
  1101. package/themes/swiss-poster/variants/chalk-sky.json +90 -0
  1102. package/themes/swiss-poster/variants/cocoa-cyan.json +117 -0
  1103. package/themes/swiss-poster/variants/dusk-coral.json +89 -0
  1104. package/themes/swiss-poster/variants/dusk-magenta.json +117 -0
  1105. package/themes/swiss-poster/variants/forest-amber.json +127 -0
  1106. package/themes/swiss-poster/variants/forest-blue.json +118 -0
  1107. package/themes/swiss-poster/variants/forest-jade.json +89 -0
  1108. package/themes/swiss-poster/variants/frost-gold.json +128 -0
  1109. package/themes/swiss-poster/variants/frost-indigo.json +117 -0
  1110. package/themes/swiss-poster/variants/sand-magenta.json +126 -0
  1111. package/themes/swiss-poster/variants/slate-orange.json +122 -0
  1112. package/themes/swiss-poster/variants/slate-pink.json +126 -0
  1113. package/themes/swiss-poster/variants/slate-violet.json +89 -0
  1114. package/themes/water-works/THEME.md +61 -0
  1115. package/themes/water-works/effects.ts +609 -0
  1116. package/themes/water-works/sample/assets/og/logo.svg +12 -0
  1117. package/themes/water-works/sample/beats.json +1005 -0
  1118. package/themes/water-works/sample/index.html +11 -0
  1119. package/themes/water-works/sample/kf.json +59 -0
  1120. package/themes/water-works/sample/lib/facts.ts +29 -0
  1121. package/themes/water-works/sample/lib/hall.ts +22 -0
  1122. package/themes/water-works/sample/main.ts +9 -0
  1123. package/themes/water-works/sample/scenes/end.ts +55 -0
  1124. package/themes/water-works/sample/scenes/gather.ts +42 -0
  1125. package/themes/water-works/sample/scenes/models.ts +51 -0
  1126. package/themes/water-works/sample/scenes/price.ts +66 -0
  1127. package/themes/water-works/sample/style.json +105 -0
  1128. package/themes/water-works/slots.ts +140 -0
  1129. package/themes/water-works/still.png +0 -0
  1130. package/themes/water-works/style.json +105 -0
  1131. package/themes/water-works/theme.json +138 -0
  1132. package/themes/water-works/variants/blush-rose.json +123 -0
  1133. package/themes/water-works/variants/chalk-blue.json +124 -0
  1134. package/themes/water-works/variants/chalk-cyan-2.json +96 -0
  1135. package/themes/water-works/variants/chalk-cyan.json +123 -0
  1136. package/themes/water-works/variants/chalk-gold-mono.json +122 -0
  1137. package/themes/water-works/variants/chalk-gold.json +123 -0
  1138. package/themes/water-works/variants/chalk-green.json +123 -0
  1139. package/themes/water-works/variants/frost-indigo.json +123 -0
  1140. package/themes/water-works/variants/frost-rose.json +132 -0
  1141. package/themes/water-works/variants/lilac-violet-mono.json +124 -0
  1142. package/themes/water-works/variants/lilac-violet.json +133 -0
  1143. package/themes/water-works/variants/sage-amber.json +133 -0
  1144. package/themes/water-works/variants/sage-green.json +133 -0
  1145. package/themes/water-works/variants/sand-amber.json +123 -0
  1146. package/themes/water-works/variants/sand-plum.json +121 -0
  1147. package/themes/whiteboard/THEME.md +78 -0
  1148. package/themes/whiteboard/effects.ts +561 -0
  1149. package/themes/whiteboard/sample/assets/og/logo.svg +12 -0
  1150. package/themes/whiteboard/sample/beats.json +1005 -0
  1151. package/themes/whiteboard/sample/index.html +11 -0
  1152. package/themes/whiteboard/sample/kf.json +59 -0
  1153. package/themes/whiteboard/sample/lib/board.ts +93 -0
  1154. package/themes/whiteboard/sample/lib/facts.ts +29 -0
  1155. package/themes/whiteboard/sample/main.ts +9 -0
  1156. package/themes/whiteboard/sample/scenes/end.ts +59 -0
  1157. package/themes/whiteboard/sample/scenes/gather.ts +50 -0
  1158. package/themes/whiteboard/sample/scenes/models.ts +56 -0
  1159. package/themes/whiteboard/sample/scenes/price.ts +79 -0
  1160. package/themes/whiteboard/sample/style.json +106 -0
  1161. package/themes/whiteboard/slots.ts +152 -0
  1162. package/themes/whiteboard/still.png +0 -0
  1163. package/themes/whiteboard/style.json +106 -0
  1164. package/themes/whiteboard/theme.json +143 -0
  1165. package/themes/whiteboard/variants/chalk-coral-do.json +126 -0
  1166. package/themes/whiteboard/variants/chalk-coral.json +132 -0
  1167. package/themes/whiteboard/variants/cream-lime.json +131 -0
  1168. package/themes/whiteboard/variants/cream-plum.json +103 -0
  1169. package/themes/whiteboard/variants/frost-teal-do.json +127 -0
  1170. package/themes/whiteboard/variants/frost-teal.json +127 -0
  1171. package/themes/whiteboard/variants/lilac-indigo.json +132 -0
  1172. package/themes/whiteboard/variants/lilac-jade.json +103 -0
  1173. package/themes/whiteboard/variants/sage-green.json +130 -0
  1174. package/themes/whiteboard/variants/sage-plum.json +126 -0
  1175. package/themes/whiteboard/variants/snow-amber.json +99 -0
  1176. package/themes/whiteboard/variants/snow-cyan.json +132 -0
  1177. package/themes/whiteboard/variants/snow-green.json +126 -0
  1178. package/themes/whiteboard/variants/snow-magenta.json +126 -0
  1179. package/themes/whiteboard/variants/snow-plum.json +130 -0
@@ -1,179 +1,462 @@
1
- import { spawnSync } from "node:child_process";
2
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
- import { basename, extname, join, resolve } from "node:path";
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, extname, join, resolve } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { fileURLToPath } from "node:url";
6
- import { genImage, genVideo, hasKey, NeedsApproval, seam, seamFrames, still, whereToPut } from "@kasaframe/gen";
7
- import { attachStudio, chat, codeBlock, detect, formatLook, jsonBlock, listTemplates, loadTemplate, look, lookSheet, type Message, type Plan, projectBrief, PURPOSES, type Purpose, readProject, writeCaptions } from "@kasaframe/plan";
6
+ import { askTerminal, blenderLibDir, capabilities, genImage, genVideo, hasKey, NeedsApproval, parseAllow, type Question, questionsText, seam, seamFrames, settle, still, whereToPut } from "@kasaframe/gen";
7
+ import { applyAnswers, attachStudio, type LookInput, loadSeries, suggestThemes, usableTheme, type Series, seriesFromRequest, chat, codeBlock, detect, formatLook, jsonBlock, listTemplates, loadTemplate, look, lookSheet, makePlan, type Message, type Plan, planQuestions, premiumBlock, projectBrief, PURPOSES, type Purpose, readProject, spend, spentLine, WAIT, writeCaptions, writeStats } from "@kasaframe/plan";
8
+ import { checkContent, contentPrompt, directBrief } from "@kasaframe/director";
8
9
  import { transform } from "esbuild";
9
- import { themeStyle } from "@kasaframe/themes";
10
+ import { bundleFor, dressSeed, pickDress } from "@kasaframe/motion/menu";
11
+ import { themeStyle, worldOf } from "@kasaframe/themes";
10
12
  import { captionMode } from "@kasaframe/motion/captions";
13
+ import { jizuraCheck } from "@kasaframe/jizura/install";
11
14
  import { openPage } from "../browser.ts";
15
+ import { askThemeTerminal, candidateSheet, refLook } from "../pick.ts";
16
+ import { librosa } from "../deps.ts";
17
+ import { lyricsCheck } from "./lyrics.ts";
12
18
  import { type Command, num, parseFlags } from "../command.ts";
19
+ import { acquireRenderLock } from "../lock.ts";
20
+ import { gate, jobLimit, pool } from "../pool.ts";
13
21
 
14
22
  const kfBin = fileURLToPath(new URL("../kf.ts", import.meta.url));
23
+ const repo = fileURLToPath(new URL("../../../../", import.meta.url));
24
+
25
+ // 박자(librosa)·뮤비 가사(JIZURA·정렬) — 없으면 넣는 법·지금 되는 다른 길·빼기를 묻는다
26
+ function toolQuestions(purpose: Purpose, willSong: boolean): Question[] {
27
+ const out: Question[] = [];
28
+ const b = librosa();
29
+ if (willSong && !b.ok)
30
+ out.push({
31
+ id: "beats", cap: "beats", header: "박자 분석", flag: "--beats", missing: true, default: WAIT, skip: "none",
32
+ ask: `곡에 장면 경계·움직임을 맞출 박자 분석(librosa)이 없다 — ${b.detail}`,
33
+ choices: [{ value: WAIT, label: "깔고 다시", note: b.fix ?? "kf setup beats" }, { value: "none", label: "곡 없이", note: "곡을 빼고 화면 안 움직임으로만 — 박 없이 곡만 까는 길은 없다" }],
34
+ });
35
+ if (purpose === "mv") {
36
+ const j = jizuraCheck();
37
+ const l = lyricsCheck();
38
+ if (!j.ok || !l.ok)
39
+ out.push({
40
+ id: "lyrics", cap: "lyrics", header: "가사", flag: "--lyrics", missing: true, default: WAIT, skip: "none",
41
+ ask: `뮤비 가사에 쓸 ${[!j.ok ? "가사 층 엔진(JIZURA)" : "", !l.ok ? "가사 정렬(demucs·stable-ts)" : ""].filter(Boolean).join("·")} 이 없다`,
42
+ choices: [
43
+ { value: WAIT, label: "깔고 다시", note: `kf setup ${[!j.ok ? "jizura" : "", !l.ok ? "lyrics" : ""].filter(Boolean).join(" ")}` },
44
+ ...(l.ok && !j.ok ? [{ value: "words", label: "가사 층 없이 낱말로", note: "정렬한 낱말을 DOM 글로 띄운다 — JIZURA 레이아웃·효과는 없다" }] : []),
45
+ { value: "none", label: "가사 없이", note: "곡과 화면만" },
46
+ ],
47
+ });
48
+ }
49
+ return out;
50
+ }
51
+
52
+ // 시리즈가 박은 답(목소리·곡 파일·생성 답) — 질문 플래그 값으로 넣어 묻지 않는다
53
+ function seriesAnswers(s: Series | null): Record<string, string> {
54
+ if (!s) return {};
55
+ return Object.fromEntries(Object.entries({ ...s.answers, voice: s.voice, music: s.music?.file && resolve(repo, s.music.file) }).filter((e): e is [string, string] => typeof e[1] === "string"));
56
+ }
15
57
 
16
58
  // Claude Code 를 OG 로 다른 모델(딥시크 등)에 붙이면 OG /v1/messages 가 그림을 버린다 — 세션이 kf sheet 를 Read 해도 못 본다.
17
59
  // 그런 「눈 없는 세션」이면 --finish 끝에 --vision-model 을 알려 준다(그림 보는 모델은 기본으로 부르지 않는다)
18
60
  export const eyelessSession = (): boolean =>
19
61
  /opengateway/i.test(process.env.ANTHROPIC_BASE_URL ?? "") && !/^anthropic\//.test(process.env.ANTHROPIC_MODEL ?? "anthropic/");
20
62
  const AUDIO = /\.(mp3|wav|m4a|aac|flac|ogg)$/i;
21
- const DEFAULT_MODEL = "deepseek/deepseek-v4-pro";
22
63
  const DEFAULT_VISION = "google/gemini-3.5-flash";
23
64
  // 화면에 찍히면 안 되는 글 — 지난 실험에서 실제로 나온 것들
24
65
  const BAD_TEXT = /\bundefined\b|\bNaN\b|\[object Object\]|\bnull\b|�/;
25
66
 
26
67
  const cmd: Command = {
27
68
  summary: "요청 한 줄로 끝까지 — 용도 판별·계획·생성 자산·장면 작성·렌더·검사·고치기",
28
- usage: `kf make "<요청>" [--out 폴더] [--assets 폴더] [--music 곡] [--model claude-code|<OG 모델>] [--purpose 용도] [--title 이름] [--yes]
69
+ usage: `kf make "<요청>" [--out 폴더] [--assets 폴더] [--music 곡] [--model claude-code|<OG 모델>] [--purpose 용도] [--theme id|auto] [--title 이름] [--yes]
29
70
  kf make --finish <폴더> [--model ...] 장면을 다 채운 뒤 렌더·검사·표지(Claude Code 모드의 둘째 단계)
30
- kf make --rewrite <폴더> --scenes a,b [--model <OG 모델>] 고른 장면만 다시 쓰고 렌더·검사(작업실 장면 카드)
71
+ kf make --rewrite <폴더> --scenes a,b [--model <OG 모델>] 고른 장면만 다시 쓰고 그 조각만 다시 굽고 그 구간만 검사(작업실 장면 카드)
72
+ 감독 프로젝트는 모델 없이 kf direct --scenes 로 다시 짓는다(--pattern a=<패턴>·--scene-seed a=<글> 로 다른 후보)
73
+ 모델을 안 주면 그 일을 만든 모델(plan.json 의 model) — 장면 코드를 쓰는 길인데 그것도 없으면 멈춘다
74
+ kf make --finish <폴더> --scenes a,b 장면 파일을 손으로 고친 뒤 — 바뀐 조각만 다시 굽고 그 구간만 검사
75
+ kf make --answer <폴더> --image <값> --video <값> … Claude Code 모드에서 MAKE.md 맨 위 질문의 답을 넣는다(계획·지시서를 고쳐 쓴다)
76
+
77
+ 생성 능력(kf doctor 「생성 능력」): 계획은 그림 컷·주인공 3D 샷(Blender)·이음매 3D 전환(three)·캐릭터가 움직이는 컷(영상 모델)을
78
+ 기본으로 넣는다. 돈이 들거나 없는 능력은 만들기 전에 한 번에 묻는다 — Claude Code 모드는 MAKE.md 맨 위, OG 모드는 터미널에서
79
+ (터미널이 아니면 보기와 다시 부를 플래그를 찍고 종료 코드 3)
80
+ --image og-…|codex-imagegen|none · --video <공급자>|none · --blender three|none 그 질문에 미리 답한다
81
+ --voice <엔진>:<목소리>|none 내레이션 목소리(쇼츠·설명 — 보기는 로컬 Qwen3(KF_QWEN3_TTS_URL 있을 때)·ElevenLabs·자막만·그 밖 순, 키 없는 보기는 넣는 법. kf narrate --voices)
82
+ --music <곡 파일>|gen:<공급자>|none 곡(쇼츠·설명은 분위기 맞는 곡 모음 곡을 묻지 않고 고르고, 없을 때만 생성·곡 넣기·없음을 묻는다)
83
+ --allow image,video,… 그 능력은 권장 답으로, 목록에 없는 능력은 돈 안 드는 길로 — 묻지 않는다
31
84
 
32
- --model claude-code: 장면 지시서(MAKE.md)까지 만들고 멈춘다 — Claude Code 가 장면을 채우고 --finish
33
- <OG 모델>: OG chat/completions 로 장면을 쓰고 렌더·검사·고치기까지(기본 ${DEFAULT_MODEL})
34
- Claude Code 안(CLAUDECODE=1)에서 부르면 기본이 claude-code
85
+ 감독 모드(기본, 뮤비 빼고): 모델은 content.json(장면마다 type·칸·들릴 말·출처)만 쓰고, kf direct 가 검증된 장면 문법을
86
+ 골라 장면 파일을 조립한다(LLM 이 장면 코드를 안 쓴다 — 어떤 모델이든 content.json 만 맞으면 화면 품질이 같다)
87
+ --no-direct 감독 없이 모델이 장면 코드를 직접 쓰는 길(뮤비는 늘 이 길)
88
+ --model claude-code(기본, 어디서 부르든): 지시서(MAKE.md)까지 만들고 멈춘다 — 감독 모드면 세션이 content.json 을 쓰고
89
+ kf direct → --finish, --no-direct 면 세션이 장면을 채우고 --finish. OG 를 한 번도 부르지 않는다
90
+ <OG 모델>: 짚을 때만 — OG chat/completions 로 content.json(--no-direct 면 장면)을 쓰고 렌더·검사까지
35
91
  --vision-model 그림 보는 검사 모델(기본 none — 세션이 kf look 의 장면 모음·확인표를 직접 본다). 그림을 못 보는 세션만 ${DEFAULT_VISION} 같은 모델을 준다
92
+ --allow-premium Opus·GPT 계열 OG 모델(--model·--vision-model·--rewrite)을 푼다(KF_ALLOW_PREMIUM=1 과 같다). 없으면 부르기 전에 막는다
93
+ 끝날 때(성공·실패·중단) <작업 폴더>/make-stats.json 에 단계별 모델·ms·토큰·usd 와 합계를 남기고 「OG 호출 N번 · 토큰 · $」 한 줄을 찍는다
94
+ --series 시리즈 묶음(series/<id>.json) · none. 안 주면 요청이 짚은 시리즈(「OG 쇼츠」처럼) — 용도·테마·목소리·곡·자막·길이·끝 카드·생성 답을 묻지 않고 그 묶음대로
36
95
  --purpose 용도를 못 박는다: ${PURPOSES.join(" · ")}
37
- --theme 테마 id · auto(기본 — 요청이 「리소 느낌」처럼 짚은 테마, 아니면 내용에 맞는 approved 후보 셋 중 첫째. 첫 장면을 셋 다 찍어 out/theme-<id>.png) · none(프리셋만)
38
- --yes 영상 생성처럼 돈이 큰 호출을 승인한다(없으면 견적만 보이고 이음매는 대체 화면으로 간다)
96
+ --theme 테마 id·변주 「<세계>/<변주>」 · auto(요청이 「리소 느낌」처럼 짚은 테마, 아니면 요청·분위기에 맞는 approved 후보 중 첫째) · none(프리셋만)
97
+ 안 주면(시리즈가 정하지도 않았으면) 묻는다 — 보기는 「알아서」(= auto)와 요청·용도·분위기 낱말에 맞는 approved 세계·변주 넷,
98
+ 넷만 담은 견본 시트를 그 자리에서 만든다(칸은 캐시). 테마 이름이 아닌 말을 주면 분위기 한 마디로 읽는다
99
+ --theme-mood 분위기 한 마디(「따뜻한 종이 느낌」·「어두운 네온」) — 그걸로 후보 넷을 다시 고른다. --answer 와 함께 주면 다시 묻는다
100
+ --theme-ref 레퍼런스 그림 경로·그림 주소·웹 페이지 주소 — 바탕·강조색을 읽어 후보 넷을 다시 고른다
101
+ --yes 묻지 않고 권장 답으로 — 돈이 드는 그림·영상도 승인한다(지역 제한이 걸린 H3 는 --video h3-pod 로 짚어야만)
39
102
  --max-usd 영상 하나 상한(기본 1)
40
103
  --video-provider 영상 공급자를 못 박는다(예: h3-pod — KF_H3_URL·KF_H3_STEPS 필요). 기본은 KF_VIDEO_PROVIDER, 없으면 생성 담당 기본
41
104
  --fix 검사가 떨어졌을 때 고치기 횟수(기본 2)
42
105
  --captions on|off|fit 화면 자막(fit 은 장면을 자막 띠 위로 줄여 올린다) — 새로 만들 땐 kf.json 에 적고, --finish 에선 그 판만 덮는다(다시 만들지 않고 켜고 끄기). srt·vtt 는 둘 다 낸다
43
106
  --suffix [글] --finish 결과 이름 뒤에 붙인다(값 없이면 -자막 · -자막없음) — 원본 video.mp4 를 덮지 않고 둘 다 남긴다
44
- --no-gen 생성 모델을 부르지 않는다. claude-code 모드는 묻지 않고 그림을 만들지 않는다 — 지시서의 「만들 그림」을 보고 세션이 프롬프트를 써서 kf gen img 로 만든다
107
+ --no-gen 생성 모델을 부르지 않는다 — 묻지 않고 그림·영상 모델은 빼고, Blender 가 없으면 three 로 대신한다
45
108
 
46
109
  용도를 못 정하면 터미널에서 묻고, 터미널이 아니면 보기를 찍고 종료 코드 3.`,
47
110
  async run(argv) {
48
- const { args, opts } = parseFlags(argv);
49
- const str = (k: string) => (typeof opts[k] === "string" ? (opts[k] as string) : undefined);
50
- // --finish 는 Claude Code 모드의 둘째 단계라 모델을 안 주면 claude-code — 세션이 직접 그림을 보니 그림 검사 모델도 부르지 않는다
51
- const model = str("model") ?? (process.env.CLAUDECODE || opts.finish ? "claude-code" : DEFAULT_MODEL);
52
- const vision = str("vision-model") ?? "none";
53
- // OG 모드는 장면 쓰기·그림 보기까지 OG 를 부른다 — 뼈대만 만들고 죽지 않게 처음에 막는다
54
- if (model !== "claude-code" && !hasKey("opengateway")) {
55
- console.error(`kf make: OG 키가 없다 — ${whereToPut("opengateway")}. 키 없이는 Claude Code 안에서(또는 --model claude-code) — 장면은 세션이 쓰고 그림 생성은 건너뛴다`);
56
- return 2;
111
+ // 장부는 어느 길로 끝나든(성공·실패·throw·Ctrl-C) 한 번 — 작업 폴더가 서기 전에 끝났으면 줄만 찍는다
112
+ const at: Track = {};
113
+ let code: number | string = "오류";
114
+ let closed = false;
115
+ const close = () => {
116
+ if (closed || at.quiet) return;
117
+ closed = true;
118
+ if (at.dir && existsSync(at.dir)) writeStats(at.dir, { command: at.command ?? "make", model: at.model ?? "", code });
119
+ console.log(`\n${spentLine()}`);
120
+ };
121
+ const stop = (sig: NodeJS.Signals) => {
122
+ code = sig;
123
+ close();
124
+ process.exit(sig === "SIGINT" ? 130 : 143);
125
+ };
126
+ process.once("SIGINT", stop);
127
+ process.once("SIGTERM", stop);
128
+ try {
129
+ code = await make(argv, at);
130
+ return code;
131
+ } finally {
132
+ process.off("SIGINT", stop);
133
+ process.off("SIGTERM", stop);
134
+ close();
57
135
  }
58
- const fixes = num(opts.fix, 2);
59
- const captions = str("captions");
60
- if (captions !== undefined && captions !== "on" && captions !== "off" && captions !== "fit") {
61
- console.error(`kf make: --captions 는 on·off·fit — ${captions}`);
136
+ },
137
+ };
138
+ export default cmd;
139
+
140
+ interface Track {
141
+ dir?: string;
142
+ model?: string;
143
+ command?: string;
144
+ quiet?: boolean;
145
+ }
146
+
147
+ async function make(argv: string[], at: Track): Promise<number> {
148
+ const { args, opts } = parseFlags(argv);
149
+ const str = (k: string) => (typeof opts[k] === "string" ? (opts[k] as string) : undefined);
150
+ // OG 모델은 짚을 때만 — 어디서 부르든(Claude Code 안팎·작업실) 기본은 지시서까지 만들고 세션이 잇는 claude-code
151
+ const model = str("model") ?? "claude-code";
152
+ const vision = str("vision-model") ?? "none";
153
+ at.model = model;
154
+ if (opts["allow-premium"]) process.env.KF_ALLOW_PREMIUM = "1";
155
+ for (const m of [model, vision]) {
156
+ const why = m !== "claude-code" && m !== "none" ? premiumBlock(m) : null;
157
+ if (why) {
158
+ console.error(`kf make: ${why}`);
62
159
  return 2;
63
160
  }
64
- const suffix = str("suffix") ?? (opts.suffix ? (captions === "off" ? "-자막없음" : "-자막") : "");
65
-
66
- if (opts.rewrite) {
67
- const dir = resolve(typeof opts.rewrite === "string" ? opts.rewrite : (args[0] ?? "."));
68
- const ids = (str("scenes") ?? "").split(",").filter(Boolean);
69
- if (!ids.length || model === "claude-code") {
70
- console.error("kf make --rewrite: --scenes 와 OG 모델(--model)이 필요하다");
71
- return 2;
72
- }
73
- writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
74
- await writeScenes(dir, model, ids);
75
- return finish(dir, { model, vision, fixes });
161
+ }
162
+ // OG 모드는 장면 쓰기·그림 보기까지 OG 를 부른다 — 뼈대만 만들고 죽지 않게 처음에 막는다
163
+ if (model !== "claude-code" && !hasKey("opengateway")) {
164
+ console.error(`kf make: OG 키가 없다 — ${whereToPut("opengateway")}. 키 없이는 --model 을 빼고(claude-code) — 장면은 세션이 쓰고 그림 생성은 건너뛴다`);
165
+ return 2;
166
+ }
167
+ const fixes = num(opts.fix, 2);
168
+ const captions = str("captions");
169
+ if (captions !== undefined && captions !== "on" && captions !== "off" && captions !== "fit") {
170
+ console.error(`kf make: --captions 는 on·off·fit — ${captions}`);
171
+ return 2;
172
+ }
173
+ const suffix = str("suffix") ?? (opts.suffix ? (captions === "off" ? "-자막없음" : "-자막") : "");
174
+
175
+ if (opts.answer) {
176
+ at.quiet = true;
177
+ const dir = resolve(typeof opts.answer === "string" ? opts.answer : (args[0] ?? "."));
178
+ return answerProject(dir, opts);
179
+ }
180
+ if (opts.rewrite) {
181
+ const dir = resolve(typeof opts.rewrite === "string" ? opts.rewrite : (args[0] ?? "."));
182
+ Object.assign(at, { dir, command: "rewrite" });
183
+ const ids = (str("scenes") ?? "").split(",").filter(Boolean);
184
+ if (!ids.length) {
185
+ console.error("kf make --rewrite: --scenes a,b 가 필요하다");
186
+ return 2;
76
187
  }
77
- if (opts.finish) {
78
- const dir = resolve(typeof opts.finish === "string" ? opts.finish : (args[0] ?? "."));
79
- const gen = { approve: !!opts.yes, maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
80
- const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
81
- const studio = await attachStudio({ dir, request: plan.request ?? dir, model, purpose: plan.purpose });
82
- const unmirror = studio?.mirror();
83
- if (studio) console.log(`작업실: ${studio.url}`);
84
- await studio?.send({ phase: "render" });
85
- const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen, captions, suffix });
86
- await unmirror?.();
87
- await studio?.send({ done: true, ok: code === 0 });
88
- return code;
188
+ // 감독 프로젝트 — 모델 없이 그 장면만 다시 짓는다(다른 장면 파일은 바이트 그대로라 그 조각은 다시 안 굽는다)
189
+ if (isDirect(dir)) {
190
+ const pick = ["pattern", "scene-seed"].flatMap((k) => (str(k) ? [`--${k}`, str(k)!] : []));
191
+ // 들릴 말을 고쳤을 때만 다시 읽힌다 — 목소리가 장면 길이를 정해서 뒤 장면 시각이 움직인다(그 조각들도 다시 굽힌다)
192
+ const narrate = sayChanged(dir, ids) ? [] : ["--no-narrate"];
193
+ const d = await relay(["direct", dir, "--scenes", ids.join(","), ...narrate, "--no-check", ...pick]);
194
+ if (d !== 0) return d;
195
+ return finish(dir, { model: "direct", vision, fixes: 0, scenes: ids });
89
196
  }
90
-
91
- const request = args.join(" ").trim();
92
- if (!request) {
93
- console.error('kf make: 요청 글이 필요하다 — kf make "카사텀 홍보 영상 1분"');
197
+ // 다시 쓰기는 그 일을 만든 모델로 — 기본 모델로 몰래 바꾸지 않는다
198
+ const own = str("model") ?? readPlan(dir).model;
199
+ if (!own || own === "claude-code") {
200
+ console.error(`kf make --rewrite: 장면 코드를 다시 쓸 OG 모델이 없다 — --model <OG 모델>로 주거나, 세션이 ${ids.join(",")} 장면 파일을 고치고 kf make --finish ${dir} --scenes ${ids.join(",")}`);
94
201
  return 2;
95
202
  }
96
- const assetFiles = str("assets") ? listFiles(resolve(str("assets")!)) : [];
97
- const given = str("music") ? resolve(str("music")!) : assetFiles.find((f) => AUDIO.test(f));
98
-
99
- let purpose = str("purpose") as Purpose | undefined;
100
- const det = detect({ request, assets: assetFiles, music: given }, listTemplates());
101
- if (!purpose) {
102
- if (det.ambiguous) {
103
- purpose = await ask(det.ranked.slice(0, 4).map((r) => r.purpose));
104
- if (!purpose) {
105
- console.error(`kf make: 용도를 못 정했다(확신 ${det.confidence}). --purpose 로 골라 다시: ${det.ranked.slice(0, 4).map((r) => `${r.purpose}(${r.p})`).join(" · ")}`);
106
- return 3;
107
- }
108
- } else purpose = det.purpose;
203
+ at.model = own;
204
+ // 그 일을 만든 모델이 비싼 계열이면 다시 쓰기도 같은 문에서 막는다
205
+ if (premiumBlock(own)) {
206
+ console.error(`kf make --rewrite: ${premiumBlock(own)}`);
207
+ return 2;
109
208
  }
110
- const chosen = det.ranked.find((r) => r.purpose === purpose);
111
- // 곡을 안 줬으면 곡이 끌고 가는 용도(홍보·공개)일 때만 곡 모음에서 고른다 — 쇼츠·설명에 아무 곡이나 깔리지 않게
112
- const music = given ?? (loadTemplate(purpose).music === "required" ? findLibraryMusic(request) : undefined);
113
- console.error(`용도 ${purpose} (확신 ${chosen?.p ?? 1}${chosen?.reasons.length ? ` — ${chosen.reasons.join(", ")}` : ""})`);
114
-
115
- const dir = resolve(str("out") ?? `${purpose}-${stamp()}`);
116
- const newArgs = [kfBin, "new", dir, "--template", purpose, "--request", request];
117
- if (music) newArgs.push("--music", music);
118
- if (str("title")) newArgs.push("--title", str("title")!);
119
- // 말로만 시작해도 테마까지 알아서 — 안 주면 auto(요청이 짚은 테마 → 내용에 맞는 approved 후보). 테마 없이 프리셋만 쓰려면 --theme none
120
- const theme = str("theme") ?? "auto";
121
- if (theme !== "none") newArgs.push("--theme", theme);
122
- if (opts.force) newArgs.push("--force");
123
- const made = spawnSync(process.execPath, newArgs, { stdio: ["ignore", "inherit", "inherit"] });
124
- if (made.status !== 0) return made.status ?? 1;
125
-
126
- const studio = await attachStudio({ dir, request, model, purpose });
209
+ writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
210
+ await writeScenes(dir, own, ids);
211
+ return finish(dir, { model: own, vision, fixes, scenes: ids });
212
+ }
213
+ if (opts.finish) {
214
+ const dir = resolve(typeof opts.finish === "string" ? opts.finish : (args[0] ?? "."));
215
+ Object.assign(at, { dir, command: "finish" });
216
+ const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
217
+ const gen = { approve: !!opts.yes || videoApproved(plan), maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
218
+ const studio = await attachStudio({ dir, request: plan.request ?? dir, model, purpose: plan.purpose });
127
219
  const unmirror = studio?.mirror();
128
220
  if (studio) console.log(`작업실: ${studio.url}`);
129
- const plan: Plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
130
- // 새 프로젝트는 이 영상의 기본으로 적는다 — 계획 다듬기가 내레이션을 쓸지도 이걸 본다
131
- if (captions) {
132
- const kfPath = join(dir, "kf.json");
133
- writeJson(kfPath, { ...JSON.parse(readFileSync(kfPath, "utf8")), captions });
134
- plan.captions = captionMode(captions);
135
- }
136
- if (!str("purpose")) {
137
- plan.confidence = chosen?.p ?? det.confidence;
138
- plan.alternatives = det.ranked.filter((r) => r.purpose !== purpose).slice(0, 2).map((r) => ({ purpose: r.purpose, p: r.p }));
139
- plan.reasons = [...(chosen?.reasons ?? []), ...plan.reasons.filter((r) => r !== "사람이 정함")];
221
+ await studio?.send({ phase: "render" });
222
+ const scenes = str("scenes")?.split(",").filter(Boolean);
223
+ const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen, captions, suffix, ...(scenes?.length ? { scenes } : {}) });
224
+ await unmirror?.();
225
+ await studio?.send({ done: true, ok: code === 0 });
226
+ return code;
227
+ }
228
+
229
+ const request = args.join(" ").trim();
230
+ if (!request) {
231
+ console.error('kf make: 요청 글이 필요하다 — kf make "카사텀 홍보 영상 1분"');
232
+ return 2;
233
+ }
234
+ const assetFiles = str("assets") ? listFiles(resolve(str("assets")!)) : [];
235
+ let themeLook: LookInput;
236
+ try {
237
+ themeLook = await lookFlags(opts);
238
+ } catch (e) {
239
+ console.error(`kf make: ${(e as Error).message}`);
240
+ return 2;
241
+ }
242
+ // 시리즈가 먼저 — 같은 시리즈면 매번 같은 묶음. 사람이 따로 준 플래그는 그대로 이긴다
243
+ const series: Series | null = str("series") === "none" ? null : str("series") ? loadSeries(str("series")!) : seriesFromRequest(request);
244
+ if (series) console.error(`시리즈 ${series.id}(${series.label}) — 용도·테마·목소리·곡·자막·길이·끝 카드를 묶음대로`);
245
+ // --music 은 곡 파일, 아니면 음악 질문의 답(gen:<공급자>·none) — 답은 파일로 읽지 않는다
246
+ const musicFlag = str("music");
247
+ const musicAnswer = musicFlag !== undefined && /^(gen:|none$)/.test(musicFlag);
248
+ const given = musicFlag && !musicAnswer ? resolve(musicFlag) : (assetFiles.find((f) => AUDIO.test(f)) ?? (series?.music?.file ? resolve(repo, series.music.file) : undefined));
249
+
250
+ let purpose = (str("purpose") ?? series?.purpose) as Purpose | undefined;
251
+ const det = detect({ request, assets: assetFiles, music: given }, listTemplates());
252
+ if (!purpose) {
253
+ if (det.ambiguous) {
254
+ purpose = await ask(det.ranked.slice(0, 4).map((r) => r.purpose));
255
+ if (!purpose) {
256
+ console.error(`kf make: 용도를 못 정했다(확신 ${det.confidence}). --purpose 로 골라 다시: ${det.ranked.slice(0, 4).map((r) => `${r.purpose}(${r.p})`).join(" · ")}`);
257
+ return 3;
258
+ }
259
+ } else purpose = det.purpose;
260
+ }
261
+ const chosen = det.ranked.find((r) => r.purpose === purpose);
262
+ // 곡을 안 줬으면 곡이 끌고 가는 용도(홍보·공개)일 때만 곡 모음에서 고른다 — 쇼츠·설명에 아무 곡이나 깔리지 않게
263
+ // 분위기를 정한 틀(쇼츠·설명)은 곡을 묻기 틀이 고른다(분위기 맞는 곡 모음 곡 → 생성 → 없음)
264
+ const tplOf = loadTemplate(purpose);
265
+ const music = given ?? (!musicAnswer && tplOf.music === "required" && !tplOf.mood ? findLibraryMusic(request) : undefined);
266
+ console.error(`용도 ${purpose} (${series && !str("purpose") ? `시리즈 ${series.id}` : `확신 ${chosen?.p ?? 1}${chosen?.reasons.length ? ` — ${chosen.reasons.join(", ")}` : ""}`})`);
267
+
268
+ // 만들기 전에 묻는다 — 뼈대를 만들고 나서 멈추면 반쯤 만든 폴더가 남는다
269
+ const caps = await capabilities({ probe: true });
270
+ const draft = makePlan({ request, purpose, assets: assetFiles, music: given ?? null, series: series?.id ?? "none", look: themeLook });
271
+ const { questions, auto } = planQuestions(draft, caps);
272
+ // 테마를 물을 거면 후보 넷 견본 시트를 그 자리에서 — 질문 글에 시트 경로가 붙는다
273
+ const themeQ = questions.find((q) => q.id === "theme");
274
+ const flags = { ...seriesAnswers(series), ...opts };
275
+ const settled = settle(questions, { given: flags, yes: !!opts.yes, noGen: !!opts["no-gen"], allow: parseAllow(opts.allow) });
276
+ // 묻지 않고 정한 답(있는 공짜 능력)도 사람이 준 플래그·시리즈 답이 이긴다. --no-gen 이면 공짜라도 생성 모델(그림·영상)은 뺀다
277
+ const forced = Object.fromEntries(
278
+ Object.keys(auto).flatMap((id) => (typeof flags[id] === "string" ? [[id, flags[id] as string]] : opts["no-gen"] && (id === "image" || id === "video") ? [[id, "none"]] : [])),
279
+ );
280
+ let answers: Record<string, string> = { ...auto, ...forced, ...settled.answers };
281
+ let open = settled.open;
282
+ // 박자·가사 도구는 뼈대를 세울 때 쓴다 — 없으면 조용히 박 없이·낱말로 가지 않고, 모드와 상관없이 뼈대 전에 답을 받는다
283
+ const willSong = !!(music ?? (answers.music && !/^(gen:|none$|wait$)/.test(answers.music) ? answers.music : undefined));
284
+ const tools = toolQuestions(purpose, willSong);
285
+ const toolSettled = settle(tools, { given: flags, yes: !!opts.yes, noGen: !!opts["no-gen"], allow: parseAllow(opts.allow) });
286
+ questions.push(...tools);
287
+ answers = { ...answers, ...toolSettled.answers };
288
+ Object.assign(settled.how, toolSettled.how);
289
+ if (toolSettled.open.length) {
290
+ const asked = await askTerminal(toolSettled.open);
291
+ if (!asked) {
292
+ console.error(`kf make: 뼈대 전에 정할 것이 ${toolSettled.open.length}가지 — 사람에게 묻고 플래그로 답해 다시 부른다\n\n${questionsText(toolSettled.open)}`);
293
+ return 3;
140
294
  }
141
- if (assetFiles.length) {
142
- mkdirSync(join(dir, "assets", "in"), { recursive: true });
143
- for (const f of assetFiles) if (!AUDIO.test(f)) copyFileSync(f, join(dir, "assets", "in", basename(f)));
295
+ answers = { ...answers, ...asked };
296
+ }
297
+ if (answers.beats === "none" && answers.music && !/^(gen:|none$)/.test(answers.music)) answers.music = "none";
298
+ if (themeQ && open.includes(themeQ)) await candidateSheet(themeQ, (l) => console.error(l));
299
+ // 터미널이면 테마는 따로 — 분위기 한 마디·그림 경로·주소를 치면 그걸로 후보를 다시 골라 다시 묻는다
300
+ if (themeQ && open.includes(themeQ) && model !== "claude-code") {
301
+ const picked = await askThemeTerminal(themeQ, async (r) => {
302
+ themeLook = { ...themeLook, ...r, ...(r.mood ? { mood: [themeLook.mood, r.mood].filter(Boolean).join(" ") } : {}) };
303
+ const q = planQuestions(makePlan({ request, purpose, assets: assetFiles, music: given ?? null, series: series?.id ?? "none", look: themeLook }), caps).questions.find((x) => x.id === "theme")!;
304
+ await candidateSheet(q, (l) => console.error(l));
305
+ return q;
306
+ });
307
+ if (picked) {
308
+ answers.theme = picked;
309
+ open = open.filter((q) => q !== themeQ);
144
310
  }
145
- writeJson(join(dir, "plan.json"), plan);
146
- if (plan.theme) {
147
- const pick = plan.themeCandidates?.find((c) => c.id === plan.theme);
148
- console.error(`테마 ${plan.theme}${pick ? ` — ${pick.why}` : ""}${plan.themeCandidates && plan.themeCandidates.length > 1 ? ` (다른 후보: ${plan.themeCandidates.filter((c) => c.id !== plan.theme).map((c) => c.id).join(" · ")})` : ""}`);
311
+ }
312
+ if (open.length && model !== "claude-code") {
313
+ const asked = await askTerminal(open);
314
+ if (!asked) {
315
+ console.error(`kf make: 만들기 전에 정할 것이 ${open.length}가지 — 플래그로 답해 다시 부른다(--yes 면 권장, --no-gen 이면 돈 안 드는 길)\n\n${questionsText(open)}`);
316
+ return 3;
149
317
  }
150
- if (plan.themeCandidates && plan.themeCandidates.length > 1) themeStills(dir, plan.themeCandidates.map((c) => c.id));
318
+ answers = { ...answers, ...asked };
319
+ open = [];
320
+ }
321
+ for (const [id, v] of Object.entries(answers)) console.error(`능력 ${id}: ${v}(${settled.how[id] === "flag" || forced[id] ? "플래그" : settled.how[id] ? `--${settled.how[id]}` : auto[id] ? "있어서 바로" : "물어서"})`);
322
+ const stops = questions.filter((q) => answers[q.id] === WAIT);
323
+ if (stops.length) {
324
+ console.error(`kf make: 켜고 다시 부른다 —\n${stops.map((q) => ` ${q.ask}: ${q.choices.find((c) => c.value === WAIT)?.note}`).join("\n")}`);
325
+ return 3;
326
+ }
151
327
 
152
- if (model !== "claude-code") await refinePlan(dir, model, request);
328
+ const dir = resolve(str("out") ?? `${purpose}-${stamp()}`);
329
+ Object.assign(at, { dir, command: "make" });
330
+ const newArgs = [kfBin, "new", dir, "--template", purpose, "--request", request, "--series", series?.id ?? "none"];
331
+ if (series?.end?.line) newArgs.push("--line", series.end.line);
332
+ // 박자 분석을 빼기로 했으면 곡도 깔지 않는다 — 박 없이 곡만 까는 길은 없다
333
+ const song = answers.beats === "none" ? undefined : (music ?? (answers.music && !/^(gen:|none$|wait$)/.test(answers.music) ? answers.music : undefined));
334
+ if (song) newArgs.push("--music", song);
335
+ if (str("title")) newArgs.push("--title", str("title")!);
336
+ // 테마 질문의 답(플래그·터미널) → 아직 묻는 중이거나 묻지 않은 테마(시리즈·요청이 짚은 것)는 auto. 테마 없이 프리셋만 쓰려면 --theme none
337
+ // 「알아서」·묻는 중이면 후보 첫째 — 분위기·레퍼런스로 고른 넷이라 kf new 의 auto(요청 낱말만)와 다를 수 있다
338
+ const firstPick = themeQ?.choices.find((c) => c.value !== "auto")?.value;
339
+ const themeArg = answers.theme && answers.theme !== WAIT && answers.theme !== "auto" ? answers.theme : (str("theme") ?? (firstPick && (answers.theme === "auto" || open.includes(themeQ!)) ? firstPick : "auto"));
340
+ newArgs.push("--theme", themeArg);
341
+ if (opts.force) newArgs.push("--force");
342
+ const made = spawnSync(process.execPath, newArgs, { stdio: ["ignore", "inherit", "inherit"] });
343
+ if (made.status !== 0) return made.status ?? 1;
153
344
 
154
- const gen = { approve: !!opts.yes, maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
155
- // claude-code 모드는 돈이 드는 그림을 묻지 않고 만들지 않는다 — 막연한 자동 프롬프트로 엉뚱한 그림이 나왔다. 지시서에 목록으로 낸다
156
- if (!opts["no-gen"] && model !== "claude-code") await genImages(dir);
345
+ const studio = await attachStudio({ dir, request, model, purpose });
346
+ const unmirror = studio?.mirror();
347
+ if (studio) console.log(`작업실: ${studio.url}`);
348
+ const plan: Plan = { ...applyAnswers(JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")), answers).plan, asks: open, ...(themeLook.mood || themeLook.ref ? { themeLook } : {}) };
349
+ // 새 프로젝트는 이 영상의 기본으로 적는다 — 계획 다듬기가 내레이션을 쓸지도 이걸 본다
350
+ if (captions) {
351
+ const kfPath = join(dir, "kf.json");
352
+ writeJson(kfPath, { ...JSON.parse(readFileSync(kfPath, "utf8")), captions });
353
+ plan.captions = captionMode(captions);
354
+ }
355
+ if (!str("purpose") && !series) {
356
+ plan.confidence = chosen?.p ?? det.confidence;
357
+ plan.alternatives = det.ranked.filter((r) => r.purpose !== purpose).slice(0, 2).map((r) => ({ purpose: r.purpose, p: r.p }));
358
+ plan.reasons = [...(chosen?.reasons ?? []), ...plan.reasons.filter((r) => r !== "사람이 정함")];
359
+ }
360
+ // 장면 다시 쓰기(--rewrite)가 같은 모델을 부른다
361
+ (plan as Plan & { model?: string }).model = model;
362
+ if (assetFiles.length) {
363
+ mkdirSync(join(dir, "assets", "in"), { recursive: true });
364
+ for (const f of assetFiles) if (!AUDIO.test(f)) copyFileSync(f, join(dir, "assets", "in", basename(f)));
365
+ }
366
+ writeJson(join(dir, "plan.json"), plan);
367
+ if (plan.theme) {
368
+ const pick = plan.themeCandidates?.find((c) => c.id === plan.theme);
369
+ console.error(`테마 ${plan.theme}${pick ? ` — ${pick.why}` : ""}${plan.themeCandidates && plan.themeCandidates.length > 1 ? ` (다른 후보: ${plan.themeCandidates.filter((c) => c.id !== plan.theme).map((c) => c.id).join(" · ")})` : ""}`);
370
+ }
371
+ if (plan.themeCandidates && plan.themeCandidates.length > 1) themeStills(dir, plan.themeCandidates.map((c) => c.id));
157
372
 
158
- writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
373
+ // 감독 모드 — 뮤비는 가사 층이 화면을 맡아 장면 문법을 안 쓴다
374
+ const direct = !opts["no-direct"] && purpose !== "mv";
375
+ if (direct) {
376
+ writeJson(join(dir, "plan.json"), { ...plan, direct: true });
377
+ writeFileSync(join(dir, "MAKE.md"), directBrief(dir));
159
378
  if (model === "claude-code") {
160
- const flags = [gen.approve ? "--yes" : "", gen.videoProvider ? `--video-provider ${gen.videoProvider}` : ""].filter(Boolean).join(" ");
161
- console.log(`\n${dir}/MAKE.md 를 읽고 scenes/*.ts 를 채운다. 다 채우면(이음매 영상은 그때 앞뒤 장면 프레임으로 만든다):\n kf make --finish ${dir}${flags ? ` ${flags}` : ""}`);
162
- // 장면은 세션이 쓴다 — 작업실 미리보기가 저장할 때마다 따라온다
163
- console.error("장면 쓰는 중(세션) — 저장할 때마다 작업실 미리보기에 바로 보인다");
379
+ if (open.length) console.log(`\n먼저 사람에게 물을 것 ${open.length}가지 — ${dir}/MAKE.md 맨 위 「사용자에게 먼저 물을 것」(AskUserQuestion), 답은 kf make --answer ${dir} ${open.map((q) => `${q.flag} <값>`).join(" ")}`);
380
+ console.log(`\n감독 모드 — ${dir}/MAKE.md 를 읽고 ${dir}/content.json 을 쓴다(장면 코드는 안 쓴다). 그다음:\n kf direct ${dir}\n kf look ${dir}\n kf make --finish ${dir}`);
164
381
  await unmirror?.();
165
382
  await studio?.send({ phase: "scenes" });
166
383
  return 0;
167
384
  }
168
- // 이음매는 틀의 3D 전환이 맡는다 — 모델이 다시 쓰지 않는다
169
- await writeScenes(dir, model, readProject(dir).scenes.filter((s) => s.role !== "seam").map((s) => s.id));
385
+ if (!(await writeContent(dir, model))) {
386
+ console.error(`kf make: ${model} 이 맞는 content.json 을 못 냈다 — ${dir}/content.json 을 손보고 kf direct ${dir}`);
387
+ return 1;
388
+ }
389
+ const d = spawnSync(process.execPath, [kfBin, "direct", dir], { stdio: ["ignore", "inherit", "inherit"] });
390
+ if (d.status !== 0 && d.status !== 1) return d.status ?? 1;
391
+ const gen = { approve: !!opts.yes || videoApproved(plan), maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
170
392
  const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen });
171
393
  await unmirror?.();
172
394
  await studio?.send({ done: true, ok: code === 0 });
173
395
  return code;
174
- },
175
- };
176
- export default cmd;
396
+ }
397
+
398
+ if (model !== "claude-code") {
399
+ await refinePlan(dir, model, request);
400
+ // 내레이션 먼저 — 계획 다듬기가 쓴 장면별 문장을 읽혀 장면 길이를 목소리에 맞춘 뒤 장면을 쓴다
401
+ const code = narrateStep(dir);
402
+ if (code !== 0) return code;
403
+ }
404
+
405
+ const gen = { approve: !!opts.yes || videoApproved(plan), maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
406
+ // claude-code 모드는 그림·3D 샷을 세션이 만든다 — 막연한 자동 프롬프트로 엉뚱한 그림이 나왔다. 지시서에 목록과 고른 공급자로 낸다
407
+ if (model !== "claude-code") {
408
+ await genImages(dir);
409
+ await writeShots(dir, model);
410
+ }
411
+
412
+ writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
413
+ if (model === "claude-code") {
414
+ const flags = [opts.yes ? "--yes" : "", gen.videoProvider ? `--video-provider ${gen.videoProvider}` : ""].filter(Boolean).join(" ");
415
+ if (open.length) console.log(`\n먼저 사람에게 물을 것 ${open.length}가지 — ${dir}/MAKE.md 맨 위 「사용자에게 먼저 물을 것」(AskUserQuestion), 답은 kf make --answer ${dir} ${open.map((q) => `${q.flag} <값>`).join(" ")}`);
416
+ console.log(`\n${dir}/MAKE.md 를 읽고 scenes/*.ts 를 채운다. 다 채우면(이음매 영상은 그때 앞뒤 장면 프레임으로 만든다):\n kf make --finish ${dir}${flags ? ` ${flags}` : ""}`);
417
+ // 장면은 세션이 쓴다 — 작업실 미리보기가 저장할 때마다 따라온다
418
+ console.error("장면 쓰는 중(세션) — 저장할 때마다 작업실 미리보기에 바로 보인다");
419
+ await unmirror?.();
420
+ await studio?.send({ phase: "scenes" });
421
+ return 0;
422
+ }
423
+ // 이음매는 틀의 3D 전환이 맡는다 — 모델이 다시 쓰지 않는다
424
+ await writeScenes(dir, model, readProject(dir).scenes.filter((s) => s.role !== "seam").map((s) => s.id));
425
+ const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen });
426
+ await unmirror?.();
427
+ await studio?.send({ done: true, ok: code === 0 });
428
+ return code;
429
+ }
430
+
431
+ // --- 감독 모드: 모델은 content.json 하나만 — 칸이 틀리면 어느 칸인지 돌려줘 두 번까지 다시
432
+
433
+ function isDirect(dir: string): boolean {
434
+ const p = join(dir, "plan.json");
435
+ return existsSync(p) && JSON.parse(readFileSync(p, "utf8")).direct === true;
436
+ }
437
+
438
+ async function writeContent(dir: string, model: string): Promise<boolean> {
439
+ const plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
440
+ let fix: string | undefined;
441
+ for (let attempt = 0; attempt < 3; attempt++) {
442
+ // 생각하는 모델은 생각에 토큰을 먼저 쓴다 — 상한이 낮으면 답 JSON 이 잘린다
443
+ const r = await chat(model, contentPrompt(dir, fix), { json: true, temperature: 0.5, maxTokens: 16000, stage: "content" });
444
+ const j = jsonBlock<Record<string, unknown>>(r.text);
445
+ if (!j) {
446
+ fix = "JSON 을 못 읽었다 — JSON 하나로만 답한다";
447
+ continue;
448
+ }
449
+ const c = checkContent({ ...j, request: plan.request });
450
+ console.error(`content.json ${attempt + 1}번째(${model}, ${(r.ms / 1000).toFixed(1)}초) — ${c.errors.length ? `칸 오류 ${c.errors.length}` : `장면 ${c.content.scenes.length}`}`);
451
+ if (c.errors.length) {
452
+ fix = c.errors.map((e) => `${e.at}: ${e.msg}`).join("\n");
453
+ continue;
454
+ }
455
+ writeJson(join(dir, "content.json"), { ...j, request: plan.request });
456
+ return true;
457
+ }
458
+ return false;
459
+ }
177
460
 
178
461
  // --- 계획 다듬기: 틀이 정한 뼈대(시각·역할)는 그대로, 이름·끝 한 줄·장면 할 일·생성 프롬프트만 요청에 맞춘다
179
462
 
@@ -193,12 +476,12 @@ async function refinePlan(dir: string, model: string, request: string): Promise<
193
476
  장면: ${JSON.stringify(plan.scenes.map((s) => ({ id: s.id, role: s.role, dur: s.dur, idea: s.idea })))}
194
477
  생성 자산: ${JSON.stringify(plan.gen.map((g) => ({ id: g.id, kind: g.kind, scene: g.scene, prompt: g.prompt })))}
195
478
 
196
- 다음 꼴로: {"title": "제품·주제 이름(짧게)", "line": "끝 카드 한 줄(없으면 빈 글)", "scenes": [{"id": "...", "idea": "이 요청에 맞춘 구체적인 화면·움직임 한두 문장"}], "gen": [{"id": "...", "prompt": "영어 생성 프롬프트, 글자·로고 없이"}]${captionMode(kf.captions) !== "none" ? ', "captions": [{"scene": "장면 id", "text": "그 장면에서 들릴 한국어 내레이션 — 짧은 마디를 쉼표로 이어 장면 길이에 맞게(1초에 4~5글자)"}]' : ""}}`,
479
+ 다음 꼴로: {"title": "제품·주제 이름(짧게)", "line": "끝 카드 한 줄(없으면 빈 글)", "scenes": [{"id": "...", "idea": "이 요청에 맞춘 구체적인 화면·움직임 한두 문장"}], "gen": [{"id": "...", "prompt": "영어 생성 프롬프트, 글자·로고 없이"}]${captionMode(kf.captions) !== "none" ? ', "captions": [{"scene": "장면 id", "text": "그 장면에서 들릴 한국어 내레이션 — 차분한 설명체, 짧은 마디를 쉼표로, 문장은 마침표로. 목소리가 장면 길이를 정하니 1초에 4–5글자(차분한 낭독)로 장면 길이에 가깝게, 훅은 한 마디(끝 장면·이음매는 비워도 된다)"}]' : ""}}`,
197
480
  },
198
481
  ];
199
482
  try {
200
483
  // 생각하는 모델은 생각에 토큰을 먼저 쓴다 — 상한이 낮으면 답 JSON 이 잘린다
201
- const r = await chat(model, ask, { json: true, temperature: 0.5, maxTokens: 16000 });
484
+ const r = await chat(model, ask, { json: true, temperature: 0.5, maxTokens: 16000, stage: "plan" });
202
485
  const j = jsonBlock<{ title?: string; line?: string; scenes?: { id: string; idea: string }[]; gen?: { id: string; prompt: string }[]; captions?: { scene: string; text: string }[] }>(r.text);
203
486
  if (!j) throw new Error(`JSON 을 못 읽었다(${r.text.length}자: ${r.text.slice(0, 160).replace(/\s+/g, " ")})`);
204
487
  if (j.title) plan.title = kf.title = j.title;
@@ -221,19 +504,24 @@ async function refinePlan(dir: string, model: string, request: string): Promise<
221
504
  }
222
505
  }
223
506
 
224
- // --- 장면 쓰기: 장면마다 파일 하나를 받아 놓고, 실제로 열어 오류·이상한 글이 없을 때까지 돌려준다
507
+ // --- 장면 쓰기: 장면마다 파일 하나를 받아 놓고, 실제로 열어 오류·이상한 글이 없을 때까지 돌려준다.
508
+ // 장면끼리는 동시에 쓴다(모델 호출이 대부분 기다림). 열어 보기는 한 번에 하나 — 페이지는 모든 장면을 같이 싣는다.
509
+ // 그래서 떨어진 판은 열어 본 뒤 바로 앞 파일로 되돌린다: 옆 장면의 열어 보기가 남의 깨진 판에 걸리지 않게
510
+
511
+ const probing = gate(1);
225
512
 
226
513
  async function writeScenes(dir: string, model: string, ids: string[], feedback: Record<string, string> = {}): Promise<void> {
227
514
  const brief = readFileSync(join(dir, "MAKE.md"), "utf8");
228
515
  const kf = readProject(dir);
229
- for (const id of ids) {
230
- const s = kf.scenes.find((x) => x.id === id);
231
- if (!s) continue;
516
+ const scenes = ids.map((id) => kf.scenes.find((x) => x.id === id)).filter((s): s is NonNullable<typeof s> => !!s);
517
+ await pool(scenes, jobLimit(3), async (s) => {
232
518
  const file = join(dir, s.file);
233
- let note = feedback[id] ?? "";
234
- console.error(`장면 ${s.id} 쓰는 중(${model})`);
519
+ const before = existsSync(file) ? readFileSync(file, "utf8") : "";
520
+ let note = feedback[s.id] ?? "";
521
+ let tried = "";
522
+ console.error(`장면 ${s.id}: 쓰는 중 (${model})`);
235
523
  for (let attempt = 0; attempt < 3; attempt++) {
236
- const current = existsSync(file) ? readFileSync(file, "utf8") : "";
524
+ const current = tried || before;
237
525
  const messages: Message[] = [
238
526
  { role: "system", content: `너는 kasaframe 장면 파일을 쓴다. 아래 지시서를 지킨다. 답은 TypeScript 코드 블록 하나뿐.\n\n${brief}` },
239
527
  {
@@ -241,7 +529,7 @@ async function writeScenes(dir: string, model: string, ids: string[], feedback:
241
529
  content: `장면 ${s.id}(${s.file}, ${s.dur.toFixed(2)}초, 역할 ${s.role})의 파일 전체를 써라.\n할 일: ${s.idea}\n\n지금 파일:\n\`\`\`ts\n${current}\`\`\`${note ? `\n\n고칠 것:\n${note}` : ""}`,
242
530
  },
243
531
  ];
244
- const r = await chat(model, messages, { temperature: 0.6, maxTokens: 32000 });
532
+ const r = await chat(model, messages, { temperature: 0.6, maxTokens: 32000, stage: `scene:${s.id}` });
245
533
  const code = r.finish === "length" ? null : codeBlock(r.text);
246
534
  if (r.finish === "length") {
247
535
  note = "답이 길이 상한에서 잘렸다. 군더더기 없이 더 짧게, ```ts 블록 하나로 파일 전체를 답하라.";
@@ -252,13 +540,25 @@ async function writeScenes(dir: string, model: string, ids: string[], feedback:
252
540
  note = "코드 블록을 못 찾았다. ```ts 블록 하나로 파일 전체를 답하라.";
253
541
  continue;
254
542
  }
255
- writeFileSync(file, code);
256
- const problems = await probeScene(dir, s.id);
543
+ tried = code;
544
+ const problems = await probing(async () => {
545
+ writeFileSync(file, code);
546
+ const p = await probeScene(dir, s.id);
547
+ if (p.length) writeFileSync(file, before);
548
+ return p;
549
+ });
257
550
  console.error(` ${s.id} ${attempt + 1}번째 ${(r.ms / 1000).toFixed(1)}초 — ${problems.length ? problems.join(" / ") : "통과"}`);
258
- if (!problems.length) break;
551
+ if (!problems.length) {
552
+ console.error(`장면 ${s.id}: 씀`);
553
+ return;
554
+ }
259
555
  note = problems.join("\n");
260
556
  }
261
- }
557
+ // 세 번 다 떨어지면 앞 파일 그대로 — 깨진 판이 통째 렌더를 멈추지 않게. 마지막 판은 out/failed 에 남긴다
558
+ mkdirSync(join(dir, "out", "failed"), { recursive: true });
559
+ if (tried) writeFileSync(join(dir, "out", "failed", `${s.id}.ts`), tried);
560
+ console.error(`장면 ${s.id}: 실패 세 번 다 검사에 걸림 — 앞 파일 그대로(마지막 판 out/failed/${s.id}.ts)`);
561
+ });
262
562
  }
263
563
 
264
564
  // 렌더 전에 장면 하나를 세 시각에서 열어 본다 — 통짜 렌더 1분을 쓰기 전에 잡을 것을 잡는다
@@ -312,6 +612,19 @@ async function probeScene(dir: string, id: string): Promise<string[]> {
312
612
  return [...new Set(problems)];
313
613
  }
314
614
 
615
+ // 테마 고르기 플래그 — --theme-mood·--theme-ref, 그리고 테마 이름이 아닌 --theme 값(분위기 한 마디로 읽는다)
616
+ async function lookFlags(opts: Record<string, string | boolean | undefined>): Promise<LookInput> {
617
+ const look: LookInput = {};
618
+ const theme = typeof opts.theme === "string" ? opts.theme : undefined;
619
+ if (theme && theme !== "auto" && theme !== "none" && !usableTheme(theme)) {
620
+ look.mood = theme;
621
+ delete opts.theme;
622
+ }
623
+ if (typeof opts["theme-mood"] === "string") look.mood = [look.mood, opts["theme-mood"]].filter(Boolean).join(" ");
624
+ if (typeof opts["theme-ref"] === "string") look.ref = await refLook(opts["theme-ref"]);
625
+ return look;
626
+ }
627
+
315
628
  // 테마 후보마다 첫 장면을 그 테마로 한 장 — 사람이 셋을 나란히 보고 고른다. style.json 만 잠깐 바꿔 찍고 되돌린다
316
629
  function themeStills(dir: string, ids: string[]): void {
317
630
  const stylePath = join(dir, "style.json");
@@ -341,20 +654,29 @@ interface GenOpts {
341
654
 
342
655
  const genLog = (l: string) => console.error(` ${l}`);
343
656
 
657
+ // 생성 자산도 make-stats.json 장부에 — 캐시에서 꺼낸 것은 이번에 돈을 안 썼다
658
+ function genSpent(a: Awaited<ReturnType<typeof genImage>>, stage: string): void {
659
+ if (a.cached) return;
660
+ spend({ model: `${a.meta.provider}/${a.meta.model}`, stage, ms: a.meta.elapsedMs, prompt: 0, completion: 0, cached: 0, usd: a.meta.costUsd, og: a.meta.provider.startsWith("og-") });
661
+ }
662
+
344
663
  async function genImages(dir: string): Promise<void> {
345
664
  const plan: Plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
346
665
  const imgs = plan.gen.filter((g) => g.kind === "img");
347
666
  if (!imgs.length) return;
348
667
  console.error(`생성 자산 그림 ${imgs.length}개…`);
349
- for (const g of imgs) {
668
+ // 장당 상한·고른 공급자는 그대로, 부르기만 겹친다
669
+ await pool(imgs, jobLimit(3), async (g) => {
670
+ if (g.scene) console.error(`장면 ${g.scene}: 재료 — 그림 ${g.id}`);
350
671
  try {
351
672
  const size = g.size ?? (plan.aspect === "9:16" ? "1080x1920" : plan.aspect === "1:1" ? "1080x1080" : "1920x1080");
352
673
  const a = await genImage(g.id, { prompt: g.prompt, size, transparent: g.transparent, quality: g.quality, refs: g.refs?.map((r) => join(dir, r)) }, { outDir: join(dir, "assets", "gen"), log: genLog }, g.provider);
674
+ genSpent(a, `gen:img:${g.id}`);
353
675
  console.error(` gen:img:${g.id} ${a.cached ? "cached" : "made"} ${a.path} $${(a.cached ? 0 : a.meta.costUsd).toFixed(3)}`);
354
676
  } catch (e) {
355
677
  console.error(` gen:img:${g.id} ${e instanceof NeedsApproval ? "needs-approval" : "error"} ${e instanceof Error ? e.message : e}`);
356
678
  }
357
- }
679
+ });
358
680
  }
359
681
 
360
682
  async function genVideos(dir: string, o: GenOpts): Promise<void> {
@@ -363,25 +685,204 @@ async function genVideos(dir: string, o: GenOpts): Promise<void> {
363
685
  if (!vids.length) return;
364
686
  console.error(`생성 자산 영상 ${vids.length}개…`);
365
687
  const ctx = { outDir: join(dir, "assets", "gen"), approve: o.approve, maxUsd: o.maxUsd, log: genLog };
366
- for (const g of vids) {
688
+ // 영상 하나마다 승인·상한(maxUsd)을 따로 본다 — 겹쳐 불러도 묻기 틀은 그대로
689
+ await pool(vids, jobLimit(2), async (g) => {
367
690
  const provider = o.videoProvider ?? g.provider;
691
+ console.error(`장면 ${g.scene ?? g.id}: 재료 — 영상 ${g.id}`);
368
692
  try {
369
693
  const a = g.seam
370
694
  ? await seam({ project: dir, scene: g.id, prompt: g.prompt, provider }, ctx)
371
695
  : await genVideo(g.id, { prompt: g.prompt, duration: g.dur ?? 5, aspect: plan.aspect, firstFrame: g.first && join(dir, g.first), lastFrame: g.last && join(dir, g.last) }, ctx, provider);
696
+ genSpent(a, `gen:vid:${g.id}`);
372
697
  console.error(` gen:vid:${g.id} ${a.cached ? "cached" : "made"} ${a.path} $${(a.cached ? 0 : a.meta.costUsd).toFixed(3)}`);
373
698
  } catch (e) {
374
699
  const tail = e instanceof NeedsApproval ? `needs-approval 견적 $${e.estimateUsd.toFixed(3)}(${e.provider}) — --yes 로 승인하면 만든다, 지금은 대체 화면` : `error ${e instanceof Error ? e.message : e}`;
375
700
  console.error(` gen:vid:${g.id} ${tail}`);
376
701
  }
702
+ });
703
+ }
704
+
705
+ // 영상 모델은 사람이 공급자를 골랐을 때(또는 --yes)만 돈을 쓴다 — 아직 묻는 중이면 견적만
706
+ function videoApproved(plan: Partial<Plan>): boolean {
707
+ const v = plan.answers?.video;
708
+ return !!v && v !== "none" && v !== WAIT && !(plan.asks ?? []).some((q) => q.id === "video");
709
+ }
710
+
711
+ // --- 주인공 3D 샷(OG 모드): 모델이 kfb 헬퍼로 샷 스크립트를 쓰고, 몇 장 찍어 보아 돌면 굽는다. 세 번 안에 못 서면 three 로 물러난다
712
+
713
+ function shotGuide(): string {
714
+ const lib = blenderLibDir();
715
+ const readme = readFileSync(join(lib, "README.md"), "utf8");
716
+ const how = readme.slice(readme.indexOf("## 샷 스크립트 짜기"), readme.indexOf("## 판마다 다른 곳"));
717
+ const api = readFileSync(join(lib, "kfb.py"), "utf8").split("\n").filter((l) => /^(def [a-z]\w*\(|class [A-Z])/.test(l)).join("\n");
718
+ return `${how}\n\nkfb 함수(이것만 쓴다):\n${api}\n\n예(등대 섬):\n\`\`\`python\n${readFileSync(join(lib, "shots", "lighthouse.py"), "utf8")}\n\`\`\``;
719
+ }
720
+
721
+ function blenderRun(dir: string, file: string, scene: string, id: string, extra: string[]): { ok: boolean; tail: string } {
722
+ const r = spawnSync(process.execPath, [kfBin, "blender", join(dir, file), "--project", dir, "--scene", scene, "--id", id, ...extra], { encoding: "utf8" });
723
+ const tail = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().split("\n").slice(-25).join("\n");
724
+ return { ok: r.status === 0, tail };
725
+ }
726
+
727
+ async function writeShots(dir: string, model: string): Promise<void> {
728
+ const plan: Plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
729
+ const shots = (plan.shots ?? []).filter((s) => s.tool === "blender");
730
+ if (!shots.length) return;
731
+ const kf = readProject(dir);
732
+ const style = JSON.parse(readFileSync(join(dir, "style.json"), "utf8"));
733
+ const guide = shotGuide();
734
+ for (const shot of shots) {
735
+ const sc = kf.scenes.find((x) => x.id === shot.scene);
736
+ if (!sc) continue;
737
+ mkdirSync(join(dir, "shots"), { recursive: true });
738
+ const peek = [0.15, 0.5, 0.9].map((k) => +(sc.from + sc.dur * k).toFixed(2)).join(",");
739
+ let note = "";
740
+ let ok = false;
741
+ console.error(`3D 샷 ${shot.id} 쓰는 중(${model}) — 장면 ${sc.id} ${sc.dur.toFixed(2)}초`);
742
+ for (let attempt = 0; attempt < 3 && !ok; attempt++) {
743
+ const r = await chat(model, [
744
+ { role: "system", content: `너는 Blender 툰 3D 샷 스크립트(파이썬, tools/blender/kfb.py 헬퍼)를 쓴다. 답은 python 코드 블록 하나뿐.\n\n${guide}` },
745
+ {
746
+ role: "user",
747
+ content: `샷: ${shot.idea}\n장면 ${sc.id}: 프로젝트 ${sc.from.toFixed(2)}–${(sc.from + sc.dur).toFixed(2)}초(update(t) 의 t 는 프로젝트 초), 화면 ${kf.width}x${kf.height}\n색(style.json): ${JSON.stringify(style.colors ?? {})}\n글자는 넣지 않는다(장면이 위에 얹는다). 주인공 하나를 화면 가운데 크게, 쉬지 않게 박에 뛰거나 도는 물체, 카메라 열쇠는 S.downbeats·S.drops 에 두고 도착하면 선다.${note ? `\n\n고칠 것:\n${note}` : ""}`,
748
+ },
749
+ ], { temperature: 0.5, maxTokens: 16000, stage: `shot:${shot.id}` });
750
+ const code = codeBlock(r.text, "python|py");
751
+ if (!code) {
752
+ note = "코드 블록을 못 찾았다. ```python 블록 하나로 파일 전체를 답하라.";
753
+ continue;
754
+ }
755
+ writeFileSync(join(dir, shot.file), code);
756
+ // 스틸 몇 장은 kf still 처럼 잠금 없이 — 남의 긴 렌더 뒤에서 기다리지 않는다(굽기는 잠금을 쓴다)
757
+ const t = blenderRun(dir, shot.file, sc.id, shot.id, ["--still", peek, "--res", "50", "--no-lock"]);
758
+ ok = t.ok;
759
+ note = ok ? "" : `kf blender --still 이 실패했다:\n${t.tail}`;
760
+ console.error(` ${shot.id} ${attempt + 1}번째 ${(r.ms / 1000).toFixed(1)}초 — ${ok ? "스틸 통과" : "실패"}`);
761
+ }
762
+ // 절반 크기·두 장에 한 장 — 무거운 렌더를 짧게(페이지가 늘려 보인다)
763
+ const baked = ok && blenderRun(dir, shot.file, sc.id, shot.id, ["--res", "50", "--step", "2"]).ok;
764
+ if (!baked) {
765
+ console.error(` ${shot.id} 를 못 구웠다 — 장면 ${sc.id} 는 three.js(stage3d)로 3D 를 짓는다`);
766
+ plan.shots = (plan.shots ?? []).map((s) => (s.id === shot.id ? { ...s, tool: "three" } : s));
767
+ writeJson(join(dir, "plan.json"), plan);
768
+ } else console.error(` ${shot.id} → assets/blender/${shot.id}.mp4`);
377
769
  }
378
770
  }
379
771
 
772
+ // --- 묻기: Claude Code 모드에서 세션이 사람에게 물은 답을 넣는다 — 질문은 plan.json 의 asks 를 지금 능력으로 다시 만들어(함수 칸까지) 고른다
773
+ async function answerProject(dir: string, opts: Record<string, string | boolean | undefined>): Promise<number> {
774
+ const file = join(dir, "plan.json");
775
+ if (!existsSync(file)) {
776
+ console.error(`kf make --answer: ${dir} 에 plan.json 이 없다`);
777
+ return 2;
778
+ }
779
+ const plan: Plan = JSON.parse(readFileSync(file, "utf8"));
780
+ // 테마에 분위기 한 마디·레퍼런스를 줬으면 후보 넷을 다시 고르고 견본 시트를 새로 만들어 다시 묻는다 — 답은 그다음
781
+ let look: LookInput;
782
+ try {
783
+ look = await lookFlags(opts);
784
+ } catch (e) {
785
+ console.error(`kf make --answer: ${(e as Error).message}`);
786
+ return 2;
787
+ }
788
+ if (look.mood || look.ref) {
789
+ plan.themeLook = { ...plan.themeLook, ...look, ...(look.mood ? { mood: [plan.themeLook?.mood, look.mood].filter(Boolean).join(" ") } : {}) };
790
+ const q = planQuestions(plan, await capabilities({ probe: false })).questions.find((x) => x.id === "theme");
791
+ if (!q) {
792
+ console.error("kf make --answer: 이 프로젝트는 테마를 묻지 않는다(시리즈나 요청이 테마를 정했다) — --theme <id> 로 바꾼다");
793
+ return 2;
794
+ }
795
+ const sheet = await candidateSheet(q, (l) => console.error(l));
796
+ plan.asks = [...(plan.asks ?? []).filter((x) => x.id !== "theme"), q];
797
+ writeJson(file, plan);
798
+ writeFileSync(join(dir, "MAKE.md"), isDirect(dir) ? directBrief(dir) : projectBrief(dir));
799
+ console.log(`테마 후보를 다시 골랐다${sheet ? ` — 견본 시트 ${sheet}` : ""}:`);
800
+ for (const c of q.choices) console.log(` --theme ${c.value.padEnd(30)} ${c.label}${c.note ? ` — ${c.note}` : ""}`);
801
+ console.error("사람에게 다시 묻고 kf make --answer <폴더> --theme <값> 으로 답을 넣는다");
802
+ return 3;
803
+ }
804
+ const waiting = new Set((plan.asks ?? []).map((q) => q.id));
805
+ const fresh = planQuestions(plan, await capabilities({ probe: true })).questions.filter((q) => waiting.has(q.id));
806
+ const settled = settle(fresh, { given: opts, yes: !!opts.yes, noGen: !!opts["no-gen"], allow: parseAllow(opts.allow) });
807
+ const { plan: next, stop } = applyAnswers(plan, settled.answers, fresh);
808
+ next.asks = (plan.asks ?? []).filter((q) => !(q.id in settled.answers));
809
+ writeJson(file, next);
810
+ if (settled.answers.theme) {
811
+ const was = readProject(dir).theme ?? null;
812
+ const now = retheme(dir, settled.answers.theme);
813
+ console.log(now === was ? `테마 ${now ?? "없음(프리셋)"} 그대로` : `테마 ${was ?? "없음"} → ${now ?? "없음(프리셋)"} — style.json·kf.json·장면 뼈대 DRESS 를 바꿨다`);
814
+ }
815
+ writeFileSync(join(dir, "MAKE.md"), isDirect(dir) ? directBrief(dir) : projectBrief(dir));
816
+ for (const [id, v] of Object.entries(settled.answers)) console.log(`${id}: ${v}`);
817
+ if (stop.length) {
818
+ console.error(`켜고 다시 부른다 —\n${stop.map((x) => ` ${x}`).join("\n")}`);
819
+ return 3;
820
+ }
821
+ if (next.asks.length) {
822
+ console.error(`아직 답이 없다: ${next.asks.map((q) => `${q.flag} <${q.choices.map((c) => c.value).join("|")}>`).join(" ")}`);
823
+ return 3;
824
+ }
825
+ console.log(`${dir}/MAKE.md 를 고쳐 썼다 — 답대로 그림·3D 샷·영상 컷을 만들고 장면을 채운다`);
826
+ return 0;
827
+ }
828
+
829
+ // 맨 위 질문의 테마 답 — kf new 가 auto 로 깐 테마를 바꾼다(plan.json·kf.json·style.json, 장면 뼈대의 DRESS 한 줄).
830
+ // 감독 모드는 kf direct 가 kf.json 의 테마로 장면을 지으니 여기까지면 된다. 바뀐 테마 id(프리셋이면 null)를 돌려준다
831
+ function retheme(dir: string, answer: string): string | null {
832
+ const plan = readPlan(dir) as Plan;
833
+ const kfPath = join(dir, "kf.json");
834
+ const kf = JSON.parse(readFileSync(kfPath, "utf8"));
835
+ const id: string | null =
836
+ answer === "auto" ? (suggestThemes(plan.request, plan.purpose, plan.aspect, 1, plan.themeLook ?? {})[0]?.id ?? plan.themeCandidates?.[0]?.id ?? kf.theme ?? null) : answer === "none" ? null : answer;
837
+ if ((kf.theme ?? null) === id) return id;
838
+ const tpl = loadTemplate(plan.purpose);
839
+ // 프리셋으로 돌아가면 kf new 가 테마 없이 골랐을 프리셋 — 시리즈·요청 낱말·틀 차례
840
+ const preset = id ? null : makePlan({ request: plan.request, purpose: plan.purpose, theme: "none", series: plan.series ?? "none" }).style;
841
+ const style = id ? themeStyle(id) : JSON.parse(readFileSync(join(repo, "presets", `${existsSync(join(repo, "presets", `${preset}.json`)) ? preset : "neutral-dark"}.json`), "utf8"));
842
+ style.motion = { ...style.motion, ...tpl.motion };
843
+ writeJson(join(dir, "style.json"), style);
844
+ if (tpl.moves) {
845
+ const dress = pickDress(dressSeed(plan.request || plan.title, plan.purpose, worldOf(id) ?? preset!), plan.scenes, bundleFor(tpl.moves, id ?? undefined));
846
+ const byId = new Map(plan.scenes.map((s, i) => [s.id, dress[i]]));
847
+ for (const s of kf.scenes as { id: string; file: string; dress?: Record<string, string> }[]) {
848
+ const next = byId.get(s.id);
849
+ if (!s.dress || !next) continue;
850
+ const file = join(dir, s.file);
851
+ // 뼈대가 채운 값만 갈아 끼운다 — 세션이 손댄 줄·뼈대에 박힌 글자 값은 그대로
852
+ if (existsSync(file))
853
+ writeFileSync(file, readFileSync(file, "utf8").replace(/^const DRESS: Dress = \{.*\};$/m, (line) => Object.entries(next).reduce((l, [k, v]) => l.replace(`${k}: "${s.dress![k]}"`, `${k}: "${v}"`), line)));
854
+ s.dress = { ...next };
855
+ }
856
+ }
857
+ if (id) kf.theme = id;
858
+ else delete kf.theme;
859
+ writeJson(kfPath, kf);
860
+ writeJson(join(dir, "plan.json"), { ...readPlan(dir), theme: id, style: id ?? preset });
861
+ return id;
862
+ }
863
+
864
+ // 결과 메타 — 이 영상에 들어간 생성 자산과 회사·바깥 결과물에 걸리는 제한(H3 공개 가중치 등)을 한 파일에
865
+ function writeCredits(dir: string, video: string): void {
866
+ const read = (sub: string) =>
867
+ existsSync(join(dir, "assets", sub))
868
+ ? readdirSync(join(dir, "assets", sub)).filter((f) => f.endsWith(".json")).map((f) => ({ file: join("assets", sub, f), ...JSON.parse(readFileSync(join(dir, "assets", sub, f), "utf8")) }))
869
+ : [];
870
+ const gen = read("gen").map((m) => ({ id: m.id, kind: m.kind, provider: m.provider, model: m.model, costUsd: m.costUsd, license: m.license, region: m.region, ...(m.restrict ? { restrict: m.restrict } : {}) }));
871
+ const blender = read("blender").map((m) => ({ id: m.file.replace(/^.*\/|\.json$/g, ""), kind: "3d", provider: "blender", model: `Blender ${m.blender}`, costUsd: 0, license: "출력물은 만든 사람 것(Blender GPL 은 출력물에 걸리지 않는다)" }));
872
+ const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
873
+ const restricted = gen.filter((g) => g.restrict);
874
+ const credits = { video: basename(video), answers: plan.answers ?? {}, assets: [...gen, ...blender], costUsd: Number(gen.reduce((n, g) => n + (g.costUsd ?? 0), 0).toFixed(4)), restricted: restricted.map((g) => ({ id: g.id, provider: g.provider, restrict: g.restrict })) };
875
+ const path = join(dir, "out", `${basename(video, ".mp4")}.credits.json`);
876
+ writeJson(path, credits);
877
+ if (restricted.length) console.log(`\n제한 있는 자산 ${restricted.length}개 — 회사·바깥 결과물에 내기 전에 바꾼다:\n${restricted.map((g) => ` ${g.id}(${g.provider}): ${g.restrict}`).join("\n")}`);
878
+ console.log(`결과 메타: ${path}`);
879
+ }
880
+
380
881
  // 이음매 장면의 모션그래픽 전환(transition3d)은 앞 장면 마지막 화면·뒤 장면 첫 화면 두 장을 받아 그 사이를 잇는다.
381
882
  // 장면을 다 쓴 뒤에 찍어야 진짜 화면이 된다 — 그림이 없으면 전환이 style 색으로 대체 화면을 그린다
382
- async function seamStills(dir: string): Promise<void> {
883
+ async function seamStills(dir: string, only?: string[]): Promise<void> {
383
884
  const kf = JSON.parse(readFileSync(join(dir, "kf.json"), "utf8"));
384
- const seams = (kf.scenes as { id: string; role: string }[]).filter((s) => s.role === "seam");
885
+ const seams = (kf.scenes as { id: string; role: string }[]).filter((s) => s.role === "seam" && (!only || only.includes(s.id)));
385
886
  if (!seams.length) return;
386
887
  mkdirSync(join(dir, "assets", "seam"), { recursive: true });
387
888
  for (const s of seams) {
@@ -399,28 +900,46 @@ async function seamStills(dir: string): Promise<void> {
399
900
  // --- 마무리: 렌더 → 검사 → (OG 모드면) 고치기 → 표지
400
901
 
401
902
  // captions 를 주면 그 판만 화면 자막을 켜고 끈다(kf.json 은 그대로) — 굽기·검사·자막 트랙이 같은 값을 본다
402
- async function finish(dir: string, o: { model: string; vision: string; fixes: number; gen?: GenOpts; captions?: "on" | "off" | "fit"; suffix?: string }): Promise<number> {
903
+ // scenes 를 주면 그 장면 둘레(그 장면 + 붙은 이음매)만 — 이음 화면·검사를 그 구간에서. 굽기는 kf render 가 바뀐 조각만 다시 찍는다
904
+ async function finish(dir: string, o: { model: string; vision: string; fixes: number; gen?: GenOpts; captions?: "on" | "off" | "fit"; suffix?: string; scenes?: string[] }): Promise<number> {
403
905
  if (!existsSync(join(dir, "kf.json"))) {
404
906
  console.error(`kf make: ${dir} 에 kf.json 이 없다`);
405
907
  return 2;
406
908
  }
909
+ // 세션이 kf narrate 를 건너뛰었으면 여기서 — 목소리·곡을 깔고 장면 길이를 맞춘 뒤 굽는다
910
+ if (readProject(dir).voice === undefined) {
911
+ const code = narrateStep(dir);
912
+ if (code !== 0) return code;
913
+ }
407
914
  if (o.gen) await genVideos(dir, o.gen);
408
- await seamStills(dir);
915
+ const touched = o.scenes ? around(dir, o.scenes) : undefined;
916
+ await seamStills(dir, touched);
409
917
  const out = join(dir, "out", `video${o.suffix ?? ""}.mp4`);
410
918
  const capFlag = o.captions ? ["--captions", o.captions] : [];
411
919
  let verdict = { code: 0, report: "" };
412
920
  for (let round = 0; ; round++) {
413
- if (round > 0) await seamStills(dir);
414
- const r = spawnSync(process.execPath, [kfBin, "render", dir, "--out", out, "--tabs", "2", ...capFlag], { stdio: ["ignore", "inherit", "inherit"] });
415
- if (r.status !== 0) {
921
+ if (round > 0) await seamStills(dir, touched);
922
+ // 굽기와 검사 사이에 잠금을 놓으면 기다리던 남의 긴 렌더가 끼어든다 — 둘을 한 번에 쥐고 자식은 잠금 없이
923
+ const release = await acquireRenderLock(dir, 3600);
924
+ const r = await relay(["render", dir, "--out", out, "--tabs", "2", "--no-lock", ...capFlag]);
925
+ if (r !== 0) {
926
+ release();
416
927
  console.error("kf make: 렌더가 실패했다");
417
- return r.status ?? 1;
928
+ return r;
418
929
  }
419
- verdict = check(dir, capFlag);
930
+ // 고친 둘레 + 실제로 다시 구운 조각(라이브러리·테마가 바뀌면 안 고친 장면도 다시 굽힌다) — 다 다시 구웠으면 통째 검사
931
+ const scope = touched && redone(out, touched);
932
+ for (const id of scope ?? readProject(dir).scenes.map((x) => x.id)) console.error(`장면 ${id}: 검사`);
933
+ verdict = scope ? checkPart(dir, scope, [...capFlag, "--no-lock"]) : check(dir, [...capFlag, "--no-lock"]);
934
+ release();
935
+ sceneVerdicts(dir, scope);
420
936
  const review = o.vision !== "none" ? await visionReview(dir, o.vision) : {};
421
937
  const toFix: Record<string, string> = { ...review };
422
938
  for (const [id, note] of Object.entries(findingsByScene(dir))) toFix[id] = [toFix[id], note].filter(Boolean).join("\n");
423
- if ((verdict.code === 0 && !Object.keys(review).length) || o.model === "claude-code" || round >= o.fixes) break;
939
+ // 고친 장면만 다시 굽는 길이면 고치기도 그 둘레 안에서 — 안 건드린 장면의 옛 지적으로 남의 장면을 다시 쓰지 않는다
940
+ if (touched) for (const id of Object.keys(toFix)) if (!touched.includes(id)) delete toFix[id];
941
+ // 감독 프로젝트는 kf direct 가 떨어진 장면을 다음 후보로 이미 다시 지었다 — 모델이 장면 코드를 덮어쓰지 않는다
942
+ if ((verdict.code === 0 && !Object.keys(review).length) || o.model === "claude-code" || isDirect(dir) || round >= o.fixes) break;
424
943
  if (!Object.keys(toFix).length) break;
425
944
  console.error(`고치기 ${round + 1}/${o.fixes}: ${Object.keys(toFix).join(" ")}`);
426
945
  writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
@@ -428,19 +947,134 @@ async function finish(dir: string, o: { model: string; vision: string; fixes: nu
428
947
  }
429
948
  const subs = writeCaptions(dir, o.captions && captionMode(o.captions));
430
949
  const cover = coverTime(dir);
431
- spawnSync(process.execPath, [kfBin, "still", dir, "--t", String(cover), "--out", join(dir, "out", "cover.png")], { stdio: "ignore" });
950
+ if (!touched || touched.includes(cover.scene) || !existsSync(join(dir, "out", "cover.png")))
951
+ spawnSync(process.execPath, [kfBin, "still", dir, "--t", String(cover.t), "--out", join(dir, "out", "cover.png")], { stdio: "ignore" });
432
952
  console.log(`\n${[out, join(dir, "out", "cover.png"), ...subs].join("\n")}`);
433
- if (o.vision === "none") lookHint(dir);
953
+ writeCredits(dir, out);
954
+ if (o.vision === "none" && !touched) lookHint(dir);
955
+ else if (o.vision === "none") console.log(`\n장면 모음: kf look ${dir}`);
434
956
  if (verdict.report) console.log(`\n검사:\n${verdict.report}`);
435
957
  return verdict.code;
436
958
  }
437
959
 
960
+ // 틀이 내레이션·분위기 곡을 정한 용도(쇼츠·설명)만. 장면별 문장(captions.json)이 없으면 곡만 깐다
961
+ function narrateStep(dir: string): number {
962
+ const tpl = loadTemplate(readProject(dir).template);
963
+ if (!tpl.narration?.on && !tpl.mood) return 0;
964
+ const lines = existsSync(join(dir, "captions.json")) ? JSON.parse(readFileSync(join(dir, "captions.json"), "utf8")) : [];
965
+ const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
966
+ const voice = plan.voice ?? plan.answers?.voice;
967
+ // 문장이 없다고 목소리를 조용히 빼지 않는다 — 사람이 목소리를 none 으로 고른 때만 곡만 깐다
968
+ if (!lines.length && tpl.narration?.on && voice !== "none") {
969
+ console.error(`kf make: 내레이션 문장(captions.json)이 없다 — 장면별 문장을 쓰고 kf narrate ${dir}, 목소리를 빼려면 kf make --answer ${dir} --voice none`);
970
+ return 3;
971
+ }
972
+ const r = spawnSync(process.execPath, [kfBin, "narrate", dir], { stdio: ["ignore", "inherit", "inherit"] });
973
+ if (r.status !== 0) console.error("kf make: kf narrate 가 실패했다 — 위 메시지를 보고 고친 뒤 kf narrate <폴더>");
974
+ return r.status ?? 1;
975
+ }
976
+
977
+ // 자식 kf 의 줄을 이 프로세스 console 로 다시 찍는다 — 작업실 거울(attachStudio.mirror)은 console.error 만 봐서, 물려준 출력(inherit)은 작업실에 안 닿는다
978
+ function relay(args: string[]): Promise<number> {
979
+ return new Promise((done) => {
980
+ const p = spawn(process.execPath, [kfBin, ...args], { stdio: ["ignore", "pipe", "pipe"] });
981
+ for (const [stream, out] of [[p.stdout, console.log], [p.stderr, console.error]] as const) {
982
+ let buf = "";
983
+ stream.setEncoding("utf8");
984
+ stream.on("data", (chunk: string) => {
985
+ buf += chunk;
986
+ const parts = buf.split(/\r?\n/);
987
+ buf = parts.pop() ?? "";
988
+ for (const l of parts) out(l);
989
+ });
990
+ stream.on("end", () => buf && out(buf));
991
+ }
992
+ p.on("close", (code) => done(code ?? 1));
993
+ });
994
+ }
995
+
438
996
  // 폴더를 주면 kf check 가 페이지를 열어 프레임마다 훑는다 — mp4 를 OCR 하는 것보다 정확하다
439
997
  function check(dir: string, extra: string[] = []): { code: number; report: string } {
440
998
  const r = spawnSync(process.execPath, [kfBin, "check", dir, ...extra], { encoding: "utf8" });
441
999
  return { code: r.status ?? 1, report: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
442
1000
  }
443
1001
 
1002
+ // 굽기 보고서(kf render 가 영상 옆에 남긴 .json)의 다시 구운 조각을 더한다. 전부면 undefined — 통째로
1003
+ function redone(video: string, touched: string[]): string[] | undefined {
1004
+ const all = readProject(dirname(dirname(video))).scenes.map((s) => s.id);
1005
+ let rendered: string[] = [];
1006
+ try {
1007
+ rendered = JSON.parse(readFileSync(video.replace(/\.[^.]+$/, ".json"), "utf8")).segments?.rendered ?? [];
1008
+ } catch {}
1009
+ const scope = all.filter((id) => touched.includes(id) || rendered.includes(id));
1010
+ return scope.length === all.length ? undefined : scope;
1011
+ }
1012
+
1013
+ // 고친 장면과 거기 붙은 이음매 — 이음매는 앞뒤 장면의 화면을 잇는다
1014
+ function around(dir: string, ids: string[]): string[] {
1015
+ const scenes = readProject(dir).scenes;
1016
+ const out = new Set(ids);
1017
+ for (const [i, s] of scenes.entries()) {
1018
+ if (!ids.includes(s.id)) continue;
1019
+ for (const n of [scenes[i - 1], scenes[i + 1]]) if (n?.role === "seam") out.add(n.id);
1020
+ }
1021
+ return scenes.filter((s) => out.has(s.id)).map((s) => s.id);
1022
+ }
1023
+
1024
+ interface CheckJson {
1025
+ ok: boolean;
1026
+ counts: { error: number; warn: number };
1027
+ findings: { rule: string; severity: string; from: number; to?: number; message: string }[];
1028
+ [k: string]: unknown;
1029
+ }
1030
+
1031
+ // 고친 구간만 훑고 out/check.json 의 그 구간 지적만 갈아 끼운다 — 나머지 장면의 앞 검사 결과는 그대로 둔다
1032
+ function checkPart(dir: string, ids: string[], extra: string[] = []): { code: number; report: string } {
1033
+ const scenes = readProject(dir).scenes;
1034
+ const file = join(dir, "out", "check.json");
1035
+ const before: CheckJson | null = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null;
1036
+ // 붙어 있는 장면끼리 한 구간으로 — 페이지를 여는 값이 커서 장면마다 따로 열지 않는다
1037
+ const ranges: [number, number][] = [];
1038
+ for (const s of scenes) {
1039
+ if (!ids.includes(s.id)) continue;
1040
+ const last = ranges.at(-1);
1041
+ if (last && Math.abs(last[1] - s.from) < 1e-6) last[1] = s.from + s.dur;
1042
+ else ranges.push([s.from, s.from + s.dur]);
1043
+ }
1044
+ const found: CheckJson["findings"] = [];
1045
+ const reports: string[] = [];
1046
+ let failed = 0;
1047
+ let last: CheckJson | null = null;
1048
+ for (const [a, b] of ranges) {
1049
+ const part = join(dir, "out", "check-part.json");
1050
+ const r = spawnSync(process.execPath, [kfBin, "check", dir, "--from", a.toFixed(4), "--to", b.toFixed(4), "--json", part, ...extra], { encoding: "utf8" });
1051
+ reports.push(`${r.stdout ?? ""}${r.stderr ?? ""}`.trim());
1052
+ if (r.status === 2 || !existsSync(part)) failed = 2;
1053
+ else {
1054
+ last = JSON.parse(readFileSync(part, "utf8"));
1055
+ found.push(...last!.findings);
1056
+ rmSync(part, { force: true });
1057
+ }
1058
+ }
1059
+ if (failed) return { code: 2, report: reports.join("\n\n") };
1060
+ const inside = (t: number) => ranges.some(([a, b]) => t >= a - 1e-6 && t < b - 1e-6);
1061
+ const findings = [...(before?.findings ?? []).filter((f) => !inside(f.from)), ...found].sort((x, y) => x.from - y.from);
1062
+ const counts = { error: findings.filter((f) => f.severity === "error").length, warn: findings.filter((f) => f.severity === "warn").length };
1063
+ writeJson(file, { ...(before ?? last), findings, counts, ok: counts.error === 0, part: ranges });
1064
+ return { code: counts.error ? 1 : 0, report: reports.join("\n\n") };
1065
+ }
1066
+
1067
+ // 장면마다 검사 결과 한 줄 — 작업실 타임라인이 「끝」·「실패」로 읽는다
1068
+ function sceneVerdicts(dir: string, only?: string[]): void {
1069
+ const file = join(dir, "out", "check.json");
1070
+ const findings: CheckJson["findings"] = existsSync(file) ? (JSON.parse(readFileSync(file, "utf8")).findings ?? []) : [];
1071
+ for (const s of readProject(dir).scenes) {
1072
+ if (only && !only.includes(s.id)) continue;
1073
+ const errs = findings.filter((f) => f.severity === "error" && f.from >= s.from - 1e-6 && f.from < s.from + s.dur);
1074
+ console.error(errs.length ? `장면 ${s.id}: 실패 검사 오류 ${errs.length} — ${[...new Set(errs.map((f) => f.rule))].join(", ")}` : `장면 ${s.id}: 끝`);
1075
+ }
1076
+ }
1077
+
444
1078
  // out/check.json 의 오류를 시각으로 장면에 묶는다 — 장면마다 자기 몫의 지적만 받아 다시 쓴다
445
1079
  function findingsByScene(dir: string): Record<string, string> {
446
1080
  const file = join(dir, "out", "check.json");
@@ -479,10 +1113,10 @@ async function visionReview(dir: string, model: string): Promise<Record<string,
479
1113
  }
480
1114
  }
481
1115
 
482
- function coverTime(dir: string): number {
1116
+ function coverTime(dir: string): { scene: string; t: number } {
483
1117
  const kf = readProject(dir);
484
1118
  const pick = kf.scenes.find((s) => s.role === "drop") ?? kf.scenes.find((s) => s.role === "hook" || s.role === "reveal") ?? kf.scenes[0];
485
- return +(pick.from + Math.min(0.8, pick.dur / 2)).toFixed(3);
1119
+ return { scene: pick.id, t: +(pick.from + Math.min(0.8, pick.dur / 2)).toFixed(3) };
486
1120
  }
487
1121
 
488
1122
  // --- 그 밖
@@ -537,6 +1171,19 @@ function stamp(): string {
537
1171
  return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
538
1172
  }
539
1173
 
1174
+ // content.json 의 say 와 지금 깔린 목소리 문장(captions.json)이 다른 장면이 있나
1175
+ function sayChanged(dir: string, ids: string[]): boolean {
1176
+ const read = (f: string) => (existsSync(join(dir, f)) ? JSON.parse(readFileSync(join(dir, f), "utf8")) : null);
1177
+ const content = read("content.json") as { scenes?: { id: string; say?: string }[] } | null;
1178
+ const lines = (read("captions.json") ?? []) as { scene?: string; text?: string }[];
1179
+ return ids.some((id) => (content?.scenes?.find((s) => s.id === id)?.say ?? "").trim() !== lines.filter((l) => l.scene === id).map((l) => l.text?.trim() ?? "").join(" ").trim());
1180
+ }
1181
+
1182
+ function readPlan(dir: string): Partial<Plan> & { model?: string } {
1183
+ const p = join(dir, "plan.json");
1184
+ return existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : {};
1185
+ }
1186
+
540
1187
  function writeJson(path: string, v: unknown): void {
541
1188
  writeFileSync(path, `${JSON.stringify(v, null, 2)}\n`);
542
1189
  }