kasaframe 0.0.0-stage → 0.1.3

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 (442) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +148 -0
  3. package/README.md +92 -2
  4. package/app/package.json +12 -0
  5. package/app/server/engine.ts +533 -0
  6. package/app/server/main.ts +209 -0
  7. package/app/web/env.d.ts +2 -0
  8. package/app/web/icons.ts +31 -0
  9. package/app/web/index.html +21 -0
  10. package/app/web/main.ts +1195 -0
  11. package/app/web/style.css +463 -0
  12. package/bin/hooks.mjs +64 -0
  13. package/bin/kf.mjs +32 -0
  14. package/docs/AGENTS.md +286 -0
  15. package/music/README.md +37 -0
  16. package/package.json +44 -4
  17. package/packages/adapters/package.json +36 -0
  18. package/packages/adapters/src/css.ts +11 -0
  19. package/packages/adapters/src/gsap.ts +26 -0
  20. package/packages/adapters/src/index.ts +9 -0
  21. package/packages/adapters/src/lottie.ts +22 -0
  22. package/packages/adapters/src/media.ts +59 -0
  23. package/packages/adapters/src/page.ts +229 -0
  24. package/packages/adapters/src/project.ts +214 -0
  25. package/packages/adapters/src/react.ts +36 -0
  26. package/packages/adapters/src/svelte.ts +22 -0
  27. package/packages/adapters/src/three.ts +18 -0
  28. package/packages/adapters/src/vue.ts +27 -0
  29. package/packages/check/package.json +19 -0
  30. package/packages/check/src/check.ts +280 -0
  31. package/packages/check/src/cli.ts +70 -0
  32. package/packages/check/src/image.ts +146 -0
  33. package/packages/check/src/index.ts +9 -0
  34. package/packages/check/src/ocr.swift +37 -0
  35. package/packages/check/src/probe.ts +328 -0
  36. package/packages/check/src/report.ts +44 -0
  37. package/packages/check/src/rules/assets.ts +45 -0
  38. package/packages/check/src/rules/beats.ts +212 -0
  39. package/packages/check/src/rules/camera.ts +217 -0
  40. package/packages/check/src/rules/captions.ts +49 -0
  41. package/packages/check/src/rules/flash.ts +235 -0
  42. package/packages/check/src/rules/frames.ts +136 -0
  43. package/packages/check/src/rules/layout.ts +281 -0
  44. package/packages/check/src/rules/page.ts +87 -0
  45. package/packages/check/src/rules/runs.ts +20 -0
  46. package/packages/check/src/rules/text.ts +326 -0
  47. package/packages/check/src/scan.ts +240 -0
  48. package/packages/check/src/serve.ts +67 -0
  49. package/packages/check/src/types.ts +80 -0
  50. package/packages/check/src/video.ts +137 -0
  51. package/packages/cli/package.json +30 -0
  52. package/packages/cli/src/blur.ts +80 -0
  53. package/packages/cli/src/browser.ts +203 -0
  54. package/packages/cli/src/command.ts +44 -0
  55. package/packages/cli/src/commands/app.ts +25 -0
  56. package/packages/cli/src/commands/beats.ts +36 -0
  57. package/packages/cli/src/commands/bench.ts +123 -0
  58. package/packages/cli/src/commands/blender.ts +293 -0
  59. package/packages/cli/src/commands/check.ts +25 -0
  60. package/packages/cli/src/commands/direct.ts +210 -0
  61. package/packages/cli/src/commands/doctor.ts +30 -0
  62. package/packages/cli/src/commands/gen.ts +173 -0
  63. package/packages/cli/src/commands/look.ts +46 -0
  64. package/packages/cli/src/commands/lyrics.ts +157 -0
  65. package/packages/cli/src/commands/make.ts +1121 -0
  66. package/packages/cli/src/commands/music.ts +78 -0
  67. package/packages/cli/src/commands/narrate.ts +238 -0
  68. package/packages/cli/src/commands/new.ts +185 -0
  69. package/packages/cli/src/commands/preview.ts +43 -0
  70. package/packages/cli/src/commands/render.ts +250 -0
  71. package/packages/cli/src/commands/series.ts +110 -0
  72. package/packages/cli/src/commands/setup.ts +102 -0
  73. package/packages/cli/src/commands/sheet.ts +114 -0
  74. package/packages/cli/src/commands/skill.ts +43 -0
  75. package/packages/cli/src/commands/still.ts +40 -0
  76. package/packages/cli/src/commands/theme.ts +172 -0
  77. package/packages/cli/src/commands/transitions.ts +40 -0
  78. package/packages/cli/src/deps.ts +122 -0
  79. package/packages/cli/src/ffmpeg.ts +81 -0
  80. package/packages/cli/src/index.ts +8 -0
  81. package/packages/cli/src/kf.ts +64 -0
  82. package/packages/cli/src/lock.ts +72 -0
  83. package/packages/cli/src/mix.ts +131 -0
  84. package/packages/cli/src/out.ts +13 -0
  85. package/packages/cli/src/pool.ts +34 -0
  86. package/packages/cli/src/segments.ts +402 -0
  87. package/packages/cli/src/serve.ts +124 -0
  88. package/packages/clock/package.json +14 -0
  89. package/packages/clock/src/index.ts +25 -0
  90. package/packages/clock/src/install.ts +351 -0
  91. package/packages/clock/src/types.ts +45 -0
  92. package/packages/core/package.json +9 -0
  93. package/packages/core/src/beats.ts +283 -0
  94. package/packages/core/src/ease.ts +87 -0
  95. package/packages/core/src/frame.ts +99 -0
  96. package/packages/core/src/index.ts +27 -0
  97. package/packages/core/src/project.ts +157 -0
  98. package/packages/core/src/rand.ts +56 -0
  99. package/packages/core/src/spring.ts +52 -0
  100. package/packages/core/src/style.ts +100 -0
  101. package/packages/core/src/time.ts +88 -0
  102. package/packages/director/package.json +23 -0
  103. package/packages/director/src/assemble.ts +361 -0
  104. package/packages/director/src/brief.ts +122 -0
  105. package/packages/director/src/catalog.ts +49 -0
  106. package/packages/director/src/content.ts +343 -0
  107. package/packages/director/src/fallback.ts +58 -0
  108. package/packages/director/src/index.ts +13 -0
  109. package/packages/director/src/pattern.ts +90 -0
  110. package/packages/director/src/patterns/fill/cells.ts +115 -0
  111. package/packages/director/src/patterns/formula/stack.ts +93 -0
  112. package/packages/director/src/patterns/util.ts +58 -0
  113. package/packages/director/src/scene.ts +62 -0
  114. package/packages/director/src/select.ts +163 -0
  115. package/packages/director/src/transitions.ts +115 -0
  116. package/packages/gen/PROVIDERS.md +90 -0
  117. package/packages/gen/package.json +9 -0
  118. package/packages/gen/src/ask.ts +132 -0
  119. package/packages/gen/src/audio.ts +91 -0
  120. package/packages/gen/src/blender.ts +54 -0
  121. package/packages/gen/src/capabilities.ts +151 -0
  122. package/packages/gen/src/generate.ts +206 -0
  123. package/packages/gen/src/image/codex.ts +117 -0
  124. package/packages/gen/src/image/og.ts +133 -0
  125. package/packages/gen/src/index.ts +26 -0
  126. package/packages/gen/src/keys.ts +86 -0
  127. package/packages/gen/src/media.ts +150 -0
  128. package/packages/gen/src/music/elevenlabs.ts +41 -0
  129. package/packages/gen/src/music/index.ts +120 -0
  130. package/packages/gen/src/music/library.ts +73 -0
  131. package/packages/gen/src/music/lyria.ts +52 -0
  132. package/packages/gen/src/music/types.ts +28 -0
  133. package/packages/gen/src/plan.ts +106 -0
  134. package/packages/gen/src/registry.ts +54 -0
  135. package/packages/gen/src/seam.ts +103 -0
  136. package/packages/gen/src/tts/dashscope.ts +64 -0
  137. package/packages/gen/src/tts/elevenlabs.ts +84 -0
  138. package/packages/gen/src/tts/gemini.ts +73 -0
  139. package/packages/gen/src/tts/index.ts +241 -0
  140. package/packages/gen/src/tts/local.ts +42 -0
  141. package/packages/gen/src/tts/types.ts +44 -0
  142. package/packages/gen/src/types.ts +90 -0
  143. package/packages/gen/src/video/fal.ts +140 -0
  144. package/packages/gen/src/video/h3.ts +112 -0
  145. package/packages/gen/src/video/http.ts +44 -0
  146. package/packages/gen/src/video/ltx.ts +60 -0
  147. package/packages/gen/src/video/minimax.ts +55 -0
  148. package/packages/gen/src/video/veo.ts +71 -0
  149. package/packages/jizura/package.json +16 -0
  150. package/packages/jizura/src/deny.ts +73 -0
  151. package/packages/jizura/src/index.ts +7 -0
  152. package/packages/jizura/src/install.ts +269 -0
  153. package/packages/jizura/src/layer.ts +325 -0
  154. package/packages/jizura/src/lyrics.ts +81 -0
  155. package/packages/jizura/src/pin.ts +8 -0
  156. package/packages/jizura/src/vendor.ts +140 -0
  157. package/packages/jizura/src/vite.ts +63 -0
  158. package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
  159. package/packages/jizura/vendor/NOTICE +43 -0
  160. package/packages/jizura/vendor/engine.json +47 -0
  161. package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
  162. package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
  163. package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
  164. package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
  165. package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  166. package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
  167. package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
  168. package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
  169. package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
  170. package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
  171. package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
  172. package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
  173. package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
  174. package/packages/jizura/vendor/fonts/fonts.json +52 -0
  175. package/packages/jizura/vendor/jizura_engine.js +35024 -0
  176. package/packages/motion/README.md +153 -0
  177. package/packages/motion/package.json +30 -0
  178. package/packages/motion/src/camera.ts +97 -0
  179. package/packages/motion/src/captions.ts +487 -0
  180. package/packages/motion/src/enter.ts +128 -0
  181. package/packages/motion/src/hangul.ts +129 -0
  182. package/packages/motion/src/impact.ts +100 -0
  183. package/packages/motion/src/index.ts +17 -0
  184. package/packages/motion/src/moves/brushWipe.ts +103 -0
  185. package/packages/motion/src/moves/cloneArray.ts +111 -0
  186. package/packages/motion/src/moves/curtains.ts +81 -0
  187. package/packages/motion/src/moves/dress.ts +191 -0
  188. package/packages/motion/src/moves/echoStack.ts +70 -0
  189. package/packages/motion/src/moves/fit.ts +70 -0
  190. package/packages/motion/src/moves/flood.ts +61 -0
  191. package/packages/motion/src/moves/ghostStack.ts +88 -0
  192. package/packages/motion/src/moves/gridFloor.ts +100 -0
  193. package/packages/motion/src/moves/imageSwarm.ts +113 -0
  194. package/packages/motion/src/moves/index.ts +38 -0
  195. package/packages/motion/src/moves/irisBlades.ts +113 -0
  196. package/packages/motion/src/moves/karaokeLine.ts +108 -0
  197. package/packages/motion/src/moves/ledBoard.ts +94 -0
  198. package/packages/motion/src/moves/liquidTab.ts +72 -0
  199. package/packages/motion/src/moves/markerWrite.ts +63 -0
  200. package/packages/motion/src/moves/menu.ts +138 -0
  201. package/packages/motion/src/moves/morphBox.ts +96 -0
  202. package/packages/motion/src/moves/neonSign.ts +78 -0
  203. package/packages/motion/src/moves/panelPop.ts +78 -0
  204. package/packages/motion/src/moves/paperTear.ts +75 -0
  205. package/packages/motion/src/moves/path.ts +64 -0
  206. package/packages/motion/src/moves/pathText.ts +107 -0
  207. package/packages/motion/src/moves/pointSphere.ts +109 -0
  208. package/packages/motion/src/moves/ransomLetters.ts +73 -0
  209. package/packages/motion/src/moves/shapeMorph.ts +143 -0
  210. package/packages/motion/src/moves/slamWords.ts +96 -0
  211. package/packages/motion/src/moves/sparkTrail.ts +130 -0
  212. package/packages/motion/src/moves/speedLines.ts +91 -0
  213. package/packages/motion/src/moves/splitFlap.ts +149 -0
  214. package/packages/motion/src/moves/stepWords.ts +128 -0
  215. package/packages/motion/src/moves/strikeOut.ts +98 -0
  216. package/packages/motion/src/moves/sunburst.ts +58 -0
  217. package/packages/motion/src/moves/tickerRows.ts +91 -0
  218. package/packages/motion/src/moves/tokenPicker.ts +115 -0
  219. package/packages/motion/src/moves/truchet.ts +85 -0
  220. package/packages/motion/src/moves/wordWall.ts +78 -0
  221. package/packages/motion/src/moves/worldRoll.ts +32 -0
  222. package/packages/motion/src/particles.ts +102 -0
  223. package/packages/motion/src/react/index.tsx +21 -0
  224. package/packages/motion/src/react/parts.tsx +431 -0
  225. package/packages/motion/src/react/time.tsx +97 -0
  226. package/packages/motion/src/text.ts +103 -0
  227. package/packages/motion/src/transition.ts +82 -0
  228. package/packages/motion/src/transition3d/cards.ts +65 -0
  229. package/packages/motion/src/transition3d/chain.ts +84 -0
  230. package/packages/motion/src/transition3d/dive.ts +181 -0
  231. package/packages/motion/src/transition3d/fold.ts +110 -0
  232. package/packages/motion/src/transition3d/gl.ts +219 -0
  233. package/packages/motion/src/transition3d/index.ts +235 -0
  234. package/packages/motion/src/transition3d/live.ts +70 -0
  235. package/packages/motion/src/transition3d/portal.ts +115 -0
  236. package/packages/motion/src/transition3d/react.tsx +29 -0
  237. package/packages/motion/src/transition3d/rig.ts +600 -0
  238. package/packages/motion/src/transition3d/shatter.ts +104 -0
  239. package/packages/motion/src/transition3d/space.ts +173 -0
  240. package/packages/motion/src/transition3d/whip.ts +78 -0
  241. package/packages/motion/src/transition3d/world.ts +737 -0
  242. package/packages/motion/src/transition3d/zoomOut.ts +114 -0
  243. package/packages/motion/src/transitions2d/catalog.ts +127 -0
  244. package/packages/motion/src/transitions2d/css.d.ts +2 -0
  245. package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
  246. package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
  247. package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
  248. package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
  249. package/packages/motion/src/transitions2d/index.ts +2 -0
  250. package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
  251. package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
  252. package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
  253. package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
  254. package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
  255. package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
  256. package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
  257. package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
  258. package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
  259. package/packages/motion/src/transitions2d/react.ts +13 -0
  260. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
  261. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
  262. package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
  263. package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
  264. package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
  265. package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
  266. package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
  267. package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
  268. package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
  269. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
  270. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
  271. package/packages/motion/src/type.ts +131 -0
  272. package/packages/motion/src/ui.ts +172 -0
  273. package/packages/patterns/package.json +15 -0
  274. package/packages/patterns/src/bars/rank.ts +119 -0
  275. package/packages/patterns/src/before-after/shrink.ts +127 -0
  276. package/packages/patterns/src/before-after/strike.ts +125 -0
  277. package/packages/patterns/src/code/terminal.ts +133 -0
  278. package/packages/patterns/src/compare/bars.ts +142 -0
  279. package/packages/patterns/src/compare/duel.ts +103 -0
  280. package/packages/patterns/src/end/lockup.ts +85 -0
  281. package/packages/patterns/src/end/slam.ts +91 -0
  282. package/packages/patterns/src/flow/graph.ts +250 -0
  283. package/packages/patterns/src/hook-number/counter.ts +101 -0
  284. package/packages/patterns/src/hook-number/roll.ts +179 -0
  285. package/packages/patterns/src/hook-number/slam.ts +91 -0
  286. package/packages/patterns/src/index.ts +38 -0
  287. package/packages/patterns/src/kit.ts +449 -0
  288. package/packages/patterns/src/limit/gauge.ts +117 -0
  289. package/packages/patterns/src/list/cards.ts +131 -0
  290. package/packages/patterns/src/list/cycle.ts +22 -0
  291. package/packages/patterns/src/media/panels.ts +100 -0
  292. package/packages/patterns/src/objects.ts +234 -0
  293. package/packages/patterns/src/quote/karaoke.ts +126 -0
  294. package/packages/patterns/src/quote/slam.ts +22 -0
  295. package/packages/patterns/src/screen/focus.ts +85 -0
  296. package/packages/patterns/src/steps/path.ts +137 -0
  297. package/packages/patterns/src/steps/stack.ts +111 -0
  298. package/packages/patterns/src/term-reveal/chips.ts +92 -0
  299. package/packages/patterns/src/term-reveal/expand.ts +127 -0
  300. package/packages/patterns/src/timeline/axis.ts +144 -0
  301. package/packages/patterns/src/title/mask.ts +85 -0
  302. package/packages/patterns/src/title/slam.ts +23 -0
  303. package/packages/patterns/src/toggle-sim/switch.ts +92 -0
  304. package/packages/patterns/src/trend/columns.ts +108 -0
  305. package/packages/patterns/src/trend/draw.ts +140 -0
  306. package/packages/patterns/src/types.ts +67 -0
  307. package/packages/patterns/src/words/moves.ts +42 -0
  308. package/packages/patterns/src/words/slam.ts +4 -0
  309. package/packages/plan/package.json +15 -0
  310. package/packages/plan/src/asks.ts +256 -0
  311. package/packages/plan/src/brief.ts +383 -0
  312. package/packages/plan/src/captions.ts +78 -0
  313. package/packages/plan/src/detect.ts +93 -0
  314. package/packages/plan/src/index.ts +27 -0
  315. package/packages/plan/src/layout.ts +176 -0
  316. package/packages/plan/src/llm.ts +172 -0
  317. package/packages/plan/src/look.ts +222 -0
  318. package/packages/plan/src/plan.ts +179 -0
  319. package/packages/plan/src/request.ts +55 -0
  320. package/packages/plan/src/series.ts +76 -0
  321. package/packages/plan/src/studio.ts +53 -0
  322. package/packages/plan/src/templates.ts +42 -0
  323. package/packages/plan/src/themes.ts +48 -0
  324. package/packages/plan/src/types.ts +157 -0
  325. package/packages/studio/package.json +12 -0
  326. package/packages/studio/src/index.ts +1 -0
  327. package/packages/studio/src/plugin.ts +236 -0
  328. package/packages/studio/src/ui/main.ts +503 -0
  329. package/packages/studio/src/ui/studio.css +165 -0
  330. package/packages/studio/src/ui/timeline.ts +317 -0
  331. package/packages/themes/kit/THEME-MAKE.md +68 -0
  332. package/packages/themes/kit/THEME.md +30 -0
  333. package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
  334. package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
  335. package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
  336. package/packages/themes/kit/effects.ts +5 -0
  337. package/packages/themes/kit/sample/index.html +11 -0
  338. package/packages/themes/kit/sample/main.ts +9 -0
  339. package/packages/themes/kit/sample/scenes/scene.ts +15 -0
  340. package/packages/themes/package.json +19 -0
  341. package/packages/themes/src/factory.ts +296 -0
  342. package/packages/themes/src/fx.ts +14 -0
  343. package/packages/themes/src/index.ts +114 -0
  344. package/packages/themes/src/slotkit.ts +79 -0
  345. package/packages/themes/src/slots.ts +46 -0
  346. package/packages/themes/src/stage3d.ts +197 -0
  347. package/packages/themes/src/types.ts +58 -0
  348. package/presets/kawaii-pastel.json +94 -0
  349. package/presets/neutral-dark.json +82 -0
  350. package/presets/og.json +98 -0
  351. package/series/og-shorts.json +14 -0
  352. package/skills/kasaframe/SKILL.md +33 -0
  353. package/templates/_shared/project/index.html +11 -0
  354. package/templates/_shared/project/lib/react.ts +22 -0
  355. package/templates/_shared/project/lib/stage.ts +292 -0
  356. package/templates/_shared/project/main.ts +9 -0
  357. package/templates/_shared/scenes/default.ts +21 -0
  358. package/templates/_shared/scenes/drop.ts +33 -0
  359. package/templates/_shared/scenes/end.ts +26 -0
  360. package/templates/_shared/scenes/hook.ts +18 -0
  361. package/templates/_shared/scenes/seam.ts +41 -0
  362. package/templates/_shared/scenes/step.ts +49 -0
  363. package/templates/ad/preview.png +0 -0
  364. package/templates/ad/template.json +151 -0
  365. package/templates/demo/preview.png +0 -0
  366. package/templates/demo/template.json +141 -0
  367. package/templates/explainer/preview.png +0 -0
  368. package/templates/explainer/template.json +193 -0
  369. package/templates/launch/preview.png +0 -0
  370. package/templates/launch/template.json +163 -0
  371. package/templates/mv/preview.png +0 -0
  372. package/templates/mv/scenes/default.ts +98 -0
  373. package/templates/mv/template.json +178 -0
  374. package/templates/promo/preview.png +0 -0
  375. package/templates/promo/template.json +185 -0
  376. package/templates/shorts/preview.png +0 -0
  377. package/templates/shorts/scenes/hook.ts +37 -0
  378. package/templates/shorts/template.json +196 -0
  379. package/templates/square/preview.png +0 -0
  380. package/templates/square/template.json +133 -0
  381. package/themes/debug-hud/THEME.md +39 -0
  382. package/themes/debug-hud/effects.ts +389 -0
  383. package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
  384. package/themes/debug-hud/sample/beats.json +1 -0
  385. package/themes/debug-hud/sample/index.html +11 -0
  386. package/themes/debug-hud/sample/kf.json +59 -0
  387. package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
  388. package/themes/debug-hud/sample/lib/og.ts +29 -0
  389. package/themes/debug-hud/sample/main.ts +9 -0
  390. package/themes/debug-hud/sample/scenes/end.ts +62 -0
  391. package/themes/debug-hud/sample/scenes/gather.ts +67 -0
  392. package/themes/debug-hud/sample/scenes/models.ts +70 -0
  393. package/themes/debug-hud/sample/scenes/price.ts +79 -0
  394. package/themes/debug-hud/sample/style.json +91 -0
  395. package/themes/debug-hud/slots.ts +99 -0
  396. package/themes/debug-hud/still.png +0 -0
  397. package/themes/debug-hud/style.json +91 -0
  398. package/themes/debug-hud/theme.json +68 -0
  399. package/themes/og-measure/THEME.md +47 -0
  400. package/themes/og-measure/effects.ts +432 -0
  401. package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
  402. package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
  403. package/themes/og-measure/sample/assets/data/measure.json +353 -0
  404. package/themes/og-measure/sample/beats.json +1 -0
  405. package/themes/og-measure/sample/index.html +11 -0
  406. package/themes/og-measure/sample/kf.json +66 -0
  407. package/themes/og-measure/sample/lib/data.ts +62 -0
  408. package/themes/og-measure/sample/main.ts +9 -0
  409. package/themes/og-measure/sample/measure.ts +78 -0
  410. package/themes/og-measure/sample/scenes/end.ts +47 -0
  411. package/themes/og-measure/sample/scenes/gather.ts +51 -0
  412. package/themes/og-measure/sample/scenes/models.ts +34 -0
  413. package/themes/og-measure/sample/scenes/one.ts +39 -0
  414. package/themes/og-measure/sample/scenes/price.ts +56 -0
  415. package/themes/og-measure/sample/style.json +97 -0
  416. package/themes/og-measure/slots.ts +88 -0
  417. package/themes/og-measure/still.png +0 -0
  418. package/themes/og-measure/style.json +97 -0
  419. package/themes/og-measure/theme.json +79 -0
  420. package/themes/riso-zine/THEME.md +43 -0
  421. package/themes/riso-zine/effects.ts +321 -0
  422. package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
  423. package/themes/riso-zine/sample/beats.json +1 -0
  424. package/themes/riso-zine/sample/index.html +11 -0
  425. package/themes/riso-zine/sample/kf.json +59 -0
  426. package/themes/riso-zine/sample/lib/og.ts +29 -0
  427. package/themes/riso-zine/sample/lib/shot.ts +86 -0
  428. package/themes/riso-zine/sample/lib/zine.ts +42 -0
  429. package/themes/riso-zine/sample/main.ts +9 -0
  430. package/themes/riso-zine/sample/scenes/end.ts +61 -0
  431. package/themes/riso-zine/sample/scenes/gather.ts +62 -0
  432. package/themes/riso-zine/sample/scenes/models.ts +52 -0
  433. package/themes/riso-zine/sample/scenes/price.ts +82 -0
  434. package/themes/riso-zine/sample/style.json +99 -0
  435. package/themes/riso-zine/slots.ts +81 -0
  436. package/themes/riso-zine/still.png +0 -0
  437. package/themes/riso-zine/style.json +99 -0
  438. package/themes/riso-zine/theme.json +75 -0
  439. package/tools/beats/beats.py +372 -0
  440. package/tools/beats/requirements.txt +3 -0
  441. package/tools/lyrics/align.py +164 -0
  442. package/tools/lyrics/requirements.txt +9 -0
@@ -0,0 +1,172 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { listThemes, sheetDir, type Theme, themeDims, themesDir } from "@kasaframe/themes";
6
+ import { briefs, review, scaffold, sessionPrompt, setStatus, staticCheck } from "@kasaframe/themes/factory";
7
+ import { launch } from "../browser.ts";
8
+ import { type Command, num, parseFlags } from "../command.ts";
9
+
10
+ const kfBin = new URL("../kf.ts", import.meta.url).pathname;
11
+
12
+ const cmd: Command = {
13
+ summary: "테마 공장 — 참고 자료로 테마 팩 뼈대·지시서, 계약 검사, 견본 review, 사람 OK 뒤 approve",
14
+ usage: `kf theme list
15
+ kf theme new <id> --ref <그림·영상·폴더·URL> [--ref …] [--name 이름] [--like <기존 테마>] [--3d] [--brief ${briefs().join("|")}] [--force]
16
+ kf theme check <id> [--no-page] 계약·빈칸 검사 + kf check sample
17
+ kf theme review <id> [--tabs 2] 굽기 → mp4 검사 → 장면 모음·대표 장면 → 공유 폴더 → status review
18
+ kf theme approve <id> 사람이 견본을 보고 OK 한 뒤에만
19
+ kf theme sheet [--out 파일.png] approved 테마마다 같은 문장 한 장면(쇼츠 9:16)을 감독 모드로 짓고 검사를 거쳐, 장면 끝 프레임을
20
+ 격자 한 장으로 — 이름·2D/3D 딱지, 안전 영역 점선. 기본 themes/_sheet/sheet.png, 칸은 그 옆 <id>.png
21
+ (kf make 의 테마 질문 보기가 이 칸을 견본으로 짚는다). 검사 오류가 남은 칸은 표시하고 종료 1
22
+
23
+ new 는 themes/<id>/ 에 계약 파일 뼈대·refs/·브리프대로 자른 곡과 박·장면 넷 뼈대·THEME-MAKE.md(지시서)를 짓고,
24
+ 그 지시서를 받을 세션을 여는 명령을 알려 준다. 테마는 그 세션이 만든다.`,
25
+ async run(argv) {
26
+ const [sub, ...rest] = argv;
27
+ const { args, opts } = parseFlags(rest);
28
+ const id = args[0];
29
+ if (sub === "sheet") return sheet(resolve(typeof opts.out === "string" ? opts.out : join(sheetDir, "sheet.png")));
30
+ if (sub === "list" || !sub) {
31
+ const all = listThemes();
32
+ if (!all.length) console.log("(테마 없음)");
33
+ for (const t of all) console.log(`${t.id.padEnd(18)} ${t.status.padEnd(9)} ${String(t.approvedAt ?? "").padEnd(11)} 효과 ${String(t.effects.length).padStart(2)} · ${t.metaphor}`);
34
+ return 0;
35
+ }
36
+ if (!id) {
37
+ console.error(`kf theme ${sub}: 테마 id 가 필요하다. kf theme --help`);
38
+ return 2;
39
+ }
40
+ if (sub === "new") {
41
+ // --ref 는 여러 번 — parseFlags 는 마지막 하나만 남기니 따로 모은다
42
+ const refs = rest.flatMap((a, i) => (a === "--ref" && rest[i + 1] ? [rest[i + 1]] : a.startsWith("--ref=") ? [a.slice(6)] : []));
43
+ try {
44
+ const r = scaffold(id, {
45
+ refs,
46
+ name: typeof opts.name === "string" ? opts.name : undefined,
47
+ like: typeof opts.like === "string" ? opts.like : undefined,
48
+ brief: typeof opts.brief === "string" ? opts.brief : undefined,
49
+ kind: opts["3d"] ? "3d" : "flat",
50
+ force: !!opts.force,
51
+ });
52
+ console.log(`${r.dir}/ — 참고 자료 ${r.refs.length}개${r.urls.length ? ` · 주소 ${r.urls.length}개` : ""}`);
53
+ console.log(`지시서: ${r.make}`);
54
+ console.log(`\n세션 열기(레포 뿌리에서):\n claude --model opus "${sessionPrompt(id)}"`);
55
+ return 0;
56
+ } catch (e) {
57
+ console.error(`kf theme new: ${(e as Error).message}`);
58
+ return 2;
59
+ }
60
+ }
61
+ if (sub === "check") {
62
+ const bad = await staticCheck(id);
63
+ for (const b of bad) console.log(`[오류] ${b}`);
64
+ if (!bad.length) console.log("계약·빈칸: 통과");
65
+ let page = 0;
66
+ if (!opts["no-page"]) page = spawnSync(process.execPath, [kfBin, "check", join(themesDir, id, "sample"), "--tabs", String(num(opts.tabs, 2))], { stdio: "inherit" }).status ?? 1;
67
+ return bad.length || page ? 1 : 0;
68
+ }
69
+ if (sub === "review") {
70
+ const bad = await staticCheck(id);
71
+ if (bad.length) {
72
+ for (const b of bad) console.log(`[오류] ${b}`);
73
+ console.error("kf theme review: 계약·빈칸 오류부터 — kf theme check");
74
+ return 1;
75
+ }
76
+ const r = review(id, { tabs: num(opts.tabs, 2), brief: typeof opts.brief === "string" ? opts.brief : undefined });
77
+ for (const n of r.notes) console.log(n);
78
+ if (!r.ok) return 1;
79
+ console.log(`status review · 공유 폴더 ${r.share}`);
80
+ return 0;
81
+ }
82
+ if (sub === "approve") {
83
+ const m = setStatus(id, "approved");
84
+ console.log(`${id} approved ${m.approvedAt} — 사람이 견본을 보고 OK 한 테마만`);
85
+ return 0;
86
+ }
87
+ console.error(`kf theme: 모르는 하위 명령 "${sub}". kf theme --help`);
88
+ return 2;
89
+ },
90
+ };
91
+
92
+ export default cmd;
93
+
94
+ // 견본 시트 — 테마끼리 견주려면 내용이 같아야 한다. 감독 모드가 실제로 짓는 장면(쇼츠 첫 장면 hook-number)을 테마만 바꿔,
95
+ // 검사를 거쳐(떨어진 패턴은 감독이 다음 후보로 다시 짓는다) 사람이 실제로 받을 화면을 장면 끝 프레임에서 찍는다 — 움직임이 다 앉은 자리
96
+ const SAMPLE = {
97
+ title: "kasaframe",
98
+ request: "테마 견본 — 같은 문장을 테마마다",
99
+ scenes: [{ id: "hook", type: "hook-number", value: 57, unit: "×", label: "KV 캐시가 이만큼 작아진다", source: "DeepSeek-V2 기술 보고서", dur: 4 }],
100
+ };
101
+
102
+ async function sheet(out: string): Promise<number> {
103
+ const themes = listThemes({ status: "approved" });
104
+ if (!themes.length) {
105
+ console.error("kf theme sheet: approved 테마가 없다");
106
+ return 1;
107
+ }
108
+ mkdirSync(dirname(out), { recursive: true });
109
+ const tmp = mkdtempSync(join(tmpdir(), "kf-theme-sheet-"));
110
+ const cells: { t: Theme; png: string; failed: boolean }[] = [];
111
+ let frame = { width: 1080, height: 1920, safe: { top: 0, bottom: 0, left: 0, right: 0 }, at: 0 };
112
+ try {
113
+ for (const t of themes) {
114
+ const dir = join(tmp, t.id);
115
+ const png = join(dirname(out), `${t.id}.png`);
116
+ const kf = (...a: string[]) => spawnSync(process.execPath, [kfBin, ...a], { encoding: "utf8" });
117
+ let r = kf("new", dir, "--template", "shorts", "--theme", t.id, "--title", SAMPLE.title, "--series", "none");
118
+ if (r.status === 0) {
119
+ writeFileSync(join(dir, "content.json"), `${JSON.stringify(SAMPLE, null, 2)}\n`);
120
+ r = kf("direct", dir, "--no-narrate", "--no-lock");
121
+ }
122
+ // 1 은 다시 지어도 검사 오류가 남았다는 뜻 — 찍되 칸에 표시한다
123
+ const failed = r.status === 1;
124
+ if (r.status === 0 || failed) {
125
+ const p = JSON.parse(readFileSync(join(dir, "kf.json"), "utf8"));
126
+ const scene = p.scenes.find((x: { id: string }) => x.id === SAMPLE.scenes[0].id);
127
+ frame = { width: p.width, height: p.height, safe: p.safeArea, at: +(scene.from + scene.dur - 1 / p.fps).toFixed(4) };
128
+ r = kf("still", dir, "--t", String(frame.at), "--out", png);
129
+ }
130
+ if (r.status !== 0) {
131
+ console.error(`테마 ${t.id}: 못 찍음 — ${`${r.stderr ?? ""}${r.stdout ?? ""}`.trim().split("\n").slice(-3).join(" / ")}`);
132
+ continue;
133
+ }
134
+ console.error(`테마 ${t.id}: ${png}${failed ? " — 검사 오류가 남았다(out/check.json)" : ""}`);
135
+ cells.push({ t, png, failed });
136
+ }
137
+ if (!cells.length) return 1;
138
+ const w = 360;
139
+ const h = Math.round((w * frame.height) / frame.width);
140
+ const k = w / frame.width;
141
+ const html = `<!doctype html><meta charset="utf-8"><style>
142
+ body { margin: 0; padding: 24px; background: #111; font: 16px/1.35 -apple-system, "Apple SD Gothic Neo", sans-serif; color: #ddd; }
143
+ h1 { margin: 0 0 16px; font-size: 18px; font-weight: 600; color: #fff; }
144
+ main { display: grid; grid-template-columns: repeat(${cells.length}, ${w}px); gap: 16px; }
145
+ figure { margin: 0; position: relative; }
146
+ .safe { position: absolute; left: 0; width: ${w}px; border-top: 2px dashed #ff4d4d; }
147
+ img { display: block; width: ${w}px; height: ${h}px; outline: 1px solid #333; }
148
+ figcaption { padding: 8px 2px 0; }
149
+ b { color: #fff; }
150
+ .d { display: inline-block; margin-left: 6px; padding: 0 6px; border: 1px solid #666; border-radius: 4px; font-size: 13px; }
151
+ small { display: block; color: #999; font-size: 13px; }
152
+ .bad { color: #ff6b6b; }
153
+ </style><h1>같은 문장 한 장면 — 「${SAMPLE.scenes[0].value}${SAMPLE.scenes[0].unit} ${SAMPLE.scenes[0].label}」 · 쇼츠 9:16 · 장면 끝 프레임(${frame.at}초) · 빨간 점선 밖은 플랫폼 UI 자리라 비운다</h1><main>${cells
154
+ .map(
155
+ ({ t, png, failed }) =>
156
+ `<figure><img src="data:image/png;base64,${readFileSync(png).toString("base64")}"><div class="safe" style="top:${Math.round(frame.safe.top * k)}px"></div><div class="safe" style="top:${Math.round((frame.height - frame.safe.bottom) * k)}px"></div><figcaption><b>${t.name}</b><span class="d">${themeDims(t)}</span><small>${t.id}${failed ? ' · <span class="bad">검사 오류 남음</span>' : ""}</small></figcaption></figure>`,
157
+ )
158
+ .join("")}</main>`;
159
+ const browser = await launch();
160
+ try {
161
+ const page = await browser.newPage({ viewport: { width: 48 + cells.length * w + (cells.length - 1) * 16, height: 400 } });
162
+ await page.setContent(html, { waitUntil: "load" });
163
+ writeFileSync(out, await page.screenshot({ fullPage: true, type: "png" }));
164
+ } finally {
165
+ await browser.close();
166
+ }
167
+ console.log(out);
168
+ return cells.length === themes.length && !cells.some((c) => c.failed) ? 0 : 1;
169
+ } finally {
170
+ rmSync(tmp, { recursive: true, force: true });
171
+ }
172
+ }
@@ -0,0 +1,40 @@
1
+ import type { Command } from "../command.ts";
2
+ import { parseFlags } from "../command.ts";
3
+ import { CATALOG, type TransitionEntry } from "@kasaframe/motion/transitions2d";
4
+
5
+ const USE: Record<TransitionEntry["use"], string> = { transition3d: "이름으로", react: "React 부품", vendor: "원본만" };
6
+
7
+ const cmd: Command = {
8
+ summary: "전환 목록 — 이름·출처·라이선스·seek/step",
9
+ usage: `kf transitions [찾을 말] [--source 출처] [--clock seek|step] [--use transition3d|react|vendor] [--json]
10
+ 이름으로(transition3d) 인 것은 장면에서 transition3d(root, ctx, { kind: "<이름>" }) 로 부른다 — 예 gl:crosswarp · smooth:sdf-circle · dive
11
+ React 부품은 @kasaframe/motion/transitions2d/react, 원본만은 geono-hub vendor/transitions/ 에 있다
12
+ step 은 자기 시계(rAF·타이머)라 kf.json 의 mode 를 "step" 으로`,
13
+ async run(argv) {
14
+ const { args, opts } = parseFlags(argv);
15
+ const has = (v: string, q: unknown) => typeof q !== "string" || v.toLowerCase().includes(q.toLowerCase());
16
+ const list = CATALOG.filter(
17
+ (e) =>
18
+ has(e.source, opts.source) &&
19
+ (typeof opts.clock !== "string" || e.clock === opts.clock) &&
20
+ (typeof opts.use !== "string" || e.use === opts.use) &&
21
+ args.every((q) => has(`${e.name} ${e.note ?? ""} ${e.author ?? ""}`, q)),
22
+ );
23
+ if (opts.json) {
24
+ console.log(JSON.stringify(list, null, 2));
25
+ return 0;
26
+ }
27
+ const count = (k: (e: TransitionEntry) => string) => [...list.reduce((m, e) => m.set(k(e), (m.get(k(e)) ?? 0) + 1), new Map<string, number>())].map(([k, n]) => `${k} ${n}`).join(" · ");
28
+ console.log(`${list.length}개 — ${count((e) => e.source)}`);
29
+ console.log(`시계 ${count((e) => e.clock)} · ${count((e) => USE[e.use])}\n`);
30
+ const w = Math.max(...list.map((e) => e.name.length), 4) + 2;
31
+ const sw = Math.max(...list.map((e) => e.source.length), 4) + 2;
32
+ const lw = Math.max(...list.map((e) => e.license.length), 4) + 2;
33
+ for (const e of list) {
34
+ const extra = [e.component && `<${e.component}>`, e.author, e.note].filter(Boolean).join(" — ");
35
+ console.log(`${e.name.padEnd(w)}${e.source.padEnd(sw)}${e.license.padEnd(lw)}${e.clock.padEnd(6)}${USE[e.use].padEnd(9)}${extra}`);
36
+ }
37
+ return 0;
38
+ },
39
+ };
40
+ export default cmd;
@@ -0,0 +1,122 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { homedir, platform } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { hasKey, whereToPut } from "@kasaframe/gen";
8
+ import { jizuraCheck } from "@kasaframe/jizura/install";
9
+ import { launch } from "./browser.ts";
10
+ import { lyricsCheck } from "./commands/lyrics.ts";
11
+
12
+ // kf doctor·kf setup 이 같이 보는 바깥 도구들 — npm 이 못 까는 것(ffmpeg·파이썬 꾸러미·크롬 본체)만.
13
+
14
+ export const requirements = fileURLToPath(new URL("../../../tools/beats/requirements.txt", import.meta.url));
15
+ // 요즘 파이썬(Homebrew·데비안)은 시스템에 pip 설치를 막는다(PEP 668) — kf setup 은 쓰임마다 venv 를 따로 둔다
16
+ export const pyVenv = (name: string): string => join(homedir(), ".cache", "kasaframe", "py", name);
17
+ export const venvPython = (dir: string): string => join(dir, platform() === "win32" ? "Scripts/python.exe" : "bin/python");
18
+ export const beatsVenv = pyVenv("beats");
19
+
20
+ export function python(): string {
21
+ return process.env.KF_PYTHON ?? (existsSync(venvPython(beatsVenv)) ? venvPython(beatsVenv) : "python3");
22
+ }
23
+
24
+ // npx playwright 는 다른 판의 playwright 를 집을 수 있다 — 크롬 판이 이 패키지가 쓰는 playwright 와 맞아야 뜬다
25
+ export const playwrightCli = (): string => join(dirname(createRequire(import.meta.url).resolve("playwright/package.json")), "cli.js");
26
+
27
+ export interface Check {
28
+ name: string;
29
+ ok: boolean;
30
+ /** 없으면 렌더가 안 된다 */
31
+ required: boolean;
32
+ detail: string;
33
+ fix?: string;
34
+ }
35
+
36
+ const run = (cmd: string, args: string[]) => spawnSync(cmd, args, { encoding: "utf8" });
37
+
38
+ function installHint(pkg: { brew: string; apt: string; url: string }): string {
39
+ if (platform() === "darwin") return `brew install ${pkg.brew}`;
40
+ if (platform() === "linux") return `sudo apt-get install -y ${pkg.apt} (데비안·우분투 — 다른 배포판은 그 패키지 관리자로)`;
41
+ return pkg.url;
42
+ }
43
+
44
+ function node(): Check {
45
+ const [major, minor] = process.versions.node.split(".").map(Number);
46
+ const ok = major > 22 || (major === 22 && minor >= 15);
47
+ return { name: "Node 22.15 이상", ok, required: true, detail: process.version, fix: ok ? undefined : "https://nodejs.org 에서 22 LTS 이상" };
48
+ }
49
+
50
+ function ffmpeg(): Check {
51
+ const r = run("ffmpeg", ["-hide_banner", "-version"]);
52
+ const p = run("ffprobe", ["-hide_banner", "-version"]);
53
+ const ok = r.status === 0 && p.status === 0;
54
+ return {
55
+ name: "ffmpeg·ffprobe",
56
+ ok,
57
+ required: true,
58
+ detail: ok ? (/version (\S+)/.exec(r.stdout)?.[1] ?? "있음") : "없음 — MP4 로 굽기·검사에 쓴다",
59
+ fix: ok ? undefined : `${installHint({ brew: "ffmpeg", apt: "ffmpeg", url: "https://ffmpeg.org/download.html" })} — PATH 밖 정적 빌드면 KF_FFMPEG=<ffmpeg 경로> KF_FFPROBE=<ffprobe 경로>`,
60
+ };
61
+ }
62
+
63
+ async function chrome(): Promise<Check> {
64
+ try {
65
+ const b = await launch();
66
+ const v = b.version();
67
+ await b.close();
68
+ return { name: "크롬(Playwright)", ok: true, required: true, detail: v };
69
+ } catch (err) {
70
+ const first = String(err instanceof Error ? err.message : err).split("\n")[0];
71
+ return { name: "크롬(Playwright)", ok: false, required: true, detail: `못 띄웠다 — ${first.slice(0, 120)}`, fix: `kf setup chrome (같은 일: node ${playwrightCli()} install chromium${platform() === "linux" ? " · 리눅스는 sudo 로 install-deps chromium 도" : ""})` };
72
+ }
73
+ }
74
+
75
+ export function librosa(): Check {
76
+ const py = python();
77
+ const r = run(py, ["-c", "import librosa; print(librosa.__version__)"]);
78
+ const ok = r.status === 0;
79
+ return {
80
+ name: "python3 + librosa",
81
+ ok,
82
+ required: false,
83
+ detail: ok ? `${py} · librosa ${r.stdout.trim()}` : `${py} 에 없다 — 곡에 박자를 맞출 때(kf beats·--music)만 쓴다`,
84
+ fix: ok ? undefined : `kf setup beats (같은 일: python3 -m venv ${beatsVenv} && ${venvPython(beatsVenv)} -m pip install -r ${requirements})`,
85
+ };
86
+ }
87
+
88
+ function ogKey(): Check {
89
+ const ok = hasKey("opengateway");
90
+ return {
91
+ name: "OG 키",
92
+ ok,
93
+ required: false,
94
+ detail: ok ? "있음 — 그림 생성(kf gen)·kf make OG 모드" : "없음 — 그림 생성(kf gen)과 kf make OG 모드만 못 쓴다. 장면·렌더·검사는 키 없이 돈다",
95
+ fix: ok ? undefined : whereToPut("opengateway"),
96
+ };
97
+ }
98
+
99
+ // 글꼴은 CDN(jsdelivr)에서 받지만, 망이 막힌 리눅스 상자에서는 한글이 두부로 나온다
100
+ function koreanFont(): Check | null {
101
+ if (platform() !== "linux") return null;
102
+ const r = run("fc-list", [":lang=ko", "family"]);
103
+ const ok = r.status === 0 && r.stdout.trim().length > 0;
104
+ return { name: "한글 글꼴", ok, required: false, detail: ok ? r.stdout.trim().split("\n")[0] : "없음 — 글꼴 CDN 이 막히면 한글이 두부로 나온다", fix: ok ? undefined : "sudo apt-get install -y fonts-noto-cjk" };
105
+ }
106
+
107
+ // 점검을 더할 땐 여기 한 줄 — kf doctor 와 kf setup 끝 표가 같이 쓴다
108
+ export async function checks(): Promise<Check[]> {
109
+ return [node(), ffmpeg(), await chrome(), librosa(), jizuraCheck(), lyricsCheck(), ogKey(), koreanFont()].filter((c): c is Check => c !== null);
110
+ }
111
+
112
+ // 한글은 터미널에서 두 칸을 먹는다 — padEnd 로는 열이 어긋난다
113
+ export const cols = (s: string) => [...s].reduce((n, ch) => n + (/[\u1100-\u11ff\u3000-\u9fff\uac00-\ud7af]/.test(ch) ? 2 : 1), 0);
114
+ export const pad = (s: string, n: number) => s + " ".repeat(Math.max(0, n - cols(s)));
115
+
116
+ export function report(list: Check[]): void {
117
+ const w = Math.max(...list.map((c) => cols(c.name))) + 2;
118
+ for (const c of list) {
119
+ console.log(` ${pad(c.ok ? "ok" : c.required ? "없음" : "선택", 4)} ${pad(c.name, w)}${c.detail}`);
120
+ if (c.fix) console.log(` ${pad("", 4 + 2 + w)}→ ${c.fix}`);
121
+ }
122
+ }
@@ -0,0 +1,81 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { once } from "node:events";
3
+
4
+ export type VideoFormat = "h264" | "prores";
5
+
6
+ export interface Encoder {
7
+ /** PNG, raw 로 만들었으면 RGBA 날것 */
8
+ write(frame: Buffer): Promise<void>;
9
+ end(): Promise<void>;
10
+ }
11
+
12
+ function run(args: string[], label: string): { proc: ChildProcess; done: Promise<void> } {
13
+ const proc = spawn("ffmpeg", ["-hide_banner", "-loglevel", "error", "-y", ...args], {
14
+ stdio: ["pipe", "ignore", "pipe"],
15
+ });
16
+ let err = "";
17
+ proc.stderr!.on("data", (d) => (err += d));
18
+ const done = new Promise<void>((resolve, reject) => {
19
+ proc.on("error", (e) => reject(new Error(`${label}: ffmpeg 를 못 띄웠다 (${e.message})`)));
20
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${label}: ffmpeg 종료 ${code}\n${err.trim()}`))));
21
+ });
22
+ return { proc, done };
23
+ }
24
+
25
+ // RGB→YUV 를 BT.709 로 바꾸고 그렇다고 적는다. 안 적으면 ffmpeg 는 BT.601 로 바꾸는데 브라우저·유튜브는 HD 를 709 로 풀어
26
+ // 색이 어긋난다(#20C040 이 (20,168,61) 로 — 초록이 눌린다)
27
+ const BT709 = ["-vf", "scale=out_color_matrix=bt709:out_range=tv,setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"];
28
+
29
+ const CODEC: Record<VideoFormat, string[]> = {
30
+ h264: ["-c:v", "libx264", "-preset", "medium", "-crf", "16", "-pix_fmt", "yuv420p", ...BT709, "-movflags", "+faststart"],
31
+ prores: ["-c:v", "prores_ks", "-profile:v", "3", "-pix_fmt", "yuv422p10le", ...BT709, "-vendor", "apl0"],
32
+ };
33
+
34
+ // PNG(또는 raw 를 주면 RGBA 날것 — 모션 블러가 평균한 프레임)를 stdin 으로 흘려 구간 파일 하나를 만든다. 쓰기가 밀리면 기다려 메모리가 차지 않게 한다.
35
+ export function encoder(out: string, fps: number, format: VideoFormat, crf?: number, raw?: { width: number; height: number }): Encoder {
36
+ const codec = [...CODEC[format]];
37
+ if (crf !== undefined && format === "h264") codec[codec.indexOf("-crf") + 1] = String(crf);
38
+ const input = raw
39
+ ? ["-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${raw.width}x${raw.height}`, "-framerate", String(fps), "-i", "-"]
40
+ : ["-f", "image2pipe", "-framerate", String(fps), "-c:v", "png", "-i", "-"];
41
+ const { proc, done } = run([...input, ...codec, "-r", String(fps), out], out);
42
+ const stdin = proc.stdin!;
43
+ let failed: Error | null = null;
44
+ done.catch((e) => (failed = e));
45
+ return {
46
+ async write(frame) {
47
+ if (failed) throw failed;
48
+ if (!stdin.write(frame)) await Promise.race([once(stdin, "drain"), done]);
49
+ },
50
+ async end() {
51
+ stdin.end();
52
+ await done;
53
+ },
54
+ };
55
+ }
56
+
57
+ // 구간들을 다시 인코딩하지 않고 잇고, 곡이 있으면 그 구간만큼 잘라 얹는다.
58
+ export async function concat(o: {
59
+ list: string;
60
+ out: string;
61
+ format: VideoFormat;
62
+ music?: string;
63
+ from: number;
64
+ dur: number;
65
+ }): Promise<void> {
66
+ const args = ["-f", "concat", "-safe", "0", "-i", o.list];
67
+ if (o.music) args.push("-ss", o.from.toFixed(6), "-i", o.music);
68
+ args.push("-map", "0:v:0");
69
+ if (o.music) {
70
+ args.push("-map", "1:a:0", ...(o.format === "prores" ? ["-c:a", "pcm_s16le"] : ["-c:a", "aac", "-b:a", "256k"]));
71
+ args.push("-af", `apad`, "-t", o.dur.toFixed(6));
72
+ }
73
+ args.push("-c:v", "copy");
74
+ // concat 이 구간 파일의 primaries·trc 태그를 떨군다 — 다시 인코딩 없이 비트스트림에 709 를 다시 적는다
75
+ args.push("-bsf:v", o.format === "h264" ? "h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1" : "prores_metadata=color_primaries=bt709:color_trc=bt709:colorspace=bt709");
76
+ args.push("-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709");
77
+ if (o.format === "h264") args.push("-movflags", "+faststart");
78
+ args.push(o.out);
79
+ const { done } = run(args, o.out);
80
+ await done;
81
+ }
@@ -0,0 +1,8 @@
1
+ export type { Command, Flags } from "./command.ts";
2
+ export { parseFlags, num } from "./command.ts";
3
+ export { openProject, repoRoot } from "./serve.ts";
4
+ export type { OpenProject, OpenProjectOptions } from "./serve.ts";
5
+ export { openPage, openSession, launch, Tab } from "./browser.ts";
6
+ export type { OpenPage, PageInfo, PageMeta, Session, SessionOptions } from "./browser.ts";
7
+ export { blurOpts, captureBlurred, rgbaToPng, subTimes } from "./blur.ts";
8
+ export type { BlurOpts } from "./blur.ts";
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { readdirSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { fontsMissing } from "@kasaframe/jizura/install";
5
+ import type { Command } from "./command.ts";
6
+
7
+ const commandsDir = fileURLToPath(new URL("./commands/", import.meta.url));
8
+
9
+ function available(): string[] {
10
+ return readdirSync(commandsDir)
11
+ .filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"))
12
+ .map((f) => f.slice(0, -3))
13
+ .sort();
14
+ }
15
+
16
+ async function load(name: string): Promise<Command> {
17
+ const mod = await import(new URL(`./commands/${name}.ts`, import.meta.url).href);
18
+ return mod.default as Command;
19
+ }
20
+
21
+ async function help(): Promise<void> {
22
+ console.log("kf <명령> [인자]\n");
23
+ for (const name of available()) {
24
+ const cmd = await load(name).catch(() => null);
25
+ console.log(` ${name.padEnd(10)} ${cmd?.summary ?? ""}`);
26
+ }
27
+ console.log("\nkf <명령> --help 로 인자를 본다.");
28
+ }
29
+
30
+ async function main(argv: string[]): Promise<number> {
31
+ const [name, ...rest] = argv;
32
+ if (!name || name === "help" || name === "--help" || name === "-h") {
33
+ await help();
34
+ return 0;
35
+ }
36
+ if (!available().includes(name)) {
37
+ console.error(`kf: 모르는 명령 "${name}". 있는 명령: ${available().join(" ")}`);
38
+ return 2;
39
+ }
40
+ const cmd = await load(name);
41
+ if (rest.includes("--help") || rest.includes("-h")) {
42
+ console.log(`kf ${name} — ${cmd.summary}\n\n${cmd.usage ?? ""}`.trimEnd());
43
+ return 0;
44
+ }
45
+ return (await cmd.run(rest)) ?? 0;
46
+ }
47
+
48
+ // 가사 층 글꼴을 처음 쓰다 못 받았으면(서버가 받는다 — jizura/vite.ts) 다른 글꼴로 구운 결과 대신 고를 길을 찍는다.
49
+ // 명령이 실패했으면 종료 3, 그 글꼴을 쓰는 프레임 없이 끝났으면 그대로
50
+ async function finish(code: number): Promise<number> {
51
+ const miss = fontsMissing();
52
+ if (!miss.length) return code;
53
+ const [{ fontsQuestion }, { questionsText }] = await Promise.all([import("./commands/lyrics.ts"), import("@kasaframe/gen")]);
54
+ console.error(`\n${code ? "" : "이번 구간은 그 글꼴 없이 끝났다 — 그 플랜이 나오는 구간을 구우려면 먼저 고른다\n"}${questionsText([fontsQuestion(miss)])}`);
55
+ return code ? 3 : code;
56
+ }
57
+
58
+ main(process.argv.slice(2)).then(
59
+ async (code) => process.exit(await finish(code)),
60
+ async (err) => {
61
+ console.error(err instanceof Error ? (err.stack ?? err.message) : err);
62
+ process.exit(await finish(1));
63
+ },
64
+ );
@@ -0,0 +1,72 @@
1
+ import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { installed, repoRoot } from "./serve.ts";
5
+
6
+ const LOCK = installed ? join(tmpdir(), "kasaframe-render-lock") : join(repoRoot, ".render-lock");
7
+
8
+ function alive(pid: number): boolean {
9
+ try {
10
+ process.kill(pid, 0);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ function owner(): { pid: number; dir: string; started: string } | null {
18
+ try {
19
+ return JSON.parse(readFileSync(join(LOCK, "owner.json"), "utf8"));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ // 최종 렌더는 레포 전체에서 하나씩 — mkdir 이 원자적이라 먼저 만든 쪽만 이긴다.
26
+ export async function acquireRenderLock(dir: string, waitSec: number): Promise<() => void> {
27
+ const until = Date.now() + waitSec * 1000;
28
+ let told = false;
29
+ for (;;) {
30
+ try {
31
+ mkdirSync(LOCK);
32
+ writeFileSync(join(LOCK, "owner.json"), JSON.stringify({ pid: process.pid, dir, started: new Date().toISOString() }));
33
+ let held = true;
34
+ const release = () => {
35
+ if (!held) return;
36
+ held = false;
37
+ if (owner()?.pid === process.pid) rmSync(LOCK, { recursive: true, force: true });
38
+ };
39
+ process.once("exit", release);
40
+ for (const sig of ["SIGINT", "SIGTERM"] as const) {
41
+ process.once(sig, () => {
42
+ release();
43
+ process.exit(130);
44
+ });
45
+ }
46
+ return release;
47
+ } catch (err) {
48
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
49
+ }
50
+ const o = owner();
51
+ // 남이 mkdir 하고 owner.json 을 쓰기 전 찰나일 수 있으니, 주인 없는 잠금은 10초 지난 것만 치운다.
52
+ const age = (() => {
53
+ try {
54
+ return Date.now() - statSync(LOCK).mtimeMs;
55
+ } catch {
56
+ return 0;
57
+ }
58
+ })();
59
+ if ((o && !alive(o.pid)) || (!o && age > 10_000)) {
60
+ rmSync(LOCK, { recursive: true, force: true });
61
+ continue;
62
+ }
63
+ if (Date.now() > until) {
64
+ throw new Error(`렌더 잠금을 못 얻었다 — ${o ? `pid ${o.pid} 가 ${o.dir} 를 ${o.started} 부터 굽는 중` : LOCK}`);
65
+ }
66
+ if (!told) {
67
+ console.error(`렌더 잠금 기다리는 중${o ? ` (pid ${o.pid}: ${o.dir})` : ""} — 최대 ${waitSec}초`);
68
+ told = true;
69
+ }
70
+ await new Promise((r) => setTimeout(r, 2000));
71
+ }
72
+ }