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,179 @@
1
+ import { detect, type Detection } from "./detect.ts";
2
+ import { layoutScenes, pickDuration } from "./layout.ts";
3
+ import { parseAspect, parseDuration, parseStyle, parseTitle } from "./request.ts";
4
+ import { frameFor, listTemplates, loadTemplate } from "./templates.ts";
5
+ import { loadSeries, type Series, seriesFromRequest } from "./series.ts";
6
+ import { pickTheme, suggestThemes, themeFromRequest } from "./themes.ts";
7
+ import { basename } from "node:path";
8
+ import type { Aspect, BeatsLike, GenSpec, Plan, PlannedScene, Purpose, ShotSpec, Template } from "./types.ts";
9
+
10
+ const IMAGE = /\.(png|jpe?g|webp)$/i;
11
+
12
+ // 이음매 기본은 모션그래픽 전환이다. 영상 모델은 요청에 분명히 적었을 때만 부른다(돈·시간이 크고 결이 들쭉날쭉하다)
13
+ export function wantsVideo(request: string): boolean {
14
+ return /영상\s*생성|생성\s*영상|AI\s*영상|영상\s*모델|video\s*(gen|model)|generated\s*video/i.test(request);
15
+ }
16
+
17
+ // 캐릭터가 실제로 움직여야 하는 컷 — 모션그래픽·3D 툰으로는 몸짓이 안 서서 영상 모델이 맡는다
18
+ const CHARACTER = /캐릭터|마스코트|인물|사람|소녀|소년|아이돌|댄서|동물|고양이|강아지|새|로봇|주인공|character|mascot|person|girl|boy|dancer|animal|cat|dog|robot/i;
19
+ const ACTION = /움직|춤|걷|달리|뛰|날아|날갯|헤엄|싸우|연기|몸짓|손짓|표정|돌아보|dance|walk|run|jump|fly|swim|wave|move|gesture/i;
20
+ export function wantsMotionCut(request: string): boolean {
21
+ return CHARACTER.test(request) && ACTION.test(request);
22
+ }
23
+
24
+ // 「2D 로」「3D 없이」라고 하면 주인공 3D 샷을 넣지 않는다
25
+ export function refuses3d(request: string): boolean {
26
+ return /3D\s*(없이|빼|말고)|2D\s*(로|만)|평면(으로|만)|no\s*3d|flat\s*only/i.test(request);
27
+ }
28
+
29
+ // 틀의 자리 이름은 장면 id 먼저, 없으면 그 역할의 첫 장면(뮤비처럼 곡 구간으로 id 가 바뀌는 틀)
30
+ function slot<T extends { id: string; role: string }>(scenes: T[], name?: string): T | undefined {
31
+ if (!name) return undefined;
32
+ return scenes.find((s) => s.id === name) ?? scenes.find((s) => s.role === name);
33
+ }
34
+
35
+ export interface PlanInput {
36
+ request: string;
37
+ purpose?: Purpose; // 사람이 못 박으면 판별을 건너뛴다
38
+ assets?: string[];
39
+ music?: string | null;
40
+ beats?: BeatsLike | null;
41
+ duration?: number;
42
+ aspect?: Aspect;
43
+ style?: string;
44
+ title?: string;
45
+ theme?: string; // 테마 id · "auto" · "none"(프리셋만)
46
+ series?: string; // 시리즈 id · "none". 안 주면 요청 글이 짚은 시리즈(series/<id>.json 의 match)
47
+ }
48
+
49
+ export function makePlan(input: PlanInput): Plan {
50
+ const templates = listTemplates();
51
+ // 시리즈가 먼저 — 짚었으면 용도·테마·길이·자막·끝 카드를 그 묶음이 정한다(사람이 따로 준 값은 그대로 이긴다)
52
+ const series: Series | null = input.series === "none" ? null : input.series ? loadSeries(input.series) : seriesFromRequest(input.request);
53
+ const det: Detection = input.purpose
54
+ ? { purpose: input.purpose, confidence: 1, ranked: [], ambiguous: false }
55
+ : series
56
+ ? { purpose: series.purpose, confidence: 1, ranked: [], ambiguous: false }
57
+ : detect({ request: input.request, assets: input.assets, music: input.music }, templates);
58
+ const base = loadTemplate(det.purpose);
59
+ const tpl: Template = series?.duration ? { ...base, duration: { ...base.duration, ...series.duration } } : base;
60
+ const aspect = input.aspect ?? parseAspect(input.request);
61
+ const frame = frameFor(tpl, aspect && (aspect === tpl.aspect || tpl.variants?.[aspect]) ? aspect : undefined);
62
+ const { duration, notes } = pickDuration(tpl, { requested: input.duration ?? parseDuration(input.request), beats: input.beats });
63
+ const laid = layoutScenes(tpl, duration, input.beats);
64
+ // auto: 요청이 테마를 말로 짚었으면 그 테마, 아니면 내용에 맞는 approved 후보 셋 중 첫째
65
+ // none 은 프리셋만 — 시리즈 테마도 쓰지 않는다
66
+ const themeIn = input.theme === "none" ? undefined : (input.theme === undefined || input.theme === "auto") && series?.theme ? series.theme : input.theme;
67
+ const named = themeIn === "auto" ? themeFromRequest(input.request, tpl.id) : null;
68
+ const themeCandidates = themeIn === "auto" ? [...(named ? [named] : []), ...suggestThemes(input.request, tpl.id, frame.aspect).filter((c) => c.id !== named?.id)].slice(0, 3) : undefined;
69
+ const theme = themeIn === "auto" ? (themeCandidates?.[0]?.id ?? null) : themeIn ? pickTheme(themeIn).id : null;
70
+ // 테마를 고르면 그 테마의 style 이 프리셋을 대신한다(kf new 가 themeStyle 로 쓴다)
71
+ const style = theme ?? input.style ?? series?.style ?? parseStyle(input.request) ?? tpl.style;
72
+ const title = input.title ?? series?.end?.title ?? parseTitle(input.request) ?? "";
73
+ const gen = genSpecs(tpl, laid.scenes, {
74
+ subject: title || input.request,
75
+ style,
76
+ width: frame.width,
77
+ height: frame.height,
78
+ video: wantsVideo(input.request),
79
+ act: tpl.id === "mv" || wantsMotionCut(input.request),
80
+ refs: (input.assets ?? []).filter((a) => IMAGE.test(a)).map((a) => `assets/in/${basename(a)}`),
81
+ });
82
+ const hero = refuses3d(input.request) ? undefined : slot(laid.scenes, tpl.assets.shot3d);
83
+ const shots: ShotSpec[] = hero
84
+ ? [{ id: `${hero.id}-3d`, scene: hero.id, tool: "blender", idea: `${title || input.request} — 「${hero.idea}」을 주인공 3D 장면으로(툰 셰이딩·라인아트, 카메라 열쇠는 첫박·드롭에)`, file: `shots/${hero.id}-3d.py` }]
85
+ : [];
86
+
87
+ const scenes: PlannedScene[] = laid.scenes.map((s) => ({
88
+ id: s.id,
89
+ role: s.role,
90
+ from: s.from,
91
+ dur: s.dur,
92
+ idea: s.idea,
93
+ assets: gen.filter((g) => g.scene === s.id).map((g) => `gen:${g.kind}:${g.id}`),
94
+ }));
95
+
96
+ const top = det.ranked[0];
97
+ const plan: Plan = {
98
+ request: input.request,
99
+ purpose: det.purpose,
100
+ confidence: det.confidence,
101
+ alternatives: det.ranked.slice(1, 3).map((r) => ({ purpose: r.purpose, p: r.p })),
102
+ reasons: [...(series ? [`시리즈 ${series.id}(${series.label})`] : []), ...(top?.reasons ?? (input.purpose ? ["사람이 정함"] : [])), ...notes, ...laid.notes],
103
+ title,
104
+ aspect: frame.aspect,
105
+ width: frame.width,
106
+ height: frame.height,
107
+ fps: tpl.fps,
108
+ duration: laid.duration,
109
+ style,
110
+ music: input.music ?? null,
111
+ captions: series?.captions ?? tpl.captions,
112
+ deliverables: tpl.deliverables,
113
+ scenes,
114
+ gen,
115
+ shots,
116
+ theme,
117
+ ...(themeCandidates ? { themeCandidates } : {}),
118
+ ...(series ? { series: series.id, ...(series.music?.mood ? { mood: series.music.mood } : {}) } : {}),
119
+ };
120
+ if (det.ambiguous) {
121
+ plan.questions = [
122
+ {
123
+ id: "purpose",
124
+ ask: "어떤 용도의 영상인가?",
125
+ choices: det.ranked.slice(0, 4).map((r) => `${r.purpose} — ${templates.find((t) => t.id === r.purpose)?.label}`),
126
+ },
127
+ ];
128
+ }
129
+ if (tpl.music === "required" && !input.music) {
130
+ plan.questions = [...(plan.questions ?? []), { id: "music", ask: `${tpl.label} 영상은 곡이 끌고 간다. 쓸 곡 파일은?`, choices: [] }];
131
+ }
132
+ return plan;
133
+ }
134
+
135
+ // "img:hook-bg" → hook 장면 바탕 그림, "vid:seam" → seam 장면 전체를 채우는 이음매 클립(앞 장면 끝 프레임 → 뒤 장면 첫 프레임, 계약 8)
136
+ function genSpecs(
137
+ tpl: Template,
138
+ scenes: { id: string; role: string; idea: string }[],
139
+ o: { subject: string; style: string; width: number; height: number; video: boolean; act: boolean; refs: string[] },
140
+ ): GenSpec[] {
141
+ const gen: GenSpec[] = [];
142
+ for (const spec of [...tpl.assets.gen, ...(o.video ? (tpl.assets.genVideo ?? []) : [])]) {
143
+ const [kind, id] = spec.split(":") as ["img" | "vid", string];
144
+ if (kind === "vid") {
145
+ const i = scenes.findIndex((s) => s.id === id);
146
+ if (i <= 0 || i === scenes.length - 1) continue;
147
+ gen.push({
148
+ id,
149
+ kind,
150
+ scene: id,
151
+ seam: true,
152
+ prompt: `${o.subject} — 「${scenes[i - 1].idea}」 화면에서 「${scenes[i + 1].idea}」 화면으로 이음매 없이 빨려 들어가는 카메라 이동. ${o.style} 결, 글자 없음.`,
153
+ });
154
+ continue;
155
+ }
156
+ // 뮤비 주인공은 받은 그림을 참조해 한 장 만들고, 나머지 그림은 그 주인공을 참조해 같은 얼굴로 간다
157
+ if (id === "character") {
158
+ gen.push({ id, kind, scene: scenes[0].id, prompt: `${o.subject} 뮤직비디오 주인공 캐릭터 — 정면 전신 한 명, 단색 바탕, 글자 없음. ${o.style} 결.`, size: `${o.width}x${o.height}`, ...(o.refs.length ? { refs: o.refs } : {}) });
159
+ continue;
160
+ }
161
+ const scene = scenes.find((s) => id === s.id || id.startsWith(`${s.id}-`)) ?? scenes[0];
162
+ const refs = gen.some((g) => g.id === "character") ? { refs: ["assets/gen/character.png"] } : {};
163
+ gen.push({ id, kind, scene: scene.id, prompt: `${o.subject} ${tpl.label} 영상 「${scene.idea}」 장면의 바탕 그림. ${o.style} 결, 글자·로고 없음.`, size: `${o.width}x${o.height}`, ...refs });
164
+ }
165
+ const act = o.act ? slot(scenes, tpl.assets.act) : undefined;
166
+ if (act) {
167
+ // 주인공 그림이 있으면 그 얼굴로 시작한다(첫 프레임) — 그림은 영상보다 먼저 만든다(resolvePlan 순서)
168
+ const face = gen.find((g) => g.id === "character") ?? gen.find((g) => g.kind === "img" && g.scene === act.id);
169
+ gen.push({
170
+ id: `${act.id}-act`,
171
+ kind: "vid",
172
+ scene: act.id,
173
+ dur: 5,
174
+ prompt: `${o.subject} — 「${act.idea}」 장면에서 캐릭터가 실제로 움직이는 컷(몸짓·걸음·표정). 카메라는 거의 고정, ${o.style} 결, 글자 없음.`,
175
+ ...(face ? { first: `assets/gen/${face.id}.png` } : {}),
176
+ });
177
+ }
178
+ return gen;
179
+ }
@@ -0,0 +1,55 @@
1
+ import type { Aspect } from "./types.ts";
2
+
3
+ // 요청 글에서 사람이 못 박은 값만 뽑는다. 못 찾으면 undefined — 틀 기본값이 채운다.
4
+
5
+ export function parseDuration(text: string): number | undefined {
6
+ const t = text.toLowerCase();
7
+ const min = t.match(/(\d+(?:\.\d+)?)\s*(?:분|min(?:ute)?s?\b)/);
8
+ const sec = t.match(/(\d+(?:\.\d+)?)\s*(?:초|s\b|sec(?:ond)?s?\b)/);
9
+ if (!min && !sec) {
10
+ if (/한\s*분|일\s*분/.test(t)) return 60;
11
+ if (/반\s*분/.test(t)) return 30;
12
+ return undefined;
13
+ }
14
+ return (min ? Number(min[1]) * 60 : 0) + (sec ? Number(sec[1]) : 0);
15
+ }
16
+
17
+ export function parseAspect(text: string): Aspect | undefined {
18
+ const t = text.toLowerCase();
19
+ if (/9\s*[:x×]\s*16|세로|vertical|portrait/.test(t)) return "9:16";
20
+ if (/1\s*[:x×]\s*1\b|정사각|square/.test(t)) return "1:1";
21
+ if (/16\s*[:x×]\s*9|가로|landscape|widescreen/.test(t)) return "16:9";
22
+ return undefined;
23
+ }
24
+
25
+ const STYLE_HINTS: [RegExp, string][] = [
26
+ [/카와이|kawaii|파스텔|pastel|귀여운|큐트|cute/i, "kawaii-pastel"],
27
+ [/카사텀|kasaterm/i, "kasaterm"],
28
+ [/\bog\b|오픈게이트웨이|opengateway|open gateway/i, "og"],
29
+ [/다크|어두운|dark|시네마틱|cinematic/i, "neutral-dark"],
30
+ ];
31
+
32
+ export function parseStyle(text: string): string | undefined {
33
+ return STYLE_HINTS.find(([re]) => re.test(text))?.[1];
34
+ }
35
+
36
+ const SUBJECTS: [RegExp, string][] = [
37
+ [/카사텀|kasaterm/i, "kasaterm"],
38
+ [/\bog\b|오픈게이트웨이|opengateway|open gateway/i, "OpenGateway"],
39
+ ];
40
+
41
+ // 아는 제품만 잡는다. 모르면 비워 두고 kf new(폴더 이름)·kf make(LLM)가 채운다 — 첫 낱말을 이름으로 찍으면 "15초" 같은 게 끝 카드에 뜬다
42
+ export function parseTitle(text: string): string | undefined {
43
+ return SUBJECTS.find(([re]) => re.test(text))?.[1];
44
+ }
45
+
46
+ const ASCII_WORD = /^[a-z0-9 .:-]+$/i;
47
+
48
+ // 영어 짧은 낱말("ad")이 "made" 같은 데 걸리지 않게 영문은 낱말 경계로, 한글은 그대로 찾는다
49
+ export function mentions(text: string, word: string): boolean {
50
+ const t = text.toLowerCase();
51
+ const w = word.toLowerCase();
52
+ if (!ASCII_WORD.test(w)) return t.includes(w);
53
+ const esc = w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
54
+ return new RegExp(`(^|[^a-z0-9])${esc}([^a-z0-9]|$)`).test(t);
55
+ }
@@ -0,0 +1,76 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { PURPOSES, type Purpose } from "./types.ts";
5
+
6
+ // 시리즈 묶음 — 같은 시리즈면 생김새·목소리·곡·자막·길이·끝 카드·생성 답이 매번 같다. 요청이 시리즈를 짚으면 묻지 않고 이 묶음을 쓴다
7
+ export interface Series {
8
+ id: string;
9
+ label: string;
10
+ summary?: string;
11
+ match: string[]; // 요청 글에 이 말이 있으면 이 시리즈(대소문자·띄어쓰기 무시)
12
+ purpose: Purpose;
13
+ theme?: string;
14
+ style?: string; // 테마 없이 프리셋으로 갈 때
15
+ voice?: string; // "<엔진>:<목소리>" · "none"
16
+ music?: { mood?: string; file?: string }; // file 은 레포 기준 곡 — 있으면 늘 그 곡, 없으면 분위기로 곡 모음에서(같은 분위기면 같은 곡)
17
+ captions?: "on" | "off" | "none";
18
+ duration?: { min?: number; max?: number; default?: number };
19
+ end?: { title?: string; line?: string }; // 끝 카드
20
+ answers?: Record<string, string>; // 생성 질문에 미리 박은 답(image·video·blender·voice·music) — 묻지 않는다
21
+ }
22
+
23
+ export const seriesDir = (): string => process.env.KF_SERIES_DIR || fileURLToPath(new URL("../../../series/", import.meta.url));
24
+
25
+ export function checkSeries(s: Partial<Series>): string[] {
26
+ const bad: string[] = [];
27
+ if (!s.id || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) bad.push("id 는 소문자·숫자·- 로");
28
+ if (!s.label) bad.push("label 이 없다");
29
+ if (!s.match?.length) bad.push("match 가 비었다 — 요청에서 짚을 말을 하나 이상");
30
+ if (!s.purpose || !PURPOSES.includes(s.purpose)) bad.push(`purpose 는 ${PURPOSES.join("·")} 중 하나`);
31
+ if (s.captions && !["on", "off", "none"].includes(s.captions)) bad.push("captions 는 on·off·none");
32
+ return bad;
33
+ }
34
+
35
+ export function listSeries(): Series[] {
36
+ const dir = seriesDir();
37
+ if (!existsSync(dir)) return [];
38
+ return readdirSync(dir)
39
+ .filter((f) => f.endsWith(".json"))
40
+ .map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")) as Series)
41
+ .filter((s) => !checkSeries(s).length)
42
+ .sort((a, b) => a.id.localeCompare(b.id));
43
+ }
44
+
45
+ export function loadSeries(id: string): Series {
46
+ const file = join(seriesDir(), `${id}.json`);
47
+ if (!existsSync(file)) throw new Error(`시리즈 없음: ${id} (있는 것: ${listSeries().map((s) => s.id).join(" ") || "없음"})`);
48
+ const s = JSON.parse(readFileSync(file, "utf8")) as Series;
49
+ const bad = checkSeries(s);
50
+ if (bad.length) throw new Error(`시리즈 ${id} 가 깨졌다 — ${bad.join(" · ")}`);
51
+ return s;
52
+ }
53
+
54
+ const squash = (x: string) => x.toLowerCase().replace(/\s+/g, "");
55
+
56
+ // 가장 길게 맞은 말의 시리즈 — 「OG 쇼츠」와 「OG 쇼츠 영어판」이 같이 있으면 긴 쪽
57
+ export function seriesFromRequest(request: string): Series | null {
58
+ const r = squash(request);
59
+ let best: { s: Series; n: number } | null = null;
60
+ for (const s of listSeries()) {
61
+ for (const m of s.match) {
62
+ const k = squash(m);
63
+ if (k && r.includes(k) && (!best || k.length > best.n)) best = { s, n: k.length };
64
+ }
65
+ }
66
+ return best?.s ?? null;
67
+ }
68
+
69
+ export function writeSeries(s: Series): string {
70
+ const bad = checkSeries(s);
71
+ if (bad.length) throw new Error(bad.join(" · "));
72
+ mkdirSync(seriesDir(), { recursive: true });
73
+ const file = join(seriesDir(), `${s.id}.json`);
74
+ writeFileSync(file, `${JSON.stringify(s, null, 2)}\n`);
75
+ return file;
76
+ }
@@ -0,0 +1,53 @@
1
+ // 말로 시작한 kf make 도 작업실이 떠 있으면 거기에 등록한다 — 페이지 「지난 작업」·실시간 미리보기로 보이게. 서버가 없으면 조용히 넘어간다
2
+ const base = () => (process.env.KF_APP_URL ?? "http://127.0.0.1:5150").replace(/\/$/, "");
3
+
4
+ async function post<T>(path: string, body: unknown): Promise<T | null> {
5
+ try {
6
+ const r = await fetch(`${base()}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(1500) });
7
+ return r.ok ? ((await r.json()) as T) : null;
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+
13
+ export interface StudioLink {
14
+ id: string;
15
+ url: string;
16
+ send(p: { lines?: string[]; phase?: string; done?: boolean; ok?: boolean }): Promise<void>;
17
+ // console.error 로 찍는 진행 줄을 작업실로도 흘린다(모아서 0.5초마다)
18
+ mirror(): () => Promise<void>;
19
+ }
20
+
21
+ export async function attachStudio(o: { dir: string; request: string; model?: string; purpose?: string }): Promise<StudioLink | null> {
22
+ const r = await post<{ id: string; url: string }>("/api/jobs/attach", o);
23
+ if (!r) return null;
24
+ const send = async (p: Parameters<StudioLink["send"]>[0]) => {
25
+ await post(`/api/jobs/${r.id}/progress`, p);
26
+ };
27
+ return {
28
+ ...r,
29
+ send,
30
+ mirror() {
31
+ const orig = console.error;
32
+ let queue: string[] = [];
33
+ let chain = Promise.resolve();
34
+ const flush = () => {
35
+ if (!queue.length) return chain;
36
+ const lines = queue;
37
+ queue = [];
38
+ chain = chain.then(() => send({ lines }));
39
+ return chain;
40
+ };
41
+ const timer = setInterval(flush, 500);
42
+ console.error = (...args: unknown[]) => {
43
+ orig(...args);
44
+ queue.push(...args.map(String).join(" ").split("\n"));
45
+ };
46
+ return async () => {
47
+ clearInterval(timer);
48
+ console.error = orig;
49
+ await flush();
50
+ };
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,42 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { type Aspect, PURPOSES, type Purpose, type SafeArea, type Template } from "./types.ts";
5
+
6
+ export const templatesDir = fileURLToPath(new URL("../../../templates/", import.meta.url));
7
+
8
+ export function listTemplates(): Template[] {
9
+ return readdirSync(templatesDir)
10
+ .filter((d) => existsSync(join(templatesDir, d, "template.json")))
11
+ .map((d) => loadTemplate(d))
12
+ .sort((a, b) => PURPOSES.indexOf(a.id) - PURPOSES.indexOf(b.id));
13
+ }
14
+
15
+ export function loadTemplate(id: string): Template {
16
+ const file = join(templatesDir, id, "template.json");
17
+ if (!existsSync(file)) {
18
+ const have = readdirSync(templatesDir).filter((d) => existsSync(join(templatesDir, d, "template.json")));
19
+ throw new Error(`틀 "${id}" 가 없다. 있는 틀: ${have.join(" ")}`);
20
+ }
21
+ return JSON.parse(readFileSync(file, "utf8")) as Template;
22
+ }
23
+
24
+ export function frameFor(tpl: Template, aspect?: Aspect): { aspect: Aspect; width: number; height: number; safeArea: SafeArea } {
25
+ if (!aspect || aspect === tpl.aspect) return { aspect: tpl.aspect, width: tpl.width, height: tpl.height, safeArea: tpl.safeArea };
26
+ const v = tpl.variants?.[aspect];
27
+ if (!v) throw new Error(`틀 ${tpl.id} 는 ${aspect} 를 받지 않는다(${[tpl.aspect, ...Object.keys(tpl.variants ?? {})].join("·")})`);
28
+ return { aspect, width: v.width, height: v.height, safeArea: v.safeArea ?? tpl.safeArea };
29
+ }
30
+
31
+ // 틀 고유(장면 id → 틀 기본) → 역할 공용 → 공용 기본. 뮤비처럼 장면 id 가 곡마다 바뀌는 틀은 틀 기본(scenes/default.ts)이 받는다
32
+ export function sceneSkeleton(tpl: Purpose, sceneId: string, role: string): string {
33
+ const tries = [
34
+ join(templatesDir, tpl, "scenes", `${sceneId}.ts`),
35
+ join(templatesDir, tpl, "scenes", "default.ts"),
36
+ join(templatesDir, "_shared", "scenes", `${role}.ts`),
37
+ join(templatesDir, "_shared", "scenes", "default.ts"),
38
+ ];
39
+ const hit = tries.find((p) => existsSync(p));
40
+ if (!hit) throw new Error(`장면 뼈대가 없다: ${tries.join(" · ")}`);
41
+ return readFileSync(hit, "utf8");
42
+ }
@@ -0,0 +1,48 @@
1
+ import { listThemes, loadTheme, type Theme } from "@kasaframe/themes";
2
+ import { parseTitle } from "./request.ts";
3
+ import type { Aspect, Purpose } from "./types.ts";
4
+
5
+ export interface ThemePick {
6
+ id: string;
7
+ why: string;
8
+ }
9
+
10
+ const words = (s: string) => s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length >= 2);
11
+
12
+ // 자동 추천은 approved 테마만(SPEC 9). 요청 낱말이 테마 이름·비유·THEME.md 에 얼마나 나오나로 줄 세우고, 같으면 최근에 승인된 쪽
13
+ export function suggestThemes(request: string, purpose: Purpose, aspect?: Aspect, n = 3): ThemePick[] {
14
+ const pool = listThemes({ status: "approved", purpose, ...(aspect ? { aspect } : {}) });
15
+ const ask = new Set(words(request));
16
+ const scored = pool.map((t) => {
17
+ const hay = `${t.name} ${t.metaphor} ${t.guide}`.toLowerCase();
18
+ const hit = [...ask].filter((w) => hay.includes(w));
19
+ return { t, score: hit.length, hit };
20
+ });
21
+ scored.sort((a, b) => b.score - a.score || (b.t.approvedAt ?? "").localeCompare(a.t.approvedAt ?? ""));
22
+ return scored.slice(0, n).map(({ t, hit }) => ({ id: t.id, why: hit.length ? `요청의 ${hit.slice(0, 3).map((w) => `「${w}」`).join("·")} 이(가) 테마 비유와 맞는다 — ${t.metaphor}` : `${purpose} 에 쓰는 승인 테마 — ${t.metaphor}` }));
23
+ }
24
+
25
+ // 「리소 느낌으로」처럼 사람이 말로 테마를 짚었는지 — 테마 이름 낱말(두 글자 넘게)·id 조각(세 글자 넘게)이 요청에 나오면 그 테마.
26
+ // 사람이 짚은 것이라 review 까지 받는다(draft 는 빼고)
27
+ export function themeFromRequest(request: string, purpose?: Purpose): ThemePick | null {
28
+ const t = request.toLowerCase();
29
+ let best: { th: Theme; score: number; hit: string[] } | null = null;
30
+ for (const th of listThemes({ status: ["approved", "review"] })) {
31
+ const keys = [...th.name.split(/\s+/).filter((w) => w.length >= 2), ...th.id.split("-").filter((w) => w.length >= 3)].map((w) => w.toLowerCase());
32
+ const hit = keys.filter((k) => t.includes(k));
33
+ if (!hit.length) continue;
34
+ // 제품 이름(「카사텀」·「OG」)은 무엇을 찍나일 뿐 결이 아니다 — 「카와이」·「리소」 같은 결 낱말보다 약하게, 「~ 느낌·풍·스타일」로 짚으면 가장 세게.
35
+ // 용도에 안 맞는 테마도 사람이 짚었으면 쓰되, 같은 점수면 용도에 맞는 쪽
36
+ const weight = (k: string) => (parseTitle(k) ? 4 : 10) + (new RegExp(`${k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*(느낌|풍|스타일|결|톤|테마|처럼)`).test(t) ? 20 : 0);
37
+ const score = hit.reduce((a, k) => a + weight(k), 0) + (purpose && th.purposes.includes(purpose) ? 1 : 0);
38
+ if (!best || score > best.score) best = { th, score, hit };
39
+ }
40
+ return best ? { id: best.th.id, why: `요청이 「${best.hit.join("·")}」 로 ${best.th.name} 을(를) 짚었다 — ${best.th.metaphor}` } : null;
41
+ }
42
+
43
+ // 사람이 고른 id 는 review 까지 쓴다. draft 는 테마 폴더 sample/ 안에서만이라 여기선 막는다
44
+ export function pickTheme(id: string): Theme {
45
+ const t = loadTheme(id);
46
+ if (t.status === "draft") throw new Error(`테마 ${id} 는 아직 draft — 견본이 review 로 올라온 뒤에 쓴다`);
47
+ return t;
48
+ }
@@ -0,0 +1,157 @@
1
+ import type { Question } from "@kasaframe/gen";
2
+ import type { Bundle } from "@kasaframe/motion/menu";
3
+ export type Aspect = "16:9" | "9:16" | "1:1";
4
+ export type Purpose = "promo" | "shorts" | "explainer" | "launch" | "demo" | "ad" | "square" | "mv";
5
+ export const PURPOSES: Purpose[] = ["promo", "shorts", "explainer", "launch", "demo", "ad", "square", "mv"];
6
+
7
+ export interface SafeArea { top: number; bottom: number; left: number; right: number }
8
+
9
+ export interface TemplateScene {
10
+ id: string;
11
+ role: string;
12
+ sec?: number;
13
+ weight?: number;
14
+ anchor?: string; // start · end · drop:<k> · drop:last · section:<kind>
15
+ snap?: "bar" | "beat" | "none";
16
+ idea: string;
17
+ transition?: string; // 이음매 장면의 3D 전환 종류(dive·portal·cards·shatter·zoomOut·fold)
18
+ }
19
+
20
+ export interface Template {
21
+ id: Purpose;
22
+ label: string;
23
+ summary: string; // 틀 고르는 화면의 한 줄
24
+ aspect: Aspect;
25
+ width: number;
26
+ height: number;
27
+ fps: number;
28
+ variants?: Partial<Record<Aspect, { width: number; height: number; safeArea?: SafeArea }>>;
29
+ duration: { min: number; max: number; default: number; allowed?: number[] };
30
+ music: "required" | "optional" | "none";
31
+ // 곡 모음 태그·생성 곡 프롬프트를 고르는 분위기(@kasaframe/gen MOOD_TAGS·MOOD_PROMPT) — 같은 틀이면 같은 결의 곡
32
+ mood?: string;
33
+ // 내레이션 먼저 — 장면별 문장을 읽혀 길이를 재고 장면을 목소리에 맞춘다. voices 는 엔진마다 박은 목소리(같은 틀이면 같은 목소리)
34
+ narration?: Narration;
35
+ // 섞기 — 내레이션 아래 곡 musicDb, 말할 때 덕킹, 끝 페이드, 정규화 LUFS
36
+ audio?: AudioMix;
37
+ // off: srt·vtt 만 · on: 화면에도 얹는다 · none: 없음 (옛 이름 track·burned 도 읽는다 — @kasaframe/motion/captions 의 captionMode)
38
+ captions: "off" | "on" | "none";
39
+ loop: boolean;
40
+ style: string;
41
+ safeArea: SafeArea;
42
+ text: { policy: "none" | "terms-only" | "captions" | "lyrics"; maxOnScreen: number; minPx: number };
43
+ // 뮤비: kinetic 이면 부른 낱말이 그 시각에 화면에 뜬다(lyrics.json 낱말 시각), track 이면 자막 트랙만
44
+ lyrics?: { mode: "kinetic" | "track" | "none"; align?: string; lipsync?: string };
45
+ motion: Record<string, unknown>;
46
+ // 장면마다 거는 연출 수 묶음(@kasaframe/motion/menu) — kf new 가 요청 글을 씨앗으로 장면마다 고른다. roles 는 역할별(hook·drop·end …)
47
+ moves?: Bundle;
48
+ // kf render 기본값 — kf new 가 kf.json render 로 옮긴다. motionBlur 하위 프레임 수(0 이면 끔)·shutter 셔터 각도
49
+ render?: { motionBlur?: number; shutter?: number };
50
+ scenes: TemplateScene[];
51
+ // sections: 곡 구간(beats.json sections)마다 장면 하나 — scenes 는 곡이 없을 때 쓰는 기본, sectionIdeas 는 구간 종류별 할 일
52
+ layout?: "fixed" | "sections";
53
+ sectionIdeas?: Record<string, string>;
54
+ // gen 은 늘 만드는 그림, genVideo 는 요청에 「영상 생성」을 분명히 적었을 때만(이음매 기본은 모션그래픽 전환).
55
+ // shot3d 는 주인공 3D 장면(Blender) 자리, act 는 캐릭터가 실제로 움직여야 할 때 영상 모델 컷 자리 — 장면 id, 없으면 역할로 찾는다
56
+ assets: { need: string[]; want: string[]; gen: string[]; genVideo?: string[]; shot3d?: string; act?: string };
57
+ deliverables: string[];
58
+ detect: { words: string[]; hints: string[]; aspect: Aspect; assetKinds?: string[] };
59
+ }
60
+
61
+ // SPEC 4 중 장면 배치에 쓰는 것만
62
+ export interface BeatsLike {
63
+ duration: number;
64
+ beats: number[];
65
+ downbeats?: number[];
66
+ bars?: number[];
67
+ drops?: number[];
68
+ sections?: { t: number; kind: string; strength?: number }[];
69
+ }
70
+
71
+ export interface PlannedScene {
72
+ id: string;
73
+ role: string;
74
+ from: number;
75
+ dur: number;
76
+ idea: string;
77
+ assets: string[];
78
+ }
79
+
80
+ // SPEC 계약 8 — @kasaframe/gen 의 GenSpec 과 같은 꼴. 이음매(seam) 영상의 id 는 kf.json 장면 id 와 같다
81
+ export interface GenSpec {
82
+ id: string;
83
+ kind: "img" | "vid";
84
+ scene?: string;
85
+ prompt: string;
86
+ size?: string;
87
+ transparent?: boolean;
88
+ quality?: string;
89
+ refs?: string[];
90
+ seam?: boolean;
91
+ dur?: number;
92
+ first?: string;
93
+ last?: string;
94
+ provider?: string;
95
+ }
96
+
97
+ // 주인공 3D 장면 — blender 면 shots/<id>.py 를 kf blender 로 굽고, three 면 그 장면이 stage3d 로 3D 를 짓는다(Blender 가 없을 때의 대신)
98
+ export interface ShotSpec {
99
+ id: string;
100
+ scene: string;
101
+ tool: "blender" | "three";
102
+ idea: string;
103
+ file: string; // 프로젝트 기준 샷 스크립트 — blender 일 때만 쓴다
104
+ }
105
+
106
+ export interface Narration {
107
+ on: boolean;
108
+ lang: string;
109
+ style: string;
110
+ // 이 틀의 목소리 하나 — 같은 틀이면 어느 기계에서든 이 목소리. 그 엔진이 없으면 다른 엔진으로 넘어가지 않고 묻는다
111
+ voice: string;
112
+ // 사람이 다른 엔진을 고를 때 그 엔진의 목소리
113
+ voices: Record<string, string>;
114
+ lead: number; // 장면 시작에서 말이 들어오기까지(초)
115
+ tail: number; // 말이 끝나고 장면이 끝나기까지(초)
116
+ }
117
+
118
+ export interface AudioMix {
119
+ musicDb: number;
120
+ duck: boolean;
121
+ lufs: number;
122
+ truePeak: number;
123
+ fadeOut: number;
124
+ }
125
+
126
+ export interface Plan {
127
+ request: string;
128
+ purpose: Purpose;
129
+ confidence: number;
130
+ alternatives: { purpose: Purpose; p: number }[];
131
+ reasons: string[];
132
+ title: string;
133
+ aspect: Aspect;
134
+ width: number;
135
+ height: number;
136
+ fps: number;
137
+ duration: number;
138
+ style: string;
139
+ music: string | null;
140
+ // 목소리 "<엔진>:<목소리>" · "none"(자막만). 정하기 전에는 없다
141
+ voice?: string;
142
+ captions: Template["captions"];
143
+ deliverables: string[];
144
+ scenes: PlannedScene[];
145
+ gen: GenSpec[];
146
+ // 고른 테마(themes/<id>). auto 면 approved 후보 셋을 themeCandidates 에 남기고 첫째를 쓴다
147
+ theme?: string | null;
148
+ themeCandidates?: { id: string; why: string }[];
149
+ questions?: { id: string; ask: string; choices: string[] }[];
150
+ shots?: ShotSpec[];
151
+ // 시리즈 묶음(series/<id>.json)으로 세운 계획이면 그 id, 곡 분위기는 시리즈가 틀 mood 를 덮는다
152
+ series?: string;
153
+ mood?: string;
154
+ // 만들기 전에 사람이 고를 것(@kasaframe/gen Question) — 아직 안 고른 것만. 고른 답은 answers 에 { 질문 id: 값 }
155
+ asks?: Question[];
156
+ answers?: Record<string, string>;
157
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@kasaframe/studio",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "devDependencies": {
10
+ "vite": "^8.3.3"
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export { studioPlugin, studioConfig, type StudioConfig } from "./plugin.ts";