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,84 @@
1
+ import { hasKey, readKey } from "../keys.ts";
2
+ import { json } from "../video/http.ts";
3
+ import type { TtsProvider, Word } from "./types.ts";
4
+
5
+ // 키가 없어 실호출로 검증하지 못했다 — 요청·응답 모양은 ElevenLabs API 문서(2026-10, text-to-speech/convert-with-timestamps) 그대로.
6
+ // eleven_v4 는 Text to Dialogue 로만 열려 있어 낱말 시각을 주는 with-timestamps 에는 multilingual_v2(문서 기본, 긴 글에 가장 안정)를 쓴다
7
+ const BASE = process.env.KF_ELEVENLABS_BASE ?? "https://api.elevenlabs.io";
8
+ const MODEL = process.env.KF_ELEVENLABS_MODEL ?? "eleven_multilingual_v2";
9
+ // 1천 글자당 — multilingual·v3 $0.08, flash·turbo $0.04 (elevenlabs.io/pricing/api)
10
+ const PER_1K = /flash|turbo/.test(MODEL) ? 0.04 : 0.08;
11
+
12
+ // 기본 제공 목소리 — 이름으로 고르면 id 로 바꿔 보낸다. 계정에서 만든 목소리는 id 를 그대로 준다
13
+ const VOICES: { id: string; name: string; label: string }[] = [
14
+ { id: "JBFqnCBsd6RMkjVDRZzb", name: "George", label: "남 · 따뜻하고 낮은 이야기꾼" },
15
+ { id: "onwK4e9ZLuTAKqWW03F9", name: "Daniel", label: "남 · 깊고 단단한 낭독" },
16
+ { id: "nPczCjzI2devNBz1zQrb", name: "Brian", label: "남 · 깊고 차분한 내레이션" },
17
+ { id: "XrExE9yKIg1WjnnlVkGX", name: "Matilda", label: "여 · 차분하고 따뜻한 설명" },
18
+ { id: "Xb7hH8MSUJpSbSDYk0k2", name: "Alice", label: "여 · 또렷한 설명" },
19
+ { id: "pFZP5JQG7iQjIQuC4Bku", name: "Lily", label: "여 · 부드럽고 낮은 목소리" },
20
+ ];
21
+
22
+ // 글자마다 온 시각을 띄어쓰기로 묶어 낱말로 — 자막 판은 어절 단위로 끊는다
23
+ export function charsToWords(chars: string[], starts: number[], ends: number[]): Word[] {
24
+ const out: Word[] = [];
25
+ let cur: Word | null = null;
26
+ chars.forEach((c, i) => {
27
+ if (/\s/.test(c)) {
28
+ if (cur) out.push(cur);
29
+ cur = null;
30
+ return;
31
+ }
32
+ if (!cur) cur = { text: c, from: starts[i], to: ends[i] };
33
+ else {
34
+ cur.text += c;
35
+ cur.to = ends[i];
36
+ }
37
+ });
38
+ if (cur) out.push(cur);
39
+ return out;
40
+ }
41
+
42
+ export function elevenlabsTts(): TtsProvider {
43
+ return {
44
+ id: "elevenlabs",
45
+ label: "ElevenLabs",
46
+ model: MODEL,
47
+ keyName: "elevenlabs",
48
+ license: "ElevenLabs 약관 — 유료 요금제 출력물 상업 가능(무료는 출처 표기)",
49
+ region: "전 세계 API",
50
+ timestamps: true,
51
+ verified: false,
52
+ voices: VOICES.map((v) => ({ id: v.name, label: v.label })),
53
+ defaultVoice: "George",
54
+ ready: () => hasKey("elevenlabs"),
55
+ estimateUsd: (text) => (text.length / 1000) * PER_1K,
56
+
57
+ async synth(o) {
58
+ const id = VOICES.find((v) => v.name.toLowerCase() === o.voice.toLowerCase())?.id ?? o.voice;
59
+ const body: Record<string, unknown> = {
60
+ text: o.text,
61
+ model_id: MODEL,
62
+ voice_settings: { stability: 0.55, similarity_boost: 0.75, style: 0, speed: 1, use_speaker_boost: true },
63
+ };
64
+ // multilingual_v2 는 language_code 를 받지 않는다(문서)
65
+ if (o.lang && !/multilingual_v2/.test(MODEL)) body.language_code = o.lang;
66
+ const r = await json(
67
+ `${BASE}/v1/text-to-speech/${encodeURIComponent(id)}/with-timestamps?output_format=mp3_44100_128`,
68
+ { method: "POST", headers: { "xi-api-key": readKey("elevenlabs"), "Content-Type": "application/json" }, body: JSON.stringify(body) },
69
+ "ElevenLabs",
70
+ );
71
+ const a = r.alignment ?? r.normalized_alignment;
72
+ if (!r.audio_base64) throw new Error(`ElevenLabs 응답에 소리가 없다: ${JSON.stringify(r).slice(0, 200)}`);
73
+ return {
74
+ bytes: Buffer.from(r.audio_base64, "base64"),
75
+ ext: "mp3",
76
+ model: MODEL,
77
+ words: a ? charsToWords(a.characters, a.character_start_times_seconds, a.character_end_times_seconds) : undefined,
78
+ costUsd: (o.text.length / 1000) * PER_1K,
79
+ costEstimated: true,
80
+ params: { voice: id, model_id: MODEL },
81
+ };
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,73 @@
1
+ import { findKey, readKey } from "../keys.ts";
2
+ import { pcmToWav } from "../audio.ts";
3
+ import { json } from "../video/http.ts";
4
+ import type { TtsProvider } from "./types.ts";
5
+
6
+ // Gemini API Interactions(2026-10, ai.google.dev/gemini-api/docs/speech-generation). 낱말 시각은 주지 않는다 — 무음 자리로 맞춘다
7
+ const BASE = "https://generativelanguage.googleapis.com/v1beta";
8
+ const MODEL = process.env.KF_GEMINI_TTS_MODEL ?? "gemini-3.8-flash-tts";
9
+ // 1M 토큰당 — 소리 출력은 1초에 25토큰(2026-12-31 까지 값). lite 는 출력 $6
10
+ const PRICE = /lite/.test(MODEL) ? { in: 0.5, out: 6 } : { in: 0.5, out: 9 };
11
+ // 한국어 낭독은 1초에 6글자 안팎
12
+ const CHARS_PER_SEC = 6;
13
+
14
+ const VOICES = [
15
+ { id: "Charon", label: "남 · 차분한 정보 전달(Informative)" },
16
+ { id: "Gacrux", label: "여 · 성숙하고 낮은(Mature)" },
17
+ { id: "Algenib", label: "남 · 거칠고 낮은(Gravelly)" },
18
+ { id: "Sulafat", label: "여 · 따뜻한(Warm)" },
19
+ { id: "Schedar", label: "남 · 고른(Even)" },
20
+ { id: "Vindemiatrix", label: "여 · 부드러운(Gentle)" },
21
+ { id: "Kore", label: "여 · 단단한(Firm)" },
22
+ { id: "Orus", label: "남 · 단단한(Firm)" },
23
+ ];
24
+
25
+ export function geminiTts(): TtsProvider {
26
+ return {
27
+ id: "gemini",
28
+ label: "Gemini TTS",
29
+ model: MODEL,
30
+ keyName: "gemini",
31
+ license: "Google 생성형 AI 약관 — 출력물 소유권 주장 없음, 상업 가능",
32
+ region: "Gemini API 제공 지역(한국 포함)",
33
+ timestamps: false,
34
+ verified: true,
35
+ voices: VOICES,
36
+ defaultVoice: "Charon",
37
+ ready: () => findKey("gemini") !== null,
38
+ estimateUsd: (text) => ((text.length / CHARS_PER_SEC) * 25 * PRICE.out + text.length * PRICE.in) / 1e6,
39
+
40
+ async synth(o) {
41
+ const content: Record<string, unknown> = { type: "text", text: o.text };
42
+ if (o.style) content.annotations = [{ type: "speech_metadata", style: o.style }];
43
+ const body = {
44
+ model: MODEL,
45
+ input: [{ type: "user_input", content: [content] }],
46
+ response_format: { type: "audio", mime_type: "audio/l16", sample_rate: 24000 },
47
+ generation_config: { speech_config: [{ voice: o.voice }] },
48
+ };
49
+ const r = await json(`${BASE}/interactions`, { method: "POST", headers: { "x-goog-api-key": readKey("gemini"), "Content-Type": "application/json" }, body: JSON.stringify(body) }, "Gemini TTS");
50
+ const audio = (r.steps ?? [])
51
+ .filter((s: { type: string }) => s.type === "model_output")
52
+ .flatMap((s: { content?: { type: string; data?: string; mime_type?: string }[] }) => s.content ?? [])
53
+ .filter((c: { type: string }) => c.type === "audio")
54
+ .at(-1);
55
+ if (!audio?.data) throw new Error(`Gemini TTS 응답에 소리가 없다: ${JSON.stringify(r).slice(0, 200)}`);
56
+ const raw = Buffer.from(audio.data, "base64");
57
+ // l16 을 달라고 했지만 머리 붙은 wav 가 오면 그대로
58
+ const bytes = raw.subarray(0, 4).toString() === "RIFF" ? raw : pcmToWav(raw, 24000, 1);
59
+ const out = r.usage?.total_output_tokens ?? r.usage?.output_tokens;
60
+ const inp = r.usage?.total_input_tokens ?? r.usage?.input_tokens;
61
+ const real = typeof out === "number";
62
+ const sec = (bytes.length - 44) / (24000 * 2);
63
+ return {
64
+ bytes,
65
+ ext: "wav",
66
+ model: MODEL,
67
+ costUsd: real ? (out * PRICE.out + (inp ?? 0) * PRICE.in) / 1e6 : (sec * 25 * PRICE.out + o.text.length * PRICE.in) / 1e6,
68
+ costEstimated: !real,
69
+ params: { voice: o.voice, style: o.style ?? null },
70
+ };
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,241 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { type Choice, type Question, WAIT } from "../ask.ts";
4
+ import { findSilences, voicedSpan } from "../audio.ts";
5
+ import type { CapabilityProvider } from "../capabilities.ts";
6
+ import { cacheDir, sha256 } from "../generate.ts";
7
+ import { howToGet, whereToPut } from "../keys.ts";
8
+ import { dashscopeTts } from "./dashscope.ts";
9
+ import { elevenlabsTts } from "./elevenlabs.ts";
10
+ import { geminiTts } from "./gemini.ts";
11
+ import { localTts } from "./local.ts";
12
+ import type { SpeechRequest, TtsProvider, Word } from "./types.ts";
13
+
14
+ export type { SpeechRaw, SpeechRequest, TtsProvider, Voice, Word } from "./types.ts";
15
+ export { charsToWords } from "./elevenlabs.ts";
16
+
17
+ // 고르는 순서 — 낱말 시각을 직접 주는 ElevenLabs 먼저, 그다음 싸고 실호출로 확인한 Gemini
18
+ export function ttsProviders(): TtsProvider[] {
19
+ return [elevenlabsTts(), geminiTts(), dashscopeTts(), localTts()];
20
+ }
21
+
22
+ export function pickTts(engine: string): TtsProvider {
23
+ const p = ttsProviders().find((x) => x.id === engine);
24
+ if (!p) throw new Error(`목소리 엔진 없음: ${engine} (있는 것: ${ttsProviders().map((x) => x.id).join(" ")})`);
25
+ return p;
26
+ }
27
+
28
+ // "gemini:Charon" → 엔진·목소리. "none" 이면 목소리 없이 자막만. 목소리를 빼면 그 엔진 기본
29
+ export function parseVoice(v: string): { engine: string; voice: string } | null {
30
+ if (!v || v === "none") return null;
31
+ const i = v.indexOf(":");
32
+ const engine = i < 0 ? v : v.slice(0, i);
33
+ const p = pickTts(engine);
34
+ return { engine, voice: i < 0 ? p.defaultVoice : v.slice(i + 1) };
35
+ }
36
+
37
+ // 차분하고 낮은 낭독 — 용도 틀의 분위기(deep)와 같은 결
38
+ export const DEFAULT_STYLE = "차분하고 낮은 다큐멘터리 내레이션. 서두르지 않고 문장 끝을 내려 놓는다";
39
+
40
+ export interface Speech {
41
+ path: string;
42
+ words: Word[];
43
+ voiced: { from: number; to: number }; // 앞뒤 무음을 뺀 말 구간(조각 기준 초)
44
+ duration: number;
45
+ engine: string;
46
+ model: string;
47
+ voice: string;
48
+ costUsd: number;
49
+ costEstimated: boolean;
50
+ license: string;
51
+ cached: boolean;
52
+ }
53
+
54
+ // 문장 하나를 읽혀 outDir/<id>.wav|mp3 + <id>.json(낱말 시각). 같은 글·목소리·모델은 캐시에서 0원
55
+ export async function speak(id: string, req: SpeechRequest, ctx: { outDir: string; cacheDir?: string; log?: (l: string) => void }, engine: string): Promise<Speech> {
56
+ const p = pickTts(engine);
57
+ if (!p.ready()) throw new Error(`${p.label} 를 못 쓴다 — ${whereToPut(p.keyName)}`);
58
+ const key = sha256(JSON.stringify({ kind: "tts", provider: p.id, model: p.model, text: req.text, voice: req.voice, lang: req.lang ?? null, style: req.style ?? null }));
59
+ const dir = cacheDir(ctx);
60
+ mkdirSync(dir, { recursive: true });
61
+ mkdirSync(ctx.outDir, { recursive: true });
62
+ const metaPath = join(dir, `${key}.json`);
63
+ let cached = existsSync(metaPath);
64
+ let meta: Omit<Speech, "path" | "cached"> & { file: string };
65
+ if (cached) meta = JSON.parse(readFileSync(metaPath, "utf8"));
66
+ if (!cached || !existsSync(join(dir, meta!.file))) {
67
+ cached = false;
68
+ ctx.log?.(`목소리 ${id}: ${p.id} ${req.voice} 견적 $${p.estimateUsd(req.text).toFixed(4)}`);
69
+ const raw = await retryBusy(() => p.synth(req), { log: ctx.log });
70
+ const file = `${key}.${raw.ext}`;
71
+ writeFileSync(join(dir, file), raw.bytes);
72
+ const span = await voicedSpan(join(dir, file));
73
+ // 공급자가 준 시각만 캐시에 — 숨 자리로 맞춘 시각은 꺼낼 때마다 다시 맞춰 맞추는 법이 나아지면 캐시 소리에도 먹는다
74
+ meta = { file, words: raw.words ?? [], voiced: { from: span.from, to: span.to }, duration: span.total, engine: p.id, model: raw.model, voice: req.voice, costUsd: Number(raw.costUsd.toFixed(5)), costEstimated: raw.costEstimated, license: p.license };
75
+ writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}\n`);
76
+ }
77
+ if (!meta!.words.length || !p.timestamps) meta!.words = await alignWords(req.text, join(dir, meta!.file), meta!.voiced);
78
+ const ext = meta!.file.split(".").pop()!;
79
+ const path = join(ctx.outDir, `${id}.${ext}`);
80
+ copyFileSync(join(dir, meta!.file), path);
81
+ const { file: _f, ...rest } = meta!;
82
+ writeFileSync(join(ctx.outDir, `${id}.json`), `${JSON.stringify({ id, text: req.text, ...rest, cached }, null, 2)}\n`);
83
+ return { ...rest, path, cached };
84
+ }
85
+
86
+ // kf narrate 는 줄을 동시에 읽힌다 — 분당 호출 수가 빡빡한 키(Gemini 무료 등급 등)는 429 를 낸다. 물러섰다 다시, 다른 실패는 바로
87
+ const BUSY = new Set([429, 503]);
88
+ export async function retryBusy<T>(fn: () => Promise<T>, o: { waits?: number[]; log?: (l: string) => void; sleep?: (ms: number) => Promise<void> } = {}): Promise<T> {
89
+ const waits = o.waits ?? [5000, 10000, 20000, 40000];
90
+ const sleep = o.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
91
+ for (let i = 0; ; i++) {
92
+ try {
93
+ return await fn();
94
+ } catch (e) {
95
+ const status = (e as { status?: number }).status;
96
+ if (!status || !BUSY.has(status) || i >= waits.length) throw e;
97
+ o.log?.(`목소리: 공급자가 바쁨(${status}) — ${waits[i] / 1000}초 뒤 다시`);
98
+ await sleep(waits[i]);
99
+ }
100
+ }
101
+ }
102
+
103
+ const spoken = (s: string) => Math.max(1, s.replace(/[\s,.?!…:;·"'“”‘’()\-]/g, "").length);
104
+
105
+ // 마디 경계(글자 비율로 어림한 자리)를 숨에 순서대로 짝짓는다 — 하나씩 가까운 숨을 고르면 앞에서 어긋난 어림이 뒤까지 끈다.
106
+ // 짝 비용 = 거리 - 긴 숨 덤, 짝 없는 경계는 SKIP(어림 자리 그대로). 경계에 안 쓰인 숨(마디 안 숨)은 비용 없다
107
+ const SKIP = 1.0;
108
+ export function matchPauses(expect: number[], pauses: { from: number; to: number }[]): number[] {
109
+ const k = expect.length;
110
+ const m = pauses.length;
111
+ const cost = Array.from({ length: k + 1 }, () => new Array<number>(m + 1).fill(Infinity));
112
+ const back = Array.from({ length: k + 1 }, () => new Array<0 | 1 | 2>(m + 1).fill(0));
113
+ for (let j = 0; j <= m; j++) cost[0][j] = 0;
114
+ for (let i = 1; i <= k; i++) {
115
+ for (let j = 0; j <= m; j++) {
116
+ // 0: 경계 i 를 짝 없이 · 1: 숨 j 를 안 쓰고 · 2: 경계 i ↔ 숨 j
117
+ let best = cost[i - 1][j] + SKIP;
118
+ let how: 0 | 1 | 2 = 0;
119
+ if (j > 0 && cost[i][j - 1] < best) {
120
+ best = cost[i][j - 1];
121
+ how = 1;
122
+ }
123
+ if (j > 0) {
124
+ const p = pauses[j - 1];
125
+ const c = cost[i - 1][j - 1] + Math.abs((p.from + p.to) / 2 - expect[i - 1]) - 0.5 * Math.min(p.to - p.from, 0.6);
126
+ if (c < best) {
127
+ best = c;
128
+ how = 2;
129
+ }
130
+ }
131
+ cost[i][j] = best;
132
+ back[i][j] = how;
133
+ }
134
+ }
135
+ const out = new Array<number>(k).fill(-1);
136
+ for (let i = k, j = m; i > 0; ) {
137
+ const how = back[i][j];
138
+ if (how === 2) out[--i] = --j;
139
+ else if (how === 1) j--;
140
+ else i--;
141
+ }
142
+ return out;
143
+ }
144
+
145
+ // 시각을 안 주는 공급자 — 쉼표·마침표 자리를 말 사이 숨(무음)에 붙이고, 마디 안 낱말은 글자 수 비율로 편다
146
+ export async function alignWords(text: string, path: string, span?: { from: number; to: number }): Promise<Word[]> {
147
+ const tokens = text.trim().split(/\s+/).filter(Boolean);
148
+ if (!tokens.length) return [];
149
+ const { from: t0, to: t1 } = span ?? (await voicedSpan(path));
150
+ const total = tokens.reduce((a, w) => a + spoken(w), 0);
151
+ // 마디 끝 = 문장부호로 끝나는 어절(마지막 어절은 늘 끝)
152
+ const ends = tokens.map((w, i) => i < tokens.length - 1 && /[,.?!…:;]$/.test(w)).flatMap((e, i) => (e ? [i] : []));
153
+ const pauses = (await findSilences(path, { db: -38, min: 0.12 })).filter((s) => s.from > t0 + 0.05 && s.to < t1 - 0.05);
154
+ // 마디 경계의 글자 비율 자리
155
+ const expect: number[] = [];
156
+ let acc = 0;
157
+ for (let i = 0, k = 0; i < tokens.length && k < ends.length; i++) {
158
+ acc += spoken(tokens[i]);
159
+ if (i === ends[k]) {
160
+ expect.push(t0 + ((t1 - t0) * acc) / total);
161
+ k++;
162
+ }
163
+ }
164
+ const bounds = matchPauses(expect, pauses).map((j, i) => ({ at: ends[i], end: j < 0 ? expect[i] : pauses[j].from, start: j < 0 ? expect[i] : pauses[j].to }));
165
+ const out: Word[] = [];
166
+ let start = t0;
167
+ let first = 0;
168
+ for (const b of [...bounds, { at: tokens.length - 1, end: t1, start: t1 }]) {
169
+ const seg = tokens.slice(first, b.at + 1);
170
+ const n = seg.reduce((a, w) => a + spoken(w), 0);
171
+ let t = start;
172
+ for (const w of seg) {
173
+ const d = ((b.end - start) * spoken(w)) / n;
174
+ out.push({ text: w, from: +t.toFixed(3), to: +(t + d).toFixed(3) });
175
+ t += d;
176
+ }
177
+ start = b.start;
178
+ first = b.at + 1;
179
+ }
180
+ return out;
181
+ }
182
+
183
+ // 묻기 틀(ask.ts)에 얹는 목소리 질문. 보기 차례는 로컬 Qwen3(KF_QWEN3_TTS_URL 이 있을 때) · ElevenLabs · 자막만 · 그 밖 엔진 — 첫째가 권장.
184
+ // 키 없는 ElevenLabs 는 넣는 법을 붙인 보기(wait)로 서고, 로컬도 ElevenLabs 도 없으면 그 밖 엔진으로 조용히 넘어가지 않게 missing.
185
+ // voices 는 틀(template.json narration.voices)이 엔진마다 박은 목소리 — 같은 틀이면 늘 같은 목소리. chars 는 견적용 대본 글자 수(모르면 쇼츠 한 편 300자)
186
+ // ready 는 쓸 수 있는 엔진 id(생성 능력 표에서) — 안 주면 키를 직접 본다
187
+ export function voiceQuestion(o: { chars?: number; voices?: Record<string, string>; ready?: string[] } = {}): Question {
188
+ const sample = "가".repeat(o.chars ?? 300);
189
+ const all = ttsProviders();
190
+ const ready = new Set(all.filter((p) => (o.ready ? o.ready.includes(p.id) : p.ready())).map((p) => p.id));
191
+ const note = (p: TtsProvider) => [p.verified ? "" : "실호출 미검증", p.timestamps ? "낱말 시각 정확" : "낱말 시각은 숨 자리로 맞춤"].filter(Boolean).join(" · ");
192
+ const pick = (p: TtsProvider): Choice => {
193
+ const v = p.voices.find((x) => x.id === (o.voices?.[p.id] ?? p.defaultVoice)) ?? { id: o.voices?.[p.id] ?? p.defaultVoice, label: "틀이 정한 목소리" };
194
+ return { value: `${p.id}:${v.id}`, label: `${p.label} ${v.id} — ${v.label}`, costUsd: p.estimateUsd(sample), note: note(p) };
195
+ };
196
+ const local = all.find((p) => p.id === "local")!;
197
+ const eleven = all.find((p) => p.id === "elevenlabs")!;
198
+ const head = ready.has("local") ? [pick(local)] : [];
199
+ const choices = [
200
+ ...head,
201
+ ready.has("elevenlabs") ? pick(eleven) : { value: WAIT, label: `${eleven.label} 키를 넣고 다시`, note: howToGet(eleven.keyName) },
202
+ { value: "none", label: "목소리 없이 — 자막만", costUsd: 0 },
203
+ ...all.filter((p) => p.id !== "local" && p.id !== "elevenlabs" && ready.has(p.id)).map(pick),
204
+ ].slice(0, 4);
205
+ const missing = !ready.has("local") && !ready.has("elevenlabs");
206
+ return {
207
+ id: "voice",
208
+ cap: "voice",
209
+ ask: missing ? "내레이션 목소리 — 로컬 Qwen3(KF_QWEN3_TTS_URL)도 ElevenLabs 키도 없다" : "내레이션 목소리는?",
210
+ header: "목소리",
211
+ choices,
212
+ default: choices[0].value,
213
+ skip: head[0]?.value ?? "none",
214
+ flag: "--voice",
215
+ ...(missing ? { missing: true } : {}),
216
+ accepts: (v: string) => {
217
+ try {
218
+ const engine = parseVoice(v)?.engine;
219
+ return !engine || (o.ready ? o.ready.includes(engine) : pickTts(engine).ready());
220
+ } catch {
221
+ return false;
222
+ }
223
+ },
224
+ };
225
+ }
226
+
227
+ // 생성 능력 표(capabilities)의 목소리 칸
228
+ export function voiceCapabilities(): CapabilityProvider[] {
229
+ const sample = "가".repeat(300);
230
+ return ttsProviders().map((p) => ({
231
+ cap: "voice",
232
+ id: p.id,
233
+ label: `${p.label} ${p.model}`,
234
+ ready: p.ready(),
235
+ detail: p.ready() ? `${p.id === "local" ? "주소 있음" : "키 있음"}(키만 봄)${p.verified ? "" : " · 실호출 미검증"} · 목소리 ${p.voices.map((v) => v.id).join("·")}` : p.id === "local" ? "KF_QWEN3_TTS_URL 없음" : "키 없음",
236
+ fix: p.ready() ? undefined : p.id === "local" ? "메모리 넉넉한 기계에 tools/tts/qwen3_tts_server.py → KF_QWEN3_TTS_URL=<주소>" : whereToPut(p.keyName),
237
+ unitUsd: p.estimateUsd(sample),
238
+ unit: "쇼츠 한 편 대본(300자)",
239
+ license: p.license,
240
+ }));
241
+ }
@@ -0,0 +1,42 @@
1
+ import { findKey } from "../keys.ts";
2
+ import type { TtsProvider } from "./types.ts";
3
+
4
+ // 로컬 Qwen3-TTS(공개 가중치 Apache-2.0)는 이 기계에서 바로 띄우지 않는다 — 1.7B 를 여러 칸이 같이 쓰는 노트북에 올리면 메모리가 넘친다.
5
+ // 메모리 넉넉한 기계에 tools/tts/qwen3_tts_server.py 를 띄우고 KF_QWEN3_TTS_URL 로 주소만 준다. 설정돼 있을 때만 보기에 나온다
6
+ const VOICES = [
7
+ { id: "Sohee", label: "여 · 한국어 화자" },
8
+ { id: "Ryan", label: "남 · 리듬감 있는" },
9
+ { id: "Serena", label: "여 · 부드러운" },
10
+ { id: "Aiden", label: "남 · 밝은" },
11
+ { id: "Vivian", label: "여 · 또렷한" },
12
+ ];
13
+ const LANG: Record<string, string> = { ko: "Korean", en: "English", ja: "Japanese", zh: "Chinese" };
14
+
15
+ export function localTts(): TtsProvider {
16
+ return {
17
+ id: "local",
18
+ label: "로컬 Qwen3-TTS",
19
+ model: process.env.KF_QWEN3_TTS_MODEL ?? "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
20
+ keyName: "qwen3-tts",
21
+ license: "Qwen3-TTS 가중치 Apache-2.0 — 출력물 상업 가능",
22
+ region: "자체 호스팅",
23
+ timestamps: false,
24
+ verified: false,
25
+ voices: VOICES,
26
+ defaultVoice: "Sohee",
27
+ ready: () => findKey("qwen3-tts") !== null,
28
+ estimateUsd: () => 0,
29
+
30
+ async synth(o) {
31
+ const url = findKey("qwen3-tts")!.replace(/\/+$/, "");
32
+ const r = await fetch(`${url}/tts`, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify({ text: o.text, speaker: o.voice, language: LANG[o.lang ?? ""] ?? "Auto", instruct: o.style ?? "" }),
36
+ signal: AbortSignal.timeout(10 * 60_000),
37
+ });
38
+ if (!r.ok) throw new Error(`로컬 Qwen3-TTS ${r.status}: ${(await r.text()).slice(0, 200)}`);
39
+ return { bytes: new Uint8Array(await r.arrayBuffer()), ext: "wav", model: this.model, costUsd: 0, costEstimated: false, params: { voice: o.voice } };
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,44 @@
1
+ // 낱말 하나와 그 시각(조각 기준 초) — 자막이 이 시각 그대로 끊긴다
2
+ export interface Word {
3
+ text: string;
4
+ from: number;
5
+ to: number;
6
+ }
7
+
8
+ export interface Voice {
9
+ id: string; // --voice <엔진>:<id> 의 id
10
+ label: string; // 사람이 고르는 한 줄(성별·결)
11
+ }
12
+
13
+ export interface SpeechRequest {
14
+ text: string;
15
+ voice: string;
16
+ lang?: string; // "ko" 같은 ISO 639-1 — 공급자 꼴로 바꿔 보낸다
17
+ style?: string; // 「차분하고 낮게」 같은 연기 지시 — 받는 공급자만 쓴다
18
+ }
19
+
20
+ export interface SpeechRaw {
21
+ bytes: Uint8Array;
22
+ ext: "wav" | "mp3";
23
+ model: string;
24
+ words?: Word[]; // 공급자가 시각을 주면(ElevenLabs with-timestamps). 없으면 무음 자리로 맞춘다
25
+ costUsd: number;
26
+ costEstimated: boolean;
27
+ params: Record<string, unknown>;
28
+ }
29
+
30
+ export interface TtsProvider {
31
+ id: string; // --voice 의 엔진 이름
32
+ label: string;
33
+ model: string;
34
+ keyName: string;
35
+ license: string;
36
+ region: string;
37
+ timestamps: boolean; // 낱말 시각을 직접 주는지
38
+ verified: boolean; // 실호출로 확인했는지 — 아니면 고르는 글에 적는다
39
+ voices: Voice[];
40
+ defaultVoice: string;
41
+ ready(): boolean;
42
+ estimateUsd(text: string): number;
43
+ synth(o: SpeechRequest): Promise<SpeechRaw>;
44
+ }
@@ -0,0 +1,90 @@
1
+ export type AssetKind = "img" | "vid";
2
+
3
+ // 자산 옆 <id>.json. 같은 것을 다시 만들지, 얼마 들었는지, 어디에 써도 되는지를 이 파일 하나로 판단한다.
4
+ export interface AssetMeta {
5
+ id: string;
6
+ kind: AssetKind;
7
+ file: string; // 메타 파일 기준 상대 경로
8
+ provider: string;
9
+ model: string;
10
+ prompt: string;
11
+ seed: number | null;
12
+ params: Record<string, unknown>;
13
+ inputs: { role: string; sha256: string }[];
14
+ costUsd: number;
15
+ costEstimated: boolean; // 응답에 실제 사용량이 없어 단가표로 계산했으면 true
16
+ elapsedMs: number;
17
+ license: string;
18
+ region: string;
19
+ restrict?: string; // 회사·바깥 결과물에 걸리는 제한(공급자의 restrict) — kf make --finish 가 결과 메타로 모은다
20
+ cacheKey: string;
21
+ createdAt: string;
22
+ width?: number;
23
+ height?: number;
24
+ duration?: number;
25
+ fps?: number;
26
+ notes?: string[];
27
+ }
28
+
29
+ export interface Asset {
30
+ path: string;
31
+ meta: AssetMeta;
32
+ cached: boolean;
33
+ }
34
+
35
+ export interface ImageRequest {
36
+ prompt: string;
37
+ size: string; // "1536x1024" 처럼. 모델이 못 받는 크기면 가까운 크기로 만든 뒤 잘라 맞춘다
38
+ refs?: string[];
39
+ transparent?: boolean;
40
+ quality?: string;
41
+ }
42
+
43
+ export interface VideoRequest {
44
+ prompt: string;
45
+ firstFrame?: string;
46
+ lastFrame?: string;
47
+ duration: number;
48
+ aspect: string; // "16:9" · "9:16" · "1:1"
49
+ seed?: number;
50
+ resolution?: string; // "480p" · "720p" · "1080p"
51
+ }
52
+
53
+ // 공급자가 돌려주는 날것. 캐시·메타·파일 이름은 generate.ts 가 붙인다.
54
+ export interface RawResult {
55
+ bytes: Uint8Array;
56
+ ext: string;
57
+ model: string;
58
+ seed: number | null;
59
+ costUsd: number;
60
+ costEstimated: boolean;
61
+ params: Record<string, unknown>;
62
+ notes?: string[];
63
+ }
64
+
65
+ export interface ProviderInfo {
66
+ id: string;
67
+ kind: AssetKind;
68
+ model: string;
69
+ license: string;
70
+ region: string;
71
+ keyName: string;
72
+ restrict?: string; // 회사·바깥 결과물에 걸리는 라이선스·지역 제한 — 묻는 글·자산 메타에 그대로 남는다
73
+ available?(): boolean; // 키 말고 로그인·실행 파일로 서는 공급자(codex) — 없으면 keyName 의 키 유무
74
+ variant?: Record<string, unknown>; // 같은 모델이라도 결과를 바꾸는 서버 설정(걸음 수 등) — 캐시 키에 들어간다
75
+ }
76
+
77
+ export interface ImageProvider extends ProviderInfo {
78
+ kind: "img";
79
+ estimateUsd?(o: ImageRequest): number; // 없으면 OG 토큰 단가표
80
+ generate(o: ImageRequest): Promise<RawResult>;
81
+ edit?(o: ImageRequest): Promise<RawResult>;
82
+ }
83
+
84
+ export interface VideoProvider extends ProviderInfo {
85
+ kind: "vid";
86
+ durations: number[]; // 받을 수 있는 길이(초). 그 사이 값은 가장 가까운 위로 올린다
87
+ firstLast: boolean; // 끝 프레임까지 받는지 — 이음매 전환에는 이게 필요하다
88
+ estimateUsd(o: VideoRequest): number;
89
+ generate(o: VideoRequest): Promise<RawResult>;
90
+ }