kasaframe 0.0.0-stage → 0.1.1

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 (338) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +146 -0
  3. package/README.md +77 -2
  4. package/app/package.json +12 -0
  5. package/app/server/engine.ts +415 -0
  6. package/app/server/main.ts +205 -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 +1013 -0
  11. package/app/web/style.css +430 -0
  12. package/bin/hooks.mjs +51 -0
  13. package/bin/kf.mjs +14 -0
  14. package/docs/AGENTS.md +238 -0
  15. package/music/README.md +13 -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 +273 -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 +42 -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 +135 -0
  43. package/packages/check/src/rules/layout.ts +257 -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 +29 -0
  52. package/packages/cli/src/blur.ts +68 -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/blender.ts +270 -0
  58. package/packages/cli/src/commands/check.ts +25 -0
  59. package/packages/cli/src/commands/doctor.ts +22 -0
  60. package/packages/cli/src/commands/gen.ts +173 -0
  61. package/packages/cli/src/commands/look.ts +39 -0
  62. package/packages/cli/src/commands/lyrics.ts +111 -0
  63. package/packages/cli/src/commands/make.ts +542 -0
  64. package/packages/cli/src/commands/new.ts +176 -0
  65. package/packages/cli/src/commands/preview.ts +43 -0
  66. package/packages/cli/src/commands/render.ts +216 -0
  67. package/packages/cli/src/commands/setup.ts +63 -0
  68. package/packages/cli/src/commands/sheet.ts +114 -0
  69. package/packages/cli/src/commands/skill.ts +43 -0
  70. package/packages/cli/src/commands/still.ts +40 -0
  71. package/packages/cli/src/commands/theme.ts +85 -0
  72. package/packages/cli/src/commands/transitions.ts +40 -0
  73. package/packages/cli/src/deps.ts +122 -0
  74. package/packages/cli/src/ffmpeg.ts +81 -0
  75. package/packages/cli/src/index.ts +8 -0
  76. package/packages/cli/src/kf.ts +53 -0
  77. package/packages/cli/src/lock.ts +72 -0
  78. package/packages/cli/src/out.ts +13 -0
  79. package/packages/cli/src/serve.ts +124 -0
  80. package/packages/clock/package.json +14 -0
  81. package/packages/clock/src/index.ts +25 -0
  82. package/packages/clock/src/install.ts +351 -0
  83. package/packages/clock/src/types.ts +45 -0
  84. package/packages/core/package.json +9 -0
  85. package/packages/core/src/beats.ts +283 -0
  86. package/packages/core/src/ease.ts +87 -0
  87. package/packages/core/src/frame.ts +99 -0
  88. package/packages/core/src/index.ts +27 -0
  89. package/packages/core/src/project.ts +146 -0
  90. package/packages/core/src/rand.ts +56 -0
  91. package/packages/core/src/spring.ts +52 -0
  92. package/packages/core/src/style.ts +100 -0
  93. package/packages/core/src/time.ts +88 -0
  94. package/packages/gen/PROVIDERS.md +55 -0
  95. package/packages/gen/package.json +9 -0
  96. package/packages/gen/src/generate.ts +204 -0
  97. package/packages/gen/src/image/og.ts +133 -0
  98. package/packages/gen/src/index.ts +18 -0
  99. package/packages/gen/src/keys.ts +60 -0
  100. package/packages/gen/src/media.ts +150 -0
  101. package/packages/gen/src/plan.ts +106 -0
  102. package/packages/gen/src/registry.ts +51 -0
  103. package/packages/gen/src/seam.ts +103 -0
  104. package/packages/gen/src/types.ts +86 -0
  105. package/packages/gen/src/video/fal.ts +140 -0
  106. package/packages/gen/src/video/h3.ts +107 -0
  107. package/packages/gen/src/video/http.ts +44 -0
  108. package/packages/gen/src/video/ltx.ts +60 -0
  109. package/packages/gen/src/video/minimax.ts +55 -0
  110. package/packages/gen/src/video/veo.ts +71 -0
  111. package/packages/jizura/package.json +16 -0
  112. package/packages/jizura/src/deny.ts +73 -0
  113. package/packages/jizura/src/index.ts +7 -0
  114. package/packages/jizura/src/install.ts +226 -0
  115. package/packages/jizura/src/layer.ts +282 -0
  116. package/packages/jizura/src/lyrics.ts +81 -0
  117. package/packages/jizura/src/pin.ts +5 -0
  118. package/packages/jizura/src/vite.ts +36 -0
  119. package/packages/motion/README.md +142 -0
  120. package/packages/motion/package.json +29 -0
  121. package/packages/motion/src/camera.ts +97 -0
  122. package/packages/motion/src/captions.ts +479 -0
  123. package/packages/motion/src/enter.ts +128 -0
  124. package/packages/motion/src/hangul.ts +129 -0
  125. package/packages/motion/src/impact.ts +100 -0
  126. package/packages/motion/src/index.ts +17 -0
  127. package/packages/motion/src/moves/brushWipe.ts +90 -0
  128. package/packages/motion/src/moves/cloneArray.ts +111 -0
  129. package/packages/motion/src/moves/curtains.ts +81 -0
  130. package/packages/motion/src/moves/echoStack.ts +70 -0
  131. package/packages/motion/src/moves/fit.ts +34 -0
  132. package/packages/motion/src/moves/flood.ts +61 -0
  133. package/packages/motion/src/moves/ghostStack.ts +79 -0
  134. package/packages/motion/src/moves/gridFloor.ts +100 -0
  135. package/packages/motion/src/moves/imageSwarm.ts +109 -0
  136. package/packages/motion/src/moves/index.ts +19 -0
  137. package/packages/motion/src/moves/irisBlades.ts +113 -0
  138. package/packages/motion/src/moves/karaokeLine.ts +94 -0
  139. package/packages/motion/src/moves/ledBoard.ts +94 -0
  140. package/packages/motion/src/moves/liquidTab.ts +72 -0
  141. package/packages/motion/src/moves/markerWrite.ts +63 -0
  142. package/packages/motion/src/moves/morphBox.ts +96 -0
  143. package/packages/motion/src/moves/neonSign.ts +77 -0
  144. package/packages/motion/src/moves/panelPop.ts +78 -0
  145. package/packages/motion/src/moves/paperTear.ts +75 -0
  146. package/packages/motion/src/moves/path.ts +64 -0
  147. package/packages/motion/src/moves/pathText.ts +107 -0
  148. package/packages/motion/src/moves/pointSphere.ts +109 -0
  149. package/packages/motion/src/moves/ransomLetters.ts +73 -0
  150. package/packages/motion/src/moves/shapeMorph.ts +143 -0
  151. package/packages/motion/src/moves/slamWords.ts +89 -0
  152. package/packages/motion/src/moves/sparkTrail.ts +114 -0
  153. package/packages/motion/src/moves/speedLines.ts +91 -0
  154. package/packages/motion/src/moves/splitFlap.ts +147 -0
  155. package/packages/motion/src/moves/stepWords.ts +117 -0
  156. package/packages/motion/src/moves/strikeOut.ts +98 -0
  157. package/packages/motion/src/moves/sunburst.ts +58 -0
  158. package/packages/motion/src/moves/tickerRows.ts +87 -0
  159. package/packages/motion/src/moves/tokenPicker.ts +114 -0
  160. package/packages/motion/src/moves/truchet.ts +85 -0
  161. package/packages/motion/src/moves/wordWall.ts +76 -0
  162. package/packages/motion/src/moves/worldRoll.ts +31 -0
  163. package/packages/motion/src/particles.ts +102 -0
  164. package/packages/motion/src/react/index.tsx +21 -0
  165. package/packages/motion/src/react/parts.tsx +431 -0
  166. package/packages/motion/src/react/time.tsx +97 -0
  167. package/packages/motion/src/text.ts +103 -0
  168. package/packages/motion/src/transition.ts +82 -0
  169. package/packages/motion/src/transition3d/cards.ts +65 -0
  170. package/packages/motion/src/transition3d/chain.ts +84 -0
  171. package/packages/motion/src/transition3d/dive.ts +181 -0
  172. package/packages/motion/src/transition3d/fold.ts +110 -0
  173. package/packages/motion/src/transition3d/gl.ts +219 -0
  174. package/packages/motion/src/transition3d/index.ts +228 -0
  175. package/packages/motion/src/transition3d/live.ts +70 -0
  176. package/packages/motion/src/transition3d/portal.ts +115 -0
  177. package/packages/motion/src/transition3d/react.tsx +29 -0
  178. package/packages/motion/src/transition3d/rig.ts +502 -0
  179. package/packages/motion/src/transition3d/shatter.ts +101 -0
  180. package/packages/motion/src/transition3d/whip.ts +65 -0
  181. package/packages/motion/src/transition3d/world.ts +683 -0
  182. package/packages/motion/src/transition3d/zoomOut.ts +114 -0
  183. package/packages/motion/src/transitions2d/catalog.ts +127 -0
  184. package/packages/motion/src/transitions2d/css.d.ts +2 -0
  185. package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
  186. package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
  187. package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
  188. package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
  189. package/packages/motion/src/transitions2d/index.ts +2 -0
  190. package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
  191. package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
  192. package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
  193. package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
  194. package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
  195. package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
  196. package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
  197. package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
  198. package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
  199. package/packages/motion/src/transitions2d/react.ts +13 -0
  200. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
  201. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
  202. package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
  203. package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
  204. package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
  205. package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
  206. package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
  207. package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
  208. package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
  209. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
  210. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
  211. package/packages/motion/src/type.ts +131 -0
  212. package/packages/motion/src/ui.ts +172 -0
  213. package/packages/plan/package.json +15 -0
  214. package/packages/plan/src/brief.ts +254 -0
  215. package/packages/plan/src/captions.ts +75 -0
  216. package/packages/plan/src/detect.ts +93 -0
  217. package/packages/plan/src/index.ts +23 -0
  218. package/packages/plan/src/layout.ts +176 -0
  219. package/packages/plan/src/llm.ts +110 -0
  220. package/packages/plan/src/look.ts +220 -0
  221. package/packages/plan/src/plan.ts +125 -0
  222. package/packages/plan/src/request.ts +55 -0
  223. package/packages/plan/src/studio.ts +53 -0
  224. package/packages/plan/src/templates.ts +42 -0
  225. package/packages/plan/src/themes.ts +48 -0
  226. package/packages/plan/src/types.ts +106 -0
  227. package/packages/studio/package.json +12 -0
  228. package/packages/studio/src/index.ts +1 -0
  229. package/packages/studio/src/plugin.ts +236 -0
  230. package/packages/studio/src/ui/main.ts +503 -0
  231. package/packages/studio/src/ui/studio.css +165 -0
  232. package/packages/studio/src/ui/timeline.ts +317 -0
  233. package/packages/themes/kit/THEME-MAKE.md +68 -0
  234. package/packages/themes/kit/THEME.md +30 -0
  235. package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
  236. package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
  237. package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
  238. package/packages/themes/kit/effects.ts +5 -0
  239. package/packages/themes/kit/sample/index.html +11 -0
  240. package/packages/themes/kit/sample/main.ts +9 -0
  241. package/packages/themes/kit/sample/scenes/scene.ts +15 -0
  242. package/packages/themes/package.json +16 -0
  243. package/packages/themes/src/factory.ts +296 -0
  244. package/packages/themes/src/fx.ts +14 -0
  245. package/packages/themes/src/index.ts +105 -0
  246. package/packages/themes/src/stage3d.ts +177 -0
  247. package/packages/themes/src/types.ts +58 -0
  248. package/presets/kawaii-pastel.json +94 -0
  249. package/presets/neutral-dark.json +82 -0
  250. package/presets/og.json +98 -0
  251. package/skills/kasaframe/SKILL.md +32 -0
  252. package/templates/_shared/project/index.html +11 -0
  253. package/templates/_shared/project/lib/react.ts +22 -0
  254. package/templates/_shared/project/lib/stage.ts +272 -0
  255. package/templates/_shared/project/main.ts +9 -0
  256. package/templates/_shared/scenes/default.ts +18 -0
  257. package/templates/_shared/scenes/drop.ts +30 -0
  258. package/templates/_shared/scenes/end.ts +23 -0
  259. package/templates/_shared/scenes/hook.ts +16 -0
  260. package/templates/_shared/scenes/seam.ts +41 -0
  261. package/templates/_shared/scenes/step.ts +40 -0
  262. package/templates/ad/preview.png +0 -0
  263. package/templates/ad/template.json +132 -0
  264. package/templates/demo/preview.png +0 -0
  265. package/templates/demo/template.json +130 -0
  266. package/templates/explainer/preview.png +0 -0
  267. package/templates/explainer/template.json +142 -0
  268. package/templates/launch/preview.png +0 -0
  269. package/templates/launch/template.json +130 -0
  270. package/templates/mv/preview.png +0 -0
  271. package/templates/mv/scenes/default.ts +95 -0
  272. package/templates/mv/template.json +176 -0
  273. package/templates/promo/preview.png +0 -0
  274. package/templates/promo/template.json +144 -0
  275. package/templates/shorts/preview.png +0 -0
  276. package/templates/shorts/scenes/hook.ts +34 -0
  277. package/templates/shorts/template.json +127 -0
  278. package/templates/square/preview.png +0 -0
  279. package/templates/square/template.json +113 -0
  280. package/themes/debug-hud/THEME.md +39 -0
  281. package/themes/debug-hud/effects.ts +389 -0
  282. package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
  283. package/themes/debug-hud/sample/beats.json +1 -0
  284. package/themes/debug-hud/sample/index.html +11 -0
  285. package/themes/debug-hud/sample/kf.json +59 -0
  286. package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
  287. package/themes/debug-hud/sample/lib/og.ts +29 -0
  288. package/themes/debug-hud/sample/main.ts +9 -0
  289. package/themes/debug-hud/sample/scenes/end.ts +62 -0
  290. package/themes/debug-hud/sample/scenes/gather.ts +67 -0
  291. package/themes/debug-hud/sample/scenes/models.ts +70 -0
  292. package/themes/debug-hud/sample/scenes/price.ts +79 -0
  293. package/themes/debug-hud/sample/style.json +91 -0
  294. package/themes/debug-hud/still.png +0 -0
  295. package/themes/debug-hud/style.json +91 -0
  296. package/themes/debug-hud/theme.json +68 -0
  297. package/themes/og-measure/THEME.md +47 -0
  298. package/themes/og-measure/effects.ts +432 -0
  299. package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
  300. package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
  301. package/themes/og-measure/sample/assets/data/measure.json +353 -0
  302. package/themes/og-measure/sample/beats.json +1 -0
  303. package/themes/og-measure/sample/index.html +11 -0
  304. package/themes/og-measure/sample/kf.json +66 -0
  305. package/themes/og-measure/sample/lib/data.ts +62 -0
  306. package/themes/og-measure/sample/main.ts +9 -0
  307. package/themes/og-measure/sample/measure.ts +78 -0
  308. package/themes/og-measure/sample/scenes/end.ts +47 -0
  309. package/themes/og-measure/sample/scenes/gather.ts +51 -0
  310. package/themes/og-measure/sample/scenes/models.ts +34 -0
  311. package/themes/og-measure/sample/scenes/one.ts +39 -0
  312. package/themes/og-measure/sample/scenes/price.ts +56 -0
  313. package/themes/og-measure/sample/style.json +97 -0
  314. package/themes/og-measure/still.png +0 -0
  315. package/themes/og-measure/style.json +97 -0
  316. package/themes/og-measure/theme.json +79 -0
  317. package/themes/riso-zine/THEME.md +43 -0
  318. package/themes/riso-zine/effects.ts +321 -0
  319. package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
  320. package/themes/riso-zine/sample/beats.json +1 -0
  321. package/themes/riso-zine/sample/index.html +11 -0
  322. package/themes/riso-zine/sample/kf.json +59 -0
  323. package/themes/riso-zine/sample/lib/og.ts +29 -0
  324. package/themes/riso-zine/sample/lib/shot.ts +86 -0
  325. package/themes/riso-zine/sample/lib/zine.ts +42 -0
  326. package/themes/riso-zine/sample/main.ts +9 -0
  327. package/themes/riso-zine/sample/scenes/end.ts +61 -0
  328. package/themes/riso-zine/sample/scenes/gather.ts +62 -0
  329. package/themes/riso-zine/sample/scenes/models.ts +52 -0
  330. package/themes/riso-zine/sample/scenes/price.ts +82 -0
  331. package/themes/riso-zine/sample/style.json +99 -0
  332. package/themes/riso-zine/still.png +0 -0
  333. package/themes/riso-zine/style.json +99 -0
  334. package/themes/riso-zine/theme.json +75 -0
  335. package/tools/beats/beats.py +372 -0
  336. package/tools/beats/requirements.txt +3 -0
  337. package/tools/lyrics/align.py +164 -0
  338. package/tools/lyrics/requirements.txt +9 -0
@@ -0,0 +1,125 @@
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 { pickTheme, suggestThemes, themeFromRequest } from "./themes.ts";
6
+ import { basename } from "node:path";
7
+ import type { Aspect, BeatsLike, GenSpec, Plan, PlannedScene, Purpose, Template } from "./types.ts";
8
+
9
+ const IMAGE = /\.(png|jpe?g|webp)$/i;
10
+
11
+ // 이음매 기본은 모션그래픽 전환이다. 영상 모델은 요청에 분명히 적었을 때만 부른다(돈·시간이 크고 결이 들쭉날쭉하다)
12
+ export function wantsVideo(request: string): boolean {
13
+ return /영상\s*생성|생성\s*영상|AI\s*영상|영상\s*모델|video\s*(gen|model)|generated\s*video/i.test(request);
14
+ }
15
+
16
+ export interface PlanInput {
17
+ request: string;
18
+ purpose?: Purpose; // 사람이 못 박으면 판별을 건너뛴다
19
+ assets?: string[];
20
+ music?: string | null;
21
+ beats?: BeatsLike | null;
22
+ duration?: number;
23
+ aspect?: Aspect;
24
+ style?: string;
25
+ title?: string;
26
+ theme?: string; // 테마 id 나 "auto"
27
+ }
28
+
29
+ export function makePlan(input: PlanInput): Plan {
30
+ const templates = listTemplates();
31
+ const det: Detection = input.purpose
32
+ ? { purpose: input.purpose, confidence: 1, ranked: [], ambiguous: false }
33
+ : detect({ request: input.request, assets: input.assets, music: input.music }, templates);
34
+ const tpl = loadTemplate(det.purpose);
35
+ const aspect = input.aspect ?? parseAspect(input.request);
36
+ const frame = frameFor(tpl, aspect && (aspect === tpl.aspect || tpl.variants?.[aspect]) ? aspect : undefined);
37
+ const { duration, notes } = pickDuration(tpl, { requested: input.duration ?? parseDuration(input.request), beats: input.beats });
38
+ const laid = layoutScenes(tpl, duration, input.beats);
39
+ // auto: 요청이 테마를 말로 짚었으면 그 테마, 아니면 내용에 맞는 approved 후보 셋 중 첫째
40
+ const named = input.theme === "auto" ? themeFromRequest(input.request, tpl.id) : null;
41
+ const themeCandidates = input.theme === "auto" ? [...(named ? [named] : []), ...suggestThemes(input.request, tpl.id, frame.aspect).filter((c) => c.id !== named?.id)].slice(0, 3) : undefined;
42
+ const theme = input.theme === "auto" ? (themeCandidates?.[0]?.id ?? null) : input.theme ? pickTheme(input.theme).id : null;
43
+ // 테마를 고르면 그 테마의 style 이 프리셋을 대신한다(kf new 가 themeStyle 로 쓴다)
44
+ const style = theme ?? input.style ?? parseStyle(input.request) ?? tpl.style;
45
+ const title = input.title ?? parseTitle(input.request) ?? "";
46
+ const gen = genSpecs(tpl, laid.scenes, { subject: title || input.request, style, width: frame.width, height: frame.height, video: wantsVideo(input.request), refs: (input.assets ?? []).filter((a) => IMAGE.test(a)).map((a) => `assets/in/${basename(a)}`) });
47
+
48
+ const scenes: PlannedScene[] = laid.scenes.map((s) => ({
49
+ id: s.id,
50
+ role: s.role,
51
+ from: s.from,
52
+ dur: s.dur,
53
+ idea: s.idea,
54
+ assets: gen.filter((g) => g.scene === s.id).map((g) => `gen:${g.kind}:${g.id}`),
55
+ }));
56
+
57
+ const top = det.ranked[0];
58
+ const plan: Plan = {
59
+ request: input.request,
60
+ purpose: det.purpose,
61
+ confidence: det.confidence,
62
+ alternatives: det.ranked.slice(1, 3).map((r) => ({ purpose: r.purpose, p: r.p })),
63
+ reasons: [...(top?.reasons ?? (input.purpose ? ["사람이 정함"] : [])), ...notes, ...laid.notes],
64
+ title,
65
+ aspect: frame.aspect,
66
+ width: frame.width,
67
+ height: frame.height,
68
+ fps: tpl.fps,
69
+ duration: laid.duration,
70
+ style,
71
+ music: input.music ?? null,
72
+ captions: tpl.captions,
73
+ deliverables: tpl.deliverables,
74
+ scenes,
75
+ gen,
76
+ theme,
77
+ ...(themeCandidates ? { themeCandidates } : {}),
78
+ };
79
+ if (det.ambiguous) {
80
+ plan.questions = [
81
+ {
82
+ id: "purpose",
83
+ ask: "어떤 용도의 영상인가?",
84
+ choices: det.ranked.slice(0, 4).map((r) => `${r.purpose} — ${templates.find((t) => t.id === r.purpose)?.label}`),
85
+ },
86
+ ];
87
+ }
88
+ if (tpl.music === "required" && !input.music) {
89
+ plan.questions = [...(plan.questions ?? []), { id: "music", ask: `${tpl.label} 영상은 곡이 끌고 간다. 쓸 곡 파일은?`, choices: [] }];
90
+ }
91
+ return plan;
92
+ }
93
+
94
+ // "img:hook-bg" → hook 장면 바탕 그림, "vid:seam" → seam 장면 전체를 채우는 이음매 클립(앞 장면 끝 프레임 → 뒤 장면 첫 프레임, 계약 8)
95
+ function genSpecs(
96
+ tpl: Template,
97
+ scenes: { id: string; role: string; idea: string }[],
98
+ o: { subject: string; style: string; width: number; height: number; video: boolean; refs: string[] },
99
+ ): GenSpec[] {
100
+ const gen: GenSpec[] = [];
101
+ for (const spec of [...tpl.assets.gen, ...(o.video ? (tpl.assets.genVideo ?? []) : [])]) {
102
+ const [kind, id] = spec.split(":") as ["img" | "vid", string];
103
+ if (kind === "vid") {
104
+ const i = scenes.findIndex((s) => s.id === id);
105
+ if (i <= 0 || i === scenes.length - 1) continue;
106
+ gen.push({
107
+ id,
108
+ kind,
109
+ scene: id,
110
+ seam: true,
111
+ prompt: `${o.subject} — 「${scenes[i - 1].idea}」 화면에서 「${scenes[i + 1].idea}」 화면으로 이음매 없이 빨려 들어가는 카메라 이동. ${o.style} 결, 글자 없음.`,
112
+ });
113
+ continue;
114
+ }
115
+ // 뮤비 주인공은 받은 그림을 참조해 한 장 만들고, 나머지 그림은 그 주인공을 참조해 같은 얼굴로 간다
116
+ if (id === "character") {
117
+ 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 } : {}) });
118
+ continue;
119
+ }
120
+ const scene = scenes.find((s) => id === s.id || id.startsWith(`${s.id}-`)) ?? scenes[0];
121
+ const refs = gen.some((g) => g.id === "character") ? { refs: ["assets/gen/character.png"] } : {};
122
+ gen.push({ id, kind, scene: scene.id, prompt: `${o.subject} ${tpl.label} 영상 「${scene.idea}」 장면의 바탕 그림. ${o.style} 결, 글자·로고 없음.`, size: `${o.width}x${o.height}`, ...refs });
123
+ }
124
+ return gen;
125
+ }
@@ -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,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,106 @@
1
+ export type Aspect = "16:9" | "9:16" | "1:1";
2
+ export type Purpose = "promo" | "shorts" | "explainer" | "launch" | "demo" | "ad" | "square" | "mv";
3
+ export const PURPOSES: Purpose[] = ["promo", "shorts", "explainer", "launch", "demo", "ad", "square", "mv"];
4
+
5
+ export interface SafeArea { top: number; bottom: number; left: number; right: number }
6
+
7
+ export interface TemplateScene {
8
+ id: string;
9
+ role: string;
10
+ sec?: number;
11
+ weight?: number;
12
+ anchor?: string; // start · end · drop:<k> · drop:last · section:<kind>
13
+ snap?: "bar" | "beat" | "none";
14
+ idea: string;
15
+ transition?: string; // 이음매 장면의 3D 전환 종류(dive·portal·cards·shatter·zoomOut·fold)
16
+ }
17
+
18
+ export interface Template {
19
+ id: Purpose;
20
+ label: string;
21
+ summary: string; // 틀 고르는 화면의 한 줄
22
+ aspect: Aspect;
23
+ width: number;
24
+ height: number;
25
+ fps: number;
26
+ variants?: Partial<Record<Aspect, { width: number; height: number; safeArea?: SafeArea }>>;
27
+ duration: { min: number; max: number; default: number; allowed?: number[] };
28
+ music: "required" | "optional" | "none";
29
+ // off: srt·vtt 만 · on: 화면에도 얹는다 · none: 없음 (옛 이름 track·burned 도 읽는다 — @kasaframe/motion/captions 의 captionMode)
30
+ captions: "off" | "on" | "none";
31
+ loop: boolean;
32
+ style: string;
33
+ safeArea: SafeArea;
34
+ text: { policy: "none" | "terms-only" | "captions" | "lyrics"; maxOnScreen: number; minPx: number };
35
+ // 뮤비: kinetic 이면 부른 낱말이 그 시각에 화면에 뜬다(lyrics.json 낱말 시각), track 이면 자막 트랙만
36
+ lyrics?: { mode: "kinetic" | "track" | "none"; align?: string; lipsync?: string };
37
+ motion: Record<string, unknown>;
38
+ scenes: TemplateScene[];
39
+ // sections: 곡 구간(beats.json sections)마다 장면 하나 — scenes 는 곡이 없을 때 쓰는 기본, sectionIdeas 는 구간 종류별 할 일
40
+ layout?: "fixed" | "sections";
41
+ sectionIdeas?: Record<string, string>;
42
+ // gen 은 늘 만드는 그림, genVideo 는 요청에 「영상 생성」을 분명히 적었을 때만(이음매 기본은 모션그래픽 전환)
43
+ assets: { need: string[]; want: string[]; gen: string[]; genVideo?: string[] };
44
+ deliverables: string[];
45
+ detect: { words: string[]; hints: string[]; aspect: Aspect; assetKinds?: string[] };
46
+ }
47
+
48
+ // SPEC 4 중 장면 배치에 쓰는 것만
49
+ export interface BeatsLike {
50
+ duration: number;
51
+ beats: number[];
52
+ downbeats?: number[];
53
+ bars?: number[];
54
+ drops?: number[];
55
+ sections?: { t: number; kind: string; strength?: number }[];
56
+ }
57
+
58
+ export interface PlannedScene {
59
+ id: string;
60
+ role: string;
61
+ from: number;
62
+ dur: number;
63
+ idea: string;
64
+ assets: string[];
65
+ }
66
+
67
+ // SPEC 계약 8 — @kasaframe/gen 의 GenSpec 과 같은 꼴. 이음매(seam) 영상의 id 는 kf.json 장면 id 와 같다
68
+ export interface GenSpec {
69
+ id: string;
70
+ kind: "img" | "vid";
71
+ scene?: string;
72
+ prompt: string;
73
+ size?: string;
74
+ transparent?: boolean;
75
+ quality?: string;
76
+ refs?: string[];
77
+ seam?: boolean;
78
+ dur?: number;
79
+ first?: string;
80
+ last?: string;
81
+ provider?: string;
82
+ }
83
+
84
+ export interface Plan {
85
+ request: string;
86
+ purpose: Purpose;
87
+ confidence: number;
88
+ alternatives: { purpose: Purpose; p: number }[];
89
+ reasons: string[];
90
+ title: string;
91
+ aspect: Aspect;
92
+ width: number;
93
+ height: number;
94
+ fps: number;
95
+ duration: number;
96
+ style: string;
97
+ music: string | null;
98
+ captions: Template["captions"];
99
+ deliverables: string[];
100
+ scenes: PlannedScene[];
101
+ gen: GenSpec[];
102
+ // 고른 테마(themes/<id>). auto 면 approved 후보 셋을 themeCandidates 에 남기고 첫째를 쓴다
103
+ theme?: string | null;
104
+ themeCandidates?: { id: string; why: string }[];
105
+ questions?: { id: string; ask: string; choices: string[] }[];
106
+ }
@@ -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";