kasaframe 0.1.1 โ†’ 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 (200) hide show
  1. package/NOTICE +10 -8
  2. package/README.md +16 -1
  3. package/app/server/engine.ts +133 -15
  4. package/app/server/main.ts +7 -3
  5. package/app/web/main.ts +218 -36
  6. package/app/web/style.css +37 -4
  7. package/bin/hooks.mjs +14 -1
  8. package/bin/kf.mjs +20 -2
  9. package/docs/AGENTS.md +64 -16
  10. package/music/README.md +31 -7
  11. package/package.json +2 -2
  12. package/packages/adapters/src/project.ts +1 -1
  13. package/packages/check/src/check.ts +10 -3
  14. package/packages/check/src/rules/assets.ts +4 -1
  15. package/packages/check/src/rules/frames.ts +2 -1
  16. package/packages/check/src/rules/layout.ts +30 -6
  17. package/packages/cli/package.json +2 -1
  18. package/packages/cli/src/blur.ts +15 -3
  19. package/packages/cli/src/commands/bench.ts +123 -0
  20. package/packages/cli/src/commands/blender.ts +72 -49
  21. package/packages/cli/src/commands/direct.ts +210 -0
  22. package/packages/cli/src/commands/doctor.ts +9 -1
  23. package/packages/cli/src/commands/look.ts +9 -2
  24. package/packages/cli/src/commands/lyrics.ts +47 -1
  25. package/packages/cli/src/commands/make.ts +729 -150
  26. package/packages/cli/src/commands/music.ts +78 -0
  27. package/packages/cli/src/commands/narrate.ts +238 -0
  28. package/packages/cli/src/commands/new.ts +14 -5
  29. package/packages/cli/src/commands/render.ts +94 -60
  30. package/packages/cli/src/commands/series.ts +110 -0
  31. package/packages/cli/src/commands/setup.ts +42 -3
  32. package/packages/cli/src/commands/theme.ts +89 -2
  33. package/packages/cli/src/deps.ts +6 -6
  34. package/packages/cli/src/kf.ts +14 -3
  35. package/packages/cli/src/mix.ts +131 -0
  36. package/packages/cli/src/pool.ts +34 -0
  37. package/packages/cli/src/segments.ts +402 -0
  38. package/packages/core/src/index.ts +1 -1
  39. package/packages/core/src/project.ts +11 -0
  40. package/packages/director/package.json +23 -0
  41. package/packages/director/src/assemble.ts +361 -0
  42. package/packages/director/src/brief.ts +122 -0
  43. package/packages/director/src/catalog.ts +49 -0
  44. package/packages/director/src/content.ts +343 -0
  45. package/packages/director/src/fallback.ts +58 -0
  46. package/packages/director/src/index.ts +13 -0
  47. package/packages/director/src/pattern.ts +90 -0
  48. package/packages/director/src/patterns/fill/cells.ts +115 -0
  49. package/packages/director/src/patterns/formula/stack.ts +93 -0
  50. package/packages/director/src/patterns/util.ts +58 -0
  51. package/packages/director/src/scene.ts +62 -0
  52. package/packages/director/src/select.ts +163 -0
  53. package/packages/director/src/transitions.ts +115 -0
  54. package/packages/gen/PROVIDERS.md +39 -4
  55. package/packages/gen/src/ask.ts +132 -0
  56. package/packages/gen/src/audio.ts +91 -0
  57. package/packages/gen/src/blender.ts +54 -0
  58. package/packages/gen/src/capabilities.ts +151 -0
  59. package/packages/gen/src/generate.ts +4 -2
  60. package/packages/gen/src/image/codex.ts +117 -0
  61. package/packages/gen/src/index.ts +10 -2
  62. package/packages/gen/src/keys.ts +26 -0
  63. package/packages/gen/src/music/elevenlabs.ts +41 -0
  64. package/packages/gen/src/music/index.ts +120 -0
  65. package/packages/gen/src/music/library.ts +73 -0
  66. package/packages/gen/src/music/lyria.ts +52 -0
  67. package/packages/gen/src/music/types.ts +28 -0
  68. package/packages/gen/src/registry.ts +6 -3
  69. package/packages/gen/src/tts/dashscope.ts +64 -0
  70. package/packages/gen/src/tts/elevenlabs.ts +84 -0
  71. package/packages/gen/src/tts/gemini.ts +73 -0
  72. package/packages/gen/src/tts/index.ts +241 -0
  73. package/packages/gen/src/tts/local.ts +42 -0
  74. package/packages/gen/src/tts/types.ts +44 -0
  75. package/packages/gen/src/types.ts +4 -0
  76. package/packages/gen/src/video/h3.ts +5 -0
  77. package/packages/gen/src/video/http.ts +1 -1
  78. package/packages/jizura/src/index.ts +2 -2
  79. package/packages/jizura/src/install.ts +198 -155
  80. package/packages/jizura/src/layer.ts +75 -32
  81. package/packages/jizura/src/pin.ts +4 -1
  82. package/packages/jizura/src/vendor.ts +140 -0
  83. package/packages/jizura/src/vite.ts +34 -7
  84. package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
  85. package/packages/jizura/vendor/NOTICE +43 -0
  86. package/packages/jizura/vendor/engine.json +47 -0
  87. package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
  88. package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
  89. package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
  90. package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
  91. package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  92. package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
  93. package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
  94. package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
  95. package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
  96. package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
  97. package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
  98. package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
  99. package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
  100. package/packages/jizura/vendor/fonts/fonts.json +52 -0
  101. package/packages/jizura/vendor/jizura_engine.js +35024 -0
  102. package/packages/motion/README.md +11 -0
  103. package/packages/motion/package.json +1 -0
  104. package/packages/motion/src/captions.ts +32 -24
  105. package/packages/motion/src/moves/brushWipe.ts +14 -1
  106. package/packages/motion/src/moves/dress.ts +191 -0
  107. package/packages/motion/src/moves/echoStack.ts +3 -3
  108. package/packages/motion/src/moves/fit.ts +36 -0
  109. package/packages/motion/src/moves/ghostStack.ts +14 -5
  110. package/packages/motion/src/moves/imageSwarm.ts +8 -4
  111. package/packages/motion/src/moves/index.ts +23 -4
  112. package/packages/motion/src/moves/karaokeLine.ts +17 -3
  113. package/packages/motion/src/moves/menu.ts +138 -0
  114. package/packages/motion/src/moves/neonSign.ts +2 -1
  115. package/packages/motion/src/moves/slamWords.ts +14 -7
  116. package/packages/motion/src/moves/sparkTrail.ts +16 -0
  117. package/packages/motion/src/moves/splitFlap.ts +4 -2
  118. package/packages/motion/src/moves/stepWords.ts +19 -8
  119. package/packages/motion/src/moves/tickerRows.ts +13 -9
  120. package/packages/motion/src/moves/tokenPicker.ts +4 -3
  121. package/packages/motion/src/moves/wordWall.ts +23 -21
  122. package/packages/motion/src/moves/worldRoll.ts +2 -1
  123. package/packages/motion/src/transition3d/index.ts +10 -3
  124. package/packages/motion/src/transition3d/rig.ts +107 -9
  125. package/packages/motion/src/transition3d/shatter.ts +5 -2
  126. package/packages/motion/src/transition3d/space.ts +173 -0
  127. package/packages/motion/src/transition3d/whip.ts +19 -6
  128. package/packages/motion/src/transition3d/world.ts +54 -0
  129. package/packages/motion/src/transitions2d/catalog.ts +1 -1
  130. package/packages/patterns/package.json +15 -0
  131. package/packages/patterns/src/bars/rank.ts +119 -0
  132. package/packages/patterns/src/before-after/shrink.ts +127 -0
  133. package/packages/patterns/src/before-after/strike.ts +125 -0
  134. package/packages/patterns/src/code/terminal.ts +133 -0
  135. package/packages/patterns/src/compare/bars.ts +142 -0
  136. package/packages/patterns/src/compare/duel.ts +103 -0
  137. package/packages/patterns/src/end/lockup.ts +85 -0
  138. package/packages/patterns/src/end/slam.ts +91 -0
  139. package/packages/patterns/src/flow/graph.ts +250 -0
  140. package/packages/patterns/src/hook-number/counter.ts +101 -0
  141. package/packages/patterns/src/hook-number/roll.ts +179 -0
  142. package/packages/patterns/src/hook-number/slam.ts +91 -0
  143. package/packages/patterns/src/index.ts +38 -0
  144. package/packages/patterns/src/kit.ts +449 -0
  145. package/packages/patterns/src/limit/gauge.ts +117 -0
  146. package/packages/patterns/src/list/cards.ts +131 -0
  147. package/packages/patterns/src/list/cycle.ts +22 -0
  148. package/packages/patterns/src/media/panels.ts +100 -0
  149. package/packages/patterns/src/objects.ts +234 -0
  150. package/packages/patterns/src/quote/karaoke.ts +126 -0
  151. package/packages/patterns/src/quote/slam.ts +22 -0
  152. package/packages/patterns/src/screen/focus.ts +85 -0
  153. package/packages/patterns/src/steps/path.ts +137 -0
  154. package/packages/patterns/src/steps/stack.ts +111 -0
  155. package/packages/patterns/src/term-reveal/chips.ts +92 -0
  156. package/packages/patterns/src/term-reveal/expand.ts +127 -0
  157. package/packages/patterns/src/timeline/axis.ts +144 -0
  158. package/packages/patterns/src/title/mask.ts +85 -0
  159. package/packages/patterns/src/title/slam.ts +23 -0
  160. package/packages/patterns/src/toggle-sim/switch.ts +92 -0
  161. package/packages/patterns/src/trend/columns.ts +108 -0
  162. package/packages/patterns/src/trend/draw.ts +140 -0
  163. package/packages/patterns/src/types.ts +67 -0
  164. package/packages/patterns/src/words/moves.ts +42 -0
  165. package/packages/patterns/src/words/slam.ts +4 -0
  166. package/packages/plan/src/asks.ts +256 -0
  167. package/packages/plan/src/brief.ts +136 -7
  168. package/packages/plan/src/captions.ts +5 -2
  169. package/packages/plan/src/index.ts +6 -2
  170. package/packages/plan/src/llm.ts +64 -2
  171. package/packages/plan/src/look.ts +3 -1
  172. package/packages/plan/src/plan.ts +67 -13
  173. package/packages/plan/src/series.ts +76 -0
  174. package/packages/plan/src/types.ts +53 -2
  175. package/packages/themes/package.json +5 -2
  176. package/packages/themes/src/index.ts +9 -0
  177. package/packages/themes/src/slotkit.ts +79 -0
  178. package/packages/themes/src/slots.ts +46 -0
  179. package/packages/themes/src/stage3d.ts +20 -0
  180. package/series/og-shorts.json +14 -0
  181. package/skills/kasaframe/SKILL.md +4 -3
  182. package/templates/_shared/project/lib/stage.ts +70 -50
  183. package/templates/_shared/scenes/default.ts +10 -7
  184. package/templates/_shared/scenes/drop.ts +11 -8
  185. package/templates/_shared/scenes/end.ts +9 -6
  186. package/templates/_shared/scenes/hook.ts +8 -6
  187. package/templates/_shared/scenes/step.ts +13 -4
  188. package/templates/ad/template.json +20 -1
  189. package/templates/demo/template.json +11 -0
  190. package/templates/explainer/template.json +54 -3
  191. package/templates/launch/template.json +34 -1
  192. package/templates/mv/scenes/default.ts +11 -8
  193. package/templates/mv/template.json +4 -2
  194. package/templates/promo/template.json +42 -1
  195. package/templates/shorts/scenes/hook.ts +8 -5
  196. package/templates/shorts/template.json +72 -3
  197. package/templates/square/template.json +21 -1
  198. package/themes/debug-hud/slots.ts +99 -0
  199. package/themes/og-measure/slots.ts +88 -0
  200. package/themes/riso-zine/slots.ts +81 -0
@@ -1,179 +1,432 @@
1
- import { spawnSync } from "node:child_process";
2
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
- import { basename, extname, join, resolve } from "node:path";
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, extname, join, resolve } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { fileURLToPath } from "node:url";
6
- import { genImage, genVideo, hasKey, NeedsApproval, seam, seamFrames, still, whereToPut } from "@kasaframe/gen";
7
- import { attachStudio, chat, codeBlock, detect, formatLook, jsonBlock, listTemplates, loadTemplate, look, lookSheet, type Message, type Plan, projectBrief, PURPOSES, type Purpose, readProject, writeCaptions } from "@kasaframe/plan";
6
+ import { askTerminal, blenderLibDir, capabilities, genImage, genVideo, hasKey, NeedsApproval, parseAllow, type Question, questionsText, seam, seamFrames, settle, still, whereToPut } from "@kasaframe/gen";
7
+ import { applyAnswers, attachStudio, loadSeries, type Series, seriesFromRequest, chat, codeBlock, detect, formatLook, jsonBlock, listTemplates, loadTemplate, look, lookSheet, makePlan, type Message, type Plan, planQuestions, premiumBlock, projectBrief, PURPOSES, type Purpose, readProject, spend, spentLine, WAIT, writeCaptions, writeStats } from "@kasaframe/plan";
8
+ import { checkContent, contentPrompt, directBrief } from "@kasaframe/director";
8
9
  import { transform } from "esbuild";
10
+ import { bundleFor, dressSeed, pickDress } from "@kasaframe/motion/menu";
9
11
  import { themeStyle } from "@kasaframe/themes";
10
12
  import { captionMode } from "@kasaframe/motion/captions";
13
+ import { jizuraCheck } from "@kasaframe/jizura/install";
11
14
  import { openPage } from "../browser.ts";
15
+ import { librosa } from "../deps.ts";
16
+ import { lyricsCheck } from "./lyrics.ts";
12
17
  import { type Command, num, parseFlags } from "../command.ts";
18
+ import { acquireRenderLock } from "../lock.ts";
19
+ import { gate, jobLimit, pool } from "../pool.ts";
13
20
 
14
21
  const kfBin = fileURLToPath(new URL("../kf.ts", import.meta.url));
22
+ const repo = fileURLToPath(new URL("../../../../", import.meta.url));
23
+
24
+ // ๋ฐ•์ž(librosa)ยท๋ฎค๋น„ ๊ฐ€์‚ฌ(JIZURAยท์ •๋ ฌ) โ€” ์—†์œผ๋ฉด ๋„ฃ๋Š” ๋ฒ•ยท์ง€๊ธˆ ๋˜๋Š” ๋‹ค๋ฅธ ๊ธธยท๋นผ๊ธฐ๋ฅผ ๋ฌป๋Š”๋‹ค
25
+ function toolQuestions(purpose: Purpose, willSong: boolean): Question[] {
26
+ const out: Question[] = [];
27
+ const b = librosa();
28
+ if (willSong && !b.ok)
29
+ out.push({
30
+ id: "beats", cap: "beats", header: "๋ฐ•์ž ๋ถ„์„", flag: "--beats", missing: true, default: WAIT, skip: "none",
31
+ ask: `๊ณก์— ์žฅ๋ฉด ๊ฒฝ๊ณ„ยท์›€์ง์ž„์„ ๋งž์ถœ ๋ฐ•์ž ๋ถ„์„(librosa)์ด ์—†๋‹ค โ€” ${b.detail}`,
32
+ choices: [{ value: WAIT, label: "๊น”๊ณ  ๋‹ค์‹œ", note: b.fix ?? "kf setup beats" }, { value: "none", label: "๊ณก ์—†์ด", note: "๊ณก์„ ๋นผ๊ณ  ํ™”๋ฉด ์•ˆ ์›€์ง์ž„์œผ๋กœ๋งŒ โ€” ๋ฐ• ์—†์ด ๊ณก๋งŒ ๊นŒ๋Š” ๊ธธ์€ ์—†๋‹ค" }],
33
+ });
34
+ if (purpose === "mv") {
35
+ const j = jizuraCheck();
36
+ const l = lyricsCheck();
37
+ if (!j.ok || !l.ok)
38
+ out.push({
39
+ id: "lyrics", cap: "lyrics", header: "๊ฐ€์‚ฌ", flag: "--lyrics", missing: true, default: WAIT, skip: "none",
40
+ ask: `๋ฎค๋น„ ๊ฐ€์‚ฌ์— ์“ธ ${[!j.ok ? "๊ฐ€์‚ฌ ์ธต ์—”์ง„(JIZURA)" : "", !l.ok ? "๊ฐ€์‚ฌ ์ •๋ ฌ(demucsยทstable-ts)" : ""].filter(Boolean).join("ยท")} ์ด ์—†๋‹ค`,
41
+ choices: [
42
+ { value: WAIT, label: "๊น”๊ณ  ๋‹ค์‹œ", note: `kf setup ${[!j.ok ? "jizura" : "", !l.ok ? "lyrics" : ""].filter(Boolean).join(" ")}` },
43
+ ...(l.ok && !j.ok ? [{ value: "words", label: "๊ฐ€์‚ฌ ์ธต ์—†์ด ๋‚ฑ๋ง๋กœ", note: "์ •๋ ฌํ•œ ๋‚ฑ๋ง์„ DOM ๊ธ€๋กœ ๋„์šด๋‹ค โ€” JIZURA ๋ ˆ์ด์•„์›ƒยทํšจ๊ณผ๋Š” ์—†๋‹ค" }] : []),
44
+ { value: "none", label: "๊ฐ€์‚ฌ ์—†์ด", note: "๊ณก๊ณผ ํ™”๋ฉด๋งŒ" },
45
+ ],
46
+ });
47
+ }
48
+ return out;
49
+ }
50
+
51
+ // ์‹œ๋ฆฌ์ฆˆ๊ฐ€ ๋ฐ•์€ ๋‹ต(๋ชฉ์†Œ๋ฆฌยท๊ณก ํŒŒ์ผยท์ƒ์„ฑ ๋‹ต) โ€” ์งˆ๋ฌธ ํ”Œ๋ž˜๊ทธ ๊ฐ’์œผ๋กœ ๋„ฃ์–ด ๋ฌป์ง€ ์•Š๋Š”๋‹ค
52
+ function seriesAnswers(s: Series | null): Record<string, string> {
53
+ if (!s) return {};
54
+ return Object.fromEntries(Object.entries({ ...s.answers, voice: s.voice, music: s.music?.file && resolve(repo, s.music.file) }).filter((e): e is [string, string] => typeof e[1] === "string"));
55
+ }
15
56
 
16
57
  // Claude Code ๋ฅผ OG ๋กœ ๋‹ค๋ฅธ ๋ชจ๋ธ(๋”ฅ์‹œํฌ ๋“ฑ)์— ๋ถ™์ด๋ฉด OG /v1/messages ๊ฐ€ ๊ทธ๋ฆผ์„ ๋ฒ„๋ฆฐ๋‹ค โ€” ์„ธ์…˜์ด kf sheet ๋ฅผ Read ํ•ด๋„ ๋ชป ๋ณธ๋‹ค.
17
58
  // ๊ทธ๋Ÿฐ ใ€Œ๋ˆˆ ์—†๋Š” ์„ธ์…˜ใ€์ด๋ฉด --finish ๋์— --vision-model ์„ ์•Œ๋ ค ์ค€๋‹ค(๊ทธ๋ฆผ ๋ณด๋Š” ๋ชจ๋ธ์€ ๊ธฐ๋ณธ์œผ๋กœ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค)
18
59
  export const eyelessSession = (): boolean =>
19
60
  /opengateway/i.test(process.env.ANTHROPIC_BASE_URL ?? "") && !/^anthropic\//.test(process.env.ANTHROPIC_MODEL ?? "anthropic/");
20
61
  const AUDIO = /\.(mp3|wav|m4a|aac|flac|ogg)$/i;
21
- const DEFAULT_MODEL = "deepseek/deepseek-v4-pro";
22
62
  const DEFAULT_VISION = "google/gemini-3.5-flash";
23
63
  // ํ™”๋ฉด์— ์ฐํžˆ๋ฉด ์•ˆ ๋˜๋Š” ๊ธ€ โ€” ์ง€๋‚œ ์‹คํ—˜์—์„œ ์‹ค์ œ๋กœ ๋‚˜์˜จ ๊ฒƒ๋“ค
24
64
  const BAD_TEXT = /\bundefined\b|\bNaN\b|\[object Object\]|\bnull\b|๏ฟฝ/;
25
65
 
26
66
  const cmd: Command = {
27
67
  summary: "์š”์ฒญ ํ•œ ์ค„๋กœ ๋๊นŒ์ง€ โ€” ์šฉ๋„ ํŒ๋ณ„ยท๊ณ„ํšยท์ƒ์„ฑ ์ž์‚ฐยท์žฅ๋ฉด ์ž‘์„ฑยท๋ Œ๋”ยท๊ฒ€์‚ฌยท๊ณ ์น˜๊ธฐ",
28
- usage: `kf make "<์š”์ฒญ>" [--out ํด๋”] [--assets ํด๋”] [--music ๊ณก] [--model claude-code|<OG ๋ชจ๋ธ>] [--purpose ์šฉ๋„] [--title ์ด๋ฆ„] [--yes]
68
+ usage: `kf make "<์š”์ฒญ>" [--out ํด๋”] [--assets ํด๋”] [--music ๊ณก] [--model claude-code|<OG ๋ชจ๋ธ>] [--purpose ์šฉ๋„] [--theme id|auto] [--title ์ด๋ฆ„] [--yes]
29
69
  kf make --finish <ํด๋”> [--model ...] ์žฅ๋ฉด์„ ๋‹ค ์ฑ„์šด ๋’ค ๋ Œ๋”ยท๊ฒ€์‚ฌยทํ‘œ์ง€(Claude Code ๋ชจ๋“œ์˜ ๋‘˜์งธ ๋‹จ๊ณ„)
30
- kf make --rewrite <ํด๋”> --scenes a,b [--model <OG ๋ชจ๋ธ>] ๊ณ ๋ฅธ ์žฅ๋ฉด๋งŒ ๋‹ค์‹œ ์“ฐ๊ณ  ๋ Œ๋”ยท๊ฒ€์‚ฌ(์ž‘์—…์‹ค ์žฅ๋ฉด ์นด๋“œ)
70
+ kf make --rewrite <ํด๋”> --scenes a,b [--model <OG ๋ชจ๋ธ>] ๊ณ ๋ฅธ ์žฅ๋ฉด๋งŒ ๋‹ค์‹œ ์“ฐ๊ณ  ๊ทธ ์กฐ๊ฐ๋งŒ ๋‹ค์‹œ ๊ตฝ๊ณ  ๊ทธ ๊ตฌ๊ฐ„๋งŒ ๊ฒ€์‚ฌ(์ž‘์—…์‹ค ์žฅ๋ฉด ์นด๋“œ)
71
+ ๊ฐ๋… ํ”„๋กœ์ ํŠธ๋Š” ๋ชจ๋ธ ์—†์ด kf direct --scenes ๋กœ ๋‹ค์‹œ ์ง“๋Š”๋‹ค(--pattern a=<ํŒจํ„ด>ยท--scene-seed a=<๊ธ€> ๋กœ ๋‹ค๋ฅธ ํ›„๋ณด)
72
+ ๋ชจ๋ธ์„ ์•ˆ ์ฃผ๋ฉด ๊ทธ ์ผ์„ ๋งŒ๋“  ๋ชจ๋ธ(plan.json ์˜ model) โ€” ์žฅ๋ฉด ์ฝ”๋“œ๋ฅผ ์“ฐ๋Š” ๊ธธ์ธ๋ฐ ๊ทธ๊ฒƒ๋„ ์—†์œผ๋ฉด ๋ฉˆ์ถ˜๋‹ค
73
+ kf make --finish <ํด๋”> --scenes a,b ์žฅ๋ฉด ํŒŒ์ผ์„ ์†์œผ๋กœ ๊ณ ์นœ ๋’ค โ€” ๋ฐ”๋€ ์กฐ๊ฐ๋งŒ ๋‹ค์‹œ ๊ตฝ๊ณ  ๊ทธ ๊ตฌ๊ฐ„๋งŒ ๊ฒ€์‚ฌ
74
+ kf make --answer <ํด๋”> --image <๊ฐ’> --video <๊ฐ’> โ€ฆ Claude Code ๋ชจ๋“œ์—์„œ MAKE.md ๋งจ ์œ„ ์งˆ๋ฌธ์˜ ๋‹ต์„ ๋„ฃ๋Š”๋‹ค(๊ณ„ํšยท์ง€์‹œ์„œ๋ฅผ ๊ณ ์ณ ์“ด๋‹ค)
75
+
76
+ ์ƒ์„ฑ ๋Šฅ๋ ฅ(kf doctor ใ€Œ์ƒ์„ฑ ๋Šฅ๋ ฅใ€): ๊ณ„ํš์€ ๊ทธ๋ฆผ ์ปทยท์ฃผ์ธ๊ณต 3D ์ƒท(Blender)ยท์ด์Œ๋งค 3D ์ „ํ™˜(three)ยท์บ๋ฆญํ„ฐ๊ฐ€ ์›€์ง์ด๋Š” ์ปท(์˜์ƒ ๋ชจ๋ธ)์„
77
+ ๊ธฐ๋ณธ์œผ๋กœ ๋„ฃ๋Š”๋‹ค. ๋ˆ์ด ๋“ค๊ฑฐ๋‚˜ ์—†๋Š” ๋Šฅ๋ ฅ์€ ๋งŒ๋“ค๊ธฐ ์ „์— ํ•œ ๋ฒˆ์— ๋ฌป๋Š”๋‹ค โ€” Claude Code ๋ชจ๋“œ๋Š” MAKE.md ๋งจ ์œ„, OG ๋ชจ๋“œ๋Š” ํ„ฐ๋ฏธ๋„์—์„œ
78
+ (ํ„ฐ๋ฏธ๋„์ด ์•„๋‹ˆ๋ฉด ๋ณด๊ธฐ์™€ ๋‹ค์‹œ ๋ถ€๋ฅผ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ฐ๊ณ  ์ข…๋ฃŒ ์ฝ”๋“œ 3)
79
+ --image og-โ€ฆ|codex-imagegen|none ยท --video <๊ณต๊ธ‰์ž>|none ยท --blender three|none ๊ทธ ์งˆ๋ฌธ์— ๋ฏธ๋ฆฌ ๋‹ตํ•œ๋‹ค
80
+ --voice <์—”์ง„>:<๋ชฉ์†Œ๋ฆฌ>|none ๋‚ด๋ ˆ์ด์…˜ ๋ชฉ์†Œ๋ฆฌ(์‡ผ์ธ ยท์„ค๋ช… โ€” ๋ณด๊ธฐ๋Š” ๋กœ์ปฌ Qwen3(KF_QWEN3_TTS_URL ์žˆ์„ ๋•Œ)ยทElevenLabsยท์ž๋ง‰๋งŒยท๊ทธ ๋ฐ– ์ˆœ, ํ‚ค ์—†๋Š” ๋ณด๊ธฐ๋Š” ๋„ฃ๋Š” ๋ฒ•. kf narrate --voices)
81
+ --music <๊ณก ํŒŒ์ผ>|gen:<๊ณต๊ธ‰์ž>|none ๊ณก(์‡ผ์ธ ยท์„ค๋ช…์€ ๋ถ„์œ„๊ธฐ ๋งž๋Š” ๊ณก ๋ชจ์Œ ๊ณก์„ ๋ฌป์ง€ ์•Š๊ณ  ๊ณ ๋ฅด๊ณ , ์—†์„ ๋•Œ๋งŒ ์ƒ์„ฑยท๊ณก ๋„ฃ๊ธฐยท์—†์Œ์„ ๋ฌป๋Š”๋‹ค)
82
+ --allow image,video,โ€ฆ ๊ทธ ๋Šฅ๋ ฅ์€ ๊ถŒ์žฅ ๋‹ต์œผ๋กœ, ๋ชฉ๋ก์— ์—†๋Š” ๋Šฅ๋ ฅ์€ ๋ˆ ์•ˆ ๋“œ๋Š” ๊ธธ๋กœ โ€” ๋ฌป์ง€ ์•Š๋Š”๋‹ค
31
83
 
32
- --model claude-code: ์žฅ๋ฉด ์ง€์‹œ์„œ(MAKE.md)๊นŒ์ง€ ๋งŒ๋“ค๊ณ  ๋ฉˆ์ถ˜๋‹ค โ€” Claude Code ๊ฐ€ ์žฅ๋ฉด์„ ์ฑ„์šฐ๊ณ  --finish
33
- <OG ๋ชจ๋ธ>: OG chat/completions ๋กœ ์žฅ๋ฉด์„ ์“ฐ๊ณ  ๋ Œ๋”ยท๊ฒ€์‚ฌยท๊ณ ์น˜๊ธฐ๊นŒ์ง€(๊ธฐ๋ณธ ${DEFAULT_MODEL})
34
- Claude Code ์•ˆ(CLAUDECODE=1)์—์„œ ๋ถ€๋ฅด๋ฉด ๊ธฐ๋ณธ์ด claude-code
84
+ ๊ฐ๋… ๋ชจ๋“œ(๊ธฐ๋ณธ, ๋ฎค๋น„ ๋นผ๊ณ ): ๋ชจ๋ธ์€ content.json(์žฅ๋ฉด๋งˆ๋‹ค typeยท์นธยท๋“ค๋ฆด ๋งยท์ถœ์ฒ˜)๋งŒ ์“ฐ๊ณ , kf direct ๊ฐ€ ๊ฒ€์ฆ๋œ ์žฅ๋ฉด ๋ฌธ๋ฒ•์„
85
+ ๊ณจ๋ผ ์žฅ๋ฉด ํŒŒ์ผ์„ ์กฐ๋ฆฝํ•œ๋‹ค(LLM ์ด ์žฅ๋ฉด ์ฝ”๋“œ๋ฅผ ์•ˆ ์“ด๋‹ค โ€” ์–ด๋–ค ๋ชจ๋ธ์ด๋“  content.json ๋งŒ ๋งž์œผ๋ฉด ํ™”๋ฉด ํ’ˆ์งˆ์ด ๊ฐ™๋‹ค)
86
+ --no-direct ๊ฐ๋… ์—†์ด ๋ชจ๋ธ์ด ์žฅ๋ฉด ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์“ฐ๋Š” ๊ธธ(๋ฎค๋น„๋Š” ๋Š˜ ์ด ๊ธธ)
87
+ --model claude-code(๊ธฐ๋ณธ, ์–ด๋””์„œ ๋ถ€๋ฅด๋“ ): ์ง€์‹œ์„œ(MAKE.md)๊นŒ์ง€ ๋งŒ๋“ค๊ณ  ๋ฉˆ์ถ˜๋‹ค โ€” ๊ฐ๋… ๋ชจ๋“œ๋ฉด ์„ธ์…˜์ด content.json ์„ ์“ฐ๊ณ 
88
+ kf direct โ†’ --finish, --no-direct ๋ฉด ์„ธ์…˜์ด ์žฅ๋ฉด์„ ์ฑ„์šฐ๊ณ  --finish. OG ๋ฅผ ํ•œ ๋ฒˆ๋„ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค
89
+ <OG ๋ชจ๋ธ>: ์งš์„ ๋•Œ๋งŒ โ€” OG chat/completions ๋กœ content.json(--no-direct ๋ฉด ์žฅ๋ฉด)์„ ์“ฐ๊ณ  ๋ Œ๋”ยท๊ฒ€์‚ฌ๊นŒ์ง€
35
90
  --vision-model ๊ทธ๋ฆผ ๋ณด๋Š” ๊ฒ€์‚ฌ ๋ชจ๋ธ(๊ธฐ๋ณธ none โ€” ์„ธ์…˜์ด kf look ์˜ ์žฅ๋ฉด ๋ชจ์Œยทํ™•์ธํ‘œ๋ฅผ ์ง์ ‘ ๋ณธ๋‹ค). ๊ทธ๋ฆผ์„ ๋ชป ๋ณด๋Š” ์„ธ์…˜๋งŒ ${DEFAULT_VISION} ๊ฐ™์€ ๋ชจ๋ธ์„ ์ค€๋‹ค
91
+ --allow-premium OpusยทGPT ๊ณ„์—ด OG ๋ชจ๋ธ(--modelยท--vision-modelยท--rewrite)์„ ํ‘ผ๋‹ค(KF_ALLOW_PREMIUM=1 ๊ณผ ๊ฐ™๋‹ค). ์—†์œผ๋ฉด ๋ถ€๋ฅด๊ธฐ ์ „์— ๋ง‰๋Š”๋‹ค
92
+ ๋๋‚  ๋•Œ(์„ฑ๊ณตยท์‹คํŒจยท์ค‘๋‹จ) <์ž‘์—… ํด๋”>/make-stats.json ์— ๋‹จ๊ณ„๋ณ„ ๋ชจ๋ธยทmsยทํ† ํฐยทusd ์™€ ํ•ฉ๊ณ„๋ฅผ ๋‚จ๊ธฐ๊ณ  ใ€ŒOG ํ˜ธ์ถœ N๋ฒˆ ยท ํ† ํฐ ยท $ใ€ ํ•œ ์ค„์„ ์ฐ๋Š”๋‹ค
93
+ --series ์‹œ๋ฆฌ์ฆˆ ๋ฌถ์Œ(series/<id>.json) ยท none. ์•ˆ ์ฃผ๋ฉด ์š”์ฒญ์ด ์งš์€ ์‹œ๋ฆฌ์ฆˆ(ใ€ŒOG ์‡ผ์ธ ใ€์ฒ˜๋Ÿผ) โ€” ์šฉ๋„ยทํ…Œ๋งˆยท๋ชฉ์†Œ๋ฆฌยท๊ณกยท์ž๋ง‰ยท๊ธธ์ดยท๋ ์นด๋“œยท์ƒ์„ฑ ๋‹ต์„ ๋ฌป์ง€ ์•Š๊ณ  ๊ทธ ๋ฌถ์Œ๋Œ€๋กœ
36
94
  --purpose ์šฉ๋„๋ฅผ ๋ชป ๋ฐ•๋Š”๋‹ค: ${PURPOSES.join(" ยท ")}
37
- --theme ํ…Œ๋งˆ id ยท auto(๊ธฐ๋ณธ โ€” ์š”์ฒญ์ด ใ€Œ๋ฆฌ์†Œ ๋А๋‚Œใ€์ฒ˜๋Ÿผ ์งš์€ ํ…Œ๋งˆ, ์•„๋‹ˆ๋ฉด ๋‚ด์šฉ์— ๋งž๋Š” approved ํ›„๋ณด ์…‹ ์ค‘ ์ฒซ์งธ. ์ฒซ ์žฅ๋ฉด์„ ์…‹ ๋‹ค ์ฐ์–ด out/theme-<id>.png) ยท none(ํ”„๋ฆฌ์…‹๋งŒ)
38
- --yes ์˜์ƒ ์ƒ์„ฑ์ฒ˜๋Ÿผ ๋ˆ์ด ํฐ ํ˜ธ์ถœ์„ ์Šน์ธํ•œ๋‹ค(์—†์œผ๋ฉด ๊ฒฌ์ ๋งŒ ๋ณด์ด๊ณ  ์ด์Œ๋งค๋Š” ๋Œ€์ฒด ํ™”๋ฉด์œผ๋กœ ๊ฐ„๋‹ค)
95
+ --theme ํ…Œ๋งˆ id ยท auto(์š”์ฒญ์ด ใ€Œ๋ฆฌ์†Œ ๋А๋‚Œใ€์ฒ˜๋Ÿผ ์งš์€ ํ…Œ๋งˆ, ์•„๋‹ˆ๋ฉด ๋‚ด์šฉ์— ๋งž๋Š” approved ํ›„๋ณด ์…‹ ์ค‘ ์ฒซ์งธ. ์ฒซ ์žฅ๋ฉด์„ ์…‹ ๋‹ค ์ฐ์–ด out/theme-<id>.png) ยท none(ํ”„๋ฆฌ์…‹๋งŒ)
96
+ ์•ˆ ์ฃผ๋ฉด(์‹œ๋ฆฌ์ฆˆ๊ฐ€ ์ •ํ•˜์ง€๋„ ์•Š์•˜์œผ๋ฉด) ๋ฌป๋Š”๋‹ค โ€” ๋ณด๊ธฐ๋Š” approved ํ…Œ๋งˆ ์ „๋ถ€(์ƒ๊น€์ƒˆ ํ•œ ์ค„ยท2D/3Dยท๊ฒฌ๋ณธ)์™€ ใ€Œ์•Œ์•„์„œใ€(= auto). kf theme sheet
97
+ --yes ๋ฌป์ง€ ์•Š๊ณ  ๊ถŒ์žฅ ๋‹ต์œผ๋กœ โ€” ๋ˆ์ด ๋“œ๋Š” ๊ทธ๋ฆผยท์˜์ƒ๋„ ์Šน์ธํ•œ๋‹ค(์ง€์—ญ ์ œํ•œ์ด ๊ฑธ๋ฆฐ H3 ๋Š” --video h3-pod ๋กœ ์งš์–ด์•ผ๋งŒ)
39
98
  --max-usd ์˜์ƒ ํ•˜๋‚˜ ์ƒํ•œ(๊ธฐ๋ณธ 1)
40
99
  --video-provider ์˜์ƒ ๊ณต๊ธ‰์ž๋ฅผ ๋ชป ๋ฐ•๋Š”๋‹ค(์˜ˆ: h3-pod โ€” KF_H3_URLยทKF_H3_STEPS ํ•„์š”). ๊ธฐ๋ณธ์€ KF_VIDEO_PROVIDER, ์—†์œผ๋ฉด ์ƒ์„ฑ ๋‹ด๋‹น ๊ธฐ๋ณธ
41
100
  --fix ๊ฒ€์‚ฌ๊ฐ€ ๋–จ์–ด์กŒ์„ ๋•Œ ๊ณ ์น˜๊ธฐ ํšŸ์ˆ˜(๊ธฐ๋ณธ 2)
42
101
  --captions on|off|fit ํ™”๋ฉด ์ž๋ง‰(fit ์€ ์žฅ๋ฉด์„ ์ž๋ง‰ ๋  ์œ„๋กœ ์ค„์—ฌ ์˜ฌ๋ฆฐ๋‹ค) โ€” ์ƒˆ๋กœ ๋งŒ๋“ค ๋• kf.json ์— ์ ๊ณ , --finish ์—์„  ๊ทธ ํŒ๋งŒ ๋ฎ๋Š”๋‹ค(๋‹ค์‹œ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ์ผœ๊ณ  ๋„๊ธฐ). srtยทvtt ๋Š” ๋‘˜ ๋‹ค ๋‚ธ๋‹ค
43
102
  --suffix [๊ธ€] --finish ๊ฒฐ๊ณผ ์ด๋ฆ„ ๋’ค์— ๋ถ™์ธ๋‹ค(๊ฐ’ ์—†์ด๋ฉด -์ž๋ง‰ ยท -์ž๋ง‰์—†์Œ) โ€” ์›๋ณธ video.mp4 ๋ฅผ ๋ฎ์ง€ ์•Š๊ณ  ๋‘˜ ๋‹ค ๋‚จ๊ธด๋‹ค
44
- --no-gen ์ƒ์„ฑ ๋ชจ๋ธ์„ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค. claude-code ๋ชจ๋“œ๋Š” ๋ฌป์ง€ ์•Š๊ณ  ๊ทธ๋ฆผ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค โ€” ์ง€์‹œ์„œ์˜ ใ€Œ๋งŒ๋“ค ๊ทธ๋ฆผใ€์„ ๋ณด๊ณ  ์„ธ์…˜์ด ํ”„๋กฌํ”„ํŠธ๋ฅผ ์จ์„œ kf gen img ๋กœ ๋งŒ๋“ ๋‹ค
103
+ --no-gen ์ƒ์„ฑ ๋ชจ๋ธ์„ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค โ€” ๋ฌป์ง€ ์•Š๊ณ  ๊ทธ๋ฆผยท์˜์ƒ ๋ชจ๋ธ์€ ๋นผ๊ณ , Blender ๊ฐ€ ์—†์œผ๋ฉด three ๋กœ ๋Œ€์‹ ํ•œ๋‹ค
45
104
 
46
105
  ์šฉ๋„๋ฅผ ๋ชป ์ •ํ•˜๋ฉด ํ„ฐ๋ฏธ๋„์—์„œ ๋ฌป๊ณ , ํ„ฐ๋ฏธ๋„์ด ์•„๋‹ˆ๋ฉด ๋ณด๊ธฐ๋ฅผ ์ฐ๊ณ  ์ข…๋ฃŒ ์ฝ”๋“œ 3.`,
47
106
  async run(argv) {
48
- const { args, opts } = parseFlags(argv);
49
- const str = (k: string) => (typeof opts[k] === "string" ? (opts[k] as string) : undefined);
50
- // --finish ๋Š” Claude Code ๋ชจ๋“œ์˜ ๋‘˜์งธ ๋‹จ๊ณ„๋ผ ๋ชจ๋ธ์„ ์•ˆ ์ฃผ๋ฉด claude-code โ€” ์„ธ์…˜์ด ์ง์ ‘ ๊ทธ๋ฆผ์„ ๋ณด๋‹ˆ ๊ทธ๋ฆผ ๊ฒ€์‚ฌ ๋ชจ๋ธ๋„ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค
51
- const model = str("model") ?? (process.env.CLAUDECODE || opts.finish ? "claude-code" : DEFAULT_MODEL);
52
- const vision = str("vision-model") ?? "none";
53
- // OG ๋ชจ๋“œ๋Š” ์žฅ๋ฉด ์“ฐ๊ธฐยท๊ทธ๋ฆผ ๋ณด๊ธฐ๊นŒ์ง€ OG ๋ฅผ ๋ถ€๋ฅธ๋‹ค โ€” ๋ผˆ๋Œ€๋งŒ ๋งŒ๋“ค๊ณ  ์ฃฝ์ง€ ์•Š๊ฒŒ ์ฒ˜์Œ์— ๋ง‰๋Š”๋‹ค
54
- if (model !== "claude-code" && !hasKey("opengateway")) {
55
- console.error(`kf make: OG ํ‚ค๊ฐ€ ์—†๋‹ค โ€” ${whereToPut("opengateway")}. ํ‚ค ์—†์ด๋Š” Claude Code ์•ˆ์—์„œ(๋˜๋Š” --model claude-code) โ€” ์žฅ๋ฉด์€ ์„ธ์…˜์ด ์“ฐ๊ณ  ๊ทธ๋ฆผ ์ƒ์„ฑ์€ ๊ฑด๋„ˆ๋›ด๋‹ค`);
56
- return 2;
107
+ // ์žฅ๋ถ€๋Š” ์–ด๋А ๊ธธ๋กœ ๋๋‚˜๋“ (์„ฑ๊ณตยท์‹คํŒจยทthrowยทCtrl-C) ํ•œ ๋ฒˆ โ€” ์ž‘์—… ํด๋”๊ฐ€ ์„œ๊ธฐ ์ „์— ๋๋‚ฌ์œผ๋ฉด ์ค„๋งŒ ์ฐ๋Š”๋‹ค
108
+ const at: Track = {};
109
+ let code: number | string = "์˜ค๋ฅ˜";
110
+ let closed = false;
111
+ const close = () => {
112
+ if (closed || at.quiet) return;
113
+ closed = true;
114
+ if (at.dir && existsSync(at.dir)) writeStats(at.dir, { command: at.command ?? "make", model: at.model ?? "", code });
115
+ console.log(`\n${spentLine()}`);
116
+ };
117
+ const stop = (sig: NodeJS.Signals) => {
118
+ code = sig;
119
+ close();
120
+ process.exit(sig === "SIGINT" ? 130 : 143);
121
+ };
122
+ process.once("SIGINT", stop);
123
+ process.once("SIGTERM", stop);
124
+ try {
125
+ code = await make(argv, at);
126
+ return code;
127
+ } finally {
128
+ process.off("SIGINT", stop);
129
+ process.off("SIGTERM", stop);
130
+ close();
57
131
  }
58
- const fixes = num(opts.fix, 2);
59
- const captions = str("captions");
60
- if (captions !== undefined && captions !== "on" && captions !== "off" && captions !== "fit") {
61
- console.error(`kf make: --captions ๋Š” onยทoffยทfit โ€” ${captions}`);
132
+ },
133
+ };
134
+ export default cmd;
135
+
136
+ interface Track {
137
+ dir?: string;
138
+ model?: string;
139
+ command?: string;
140
+ quiet?: boolean;
141
+ }
142
+
143
+ async function make(argv: string[], at: Track): Promise<number> {
144
+ const { args, opts } = parseFlags(argv);
145
+ const str = (k: string) => (typeof opts[k] === "string" ? (opts[k] as string) : undefined);
146
+ // OG ๋ชจ๋ธ์€ ์งš์„ ๋•Œ๋งŒ โ€” ์–ด๋””์„œ ๋ถ€๋ฅด๋“ (Claude Code ์•ˆํŒŽยท์ž‘์—…์‹ค) ๊ธฐ๋ณธ์€ ์ง€์‹œ์„œ๊นŒ์ง€ ๋งŒ๋“ค๊ณ  ์„ธ์…˜์ด ์ž‡๋Š” claude-code
147
+ const model = str("model") ?? "claude-code";
148
+ const vision = str("vision-model") ?? "none";
149
+ at.model = model;
150
+ if (opts["allow-premium"]) process.env.KF_ALLOW_PREMIUM = "1";
151
+ for (const m of [model, vision]) {
152
+ const why = m !== "claude-code" && m !== "none" ? premiumBlock(m) : null;
153
+ if (why) {
154
+ console.error(`kf make: ${why}`);
62
155
  return 2;
63
156
  }
64
- const suffix = str("suffix") ?? (opts.suffix ? (captions === "off" ? "-์ž๋ง‰์—†์Œ" : "-์ž๋ง‰") : "");
65
-
66
- if (opts.rewrite) {
67
- const dir = resolve(typeof opts.rewrite === "string" ? opts.rewrite : (args[0] ?? "."));
68
- const ids = (str("scenes") ?? "").split(",").filter(Boolean);
69
- if (!ids.length || model === "claude-code") {
70
- console.error("kf make --rewrite: --scenes ์™€ OG ๋ชจ๋ธ(--model)์ด ํ•„์š”ํ•˜๋‹ค");
71
- return 2;
72
- }
73
- writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
74
- await writeScenes(dir, model, ids);
75
- return finish(dir, { model, vision, fixes });
157
+ }
158
+ // OG ๋ชจ๋“œ๋Š” ์žฅ๋ฉด ์“ฐ๊ธฐยท๊ทธ๋ฆผ ๋ณด๊ธฐ๊นŒ์ง€ OG ๋ฅผ ๋ถ€๋ฅธ๋‹ค โ€” ๋ผˆ๋Œ€๋งŒ ๋งŒ๋“ค๊ณ  ์ฃฝ์ง€ ์•Š๊ฒŒ ์ฒ˜์Œ์— ๋ง‰๋Š”๋‹ค
159
+ if (model !== "claude-code" && !hasKey("opengateway")) {
160
+ console.error(`kf make: OG ํ‚ค๊ฐ€ ์—†๋‹ค โ€” ${whereToPut("opengateway")}. ํ‚ค ์—†์ด๋Š” --model ์„ ๋นผ๊ณ (claude-code) โ€” ์žฅ๋ฉด์€ ์„ธ์…˜์ด ์“ฐ๊ณ  ๊ทธ๋ฆผ ์ƒ์„ฑ์€ ๊ฑด๋„ˆ๋›ด๋‹ค`);
161
+ return 2;
162
+ }
163
+ const fixes = num(opts.fix, 2);
164
+ const captions = str("captions");
165
+ if (captions !== undefined && captions !== "on" && captions !== "off" && captions !== "fit") {
166
+ console.error(`kf make: --captions ๋Š” onยทoffยทfit โ€” ${captions}`);
167
+ return 2;
168
+ }
169
+ const suffix = str("suffix") ?? (opts.suffix ? (captions === "off" ? "-์ž๋ง‰์—†์Œ" : "-์ž๋ง‰") : "");
170
+
171
+ if (opts.answer) {
172
+ at.quiet = true;
173
+ const dir = resolve(typeof opts.answer === "string" ? opts.answer : (args[0] ?? "."));
174
+ return answerProject(dir, opts);
175
+ }
176
+ if (opts.rewrite) {
177
+ const dir = resolve(typeof opts.rewrite === "string" ? opts.rewrite : (args[0] ?? "."));
178
+ Object.assign(at, { dir, command: "rewrite" });
179
+ const ids = (str("scenes") ?? "").split(",").filter(Boolean);
180
+ if (!ids.length) {
181
+ console.error("kf make --rewrite: --scenes a,b ๊ฐ€ ํ•„์š”ํ•˜๋‹ค");
182
+ return 2;
76
183
  }
77
- if (opts.finish) {
78
- const dir = resolve(typeof opts.finish === "string" ? opts.finish : (args[0] ?? "."));
79
- const gen = { approve: !!opts.yes, maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
80
- const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
81
- const studio = await attachStudio({ dir, request: plan.request ?? dir, model, purpose: plan.purpose });
82
- const unmirror = studio?.mirror();
83
- if (studio) console.log(`์ž‘์—…์‹ค: ${studio.url}`);
84
- await studio?.send({ phase: "render" });
85
- const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen, captions, suffix });
86
- await unmirror?.();
87
- await studio?.send({ done: true, ok: code === 0 });
88
- return code;
184
+ // ๊ฐ๋… ํ”„๋กœ์ ํŠธ โ€” ๋ชจ๋ธ ์—†์ด ๊ทธ ์žฅ๋ฉด๋งŒ ๋‹ค์‹œ ์ง“๋Š”๋‹ค(๋‹ค๋ฅธ ์žฅ๋ฉด ํŒŒ์ผ์€ ๋ฐ”์ดํŠธ ๊ทธ๋Œ€๋กœ๋ผ ๊ทธ ์กฐ๊ฐ์€ ๋‹ค์‹œ ์•ˆ ๊ตฝ๋Š”๋‹ค)
185
+ if (isDirect(dir)) {
186
+ const pick = ["pattern", "scene-seed"].flatMap((k) => (str(k) ? [`--${k}`, str(k)!] : []));
187
+ // ๋“ค๋ฆด ๋ง์„ ๊ณ ์ณค์„ ๋•Œ๋งŒ ๋‹ค์‹œ ์ฝํžŒ๋‹ค โ€” ๋ชฉ์†Œ๋ฆฌ๊ฐ€ ์žฅ๋ฉด ๊ธธ์ด๋ฅผ ์ •ํ•ด์„œ ๋’ค ์žฅ๋ฉด ์‹œ๊ฐ์ด ์›€์ง์ธ๋‹ค(๊ทธ ์กฐ๊ฐ๋“ค๋„ ๋‹ค์‹œ ๊ตฝํžŒ๋‹ค)
188
+ const narrate = sayChanged(dir, ids) ? [] : ["--no-narrate"];
189
+ const d = await relay(["direct", dir, "--scenes", ids.join(","), ...narrate, "--no-check", ...pick]);
190
+ if (d !== 0) return d;
191
+ return finish(dir, { model: "direct", vision, fixes: 0, scenes: ids });
89
192
  }
90
-
91
- const request = args.join(" ").trim();
92
- if (!request) {
93
- console.error('kf make: ์š”์ฒญ ๊ธ€์ด ํ•„์š”ํ•˜๋‹ค โ€” kf make "์นด์‚ฌํ…€ ํ™๋ณด ์˜์ƒ 1๋ถ„"');
193
+ // ๋‹ค์‹œ ์“ฐ๊ธฐ๋Š” ๊ทธ ์ผ์„ ๋งŒ๋“  ๋ชจ๋ธ๋กœ โ€” ๊ธฐ๋ณธ ๋ชจ๋ธ๋กœ ๋ชฐ๋ž˜ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค
194
+ const own = str("model") ?? readPlan(dir).model;
195
+ if (!own || own === "claude-code") {
196
+ console.error(`kf make --rewrite: ์žฅ๋ฉด ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์“ธ OG ๋ชจ๋ธ์ด ์—†๋‹ค โ€” --model <OG ๋ชจ๋ธ>๋กœ ์ฃผ๊ฑฐ๋‚˜, ์„ธ์…˜์ด ${ids.join(",")} ์žฅ๋ฉด ํŒŒ์ผ์„ ๊ณ ์น˜๊ณ  kf make --finish ${dir} --scenes ${ids.join(",")}`);
94
197
  return 2;
95
198
  }
96
- const assetFiles = str("assets") ? listFiles(resolve(str("assets")!)) : [];
97
- const given = str("music") ? resolve(str("music")!) : assetFiles.find((f) => AUDIO.test(f));
98
-
99
- let purpose = str("purpose") as Purpose | undefined;
100
- const det = detect({ request, assets: assetFiles, music: given }, listTemplates());
101
- if (!purpose) {
102
- if (det.ambiguous) {
103
- purpose = await ask(det.ranked.slice(0, 4).map((r) => r.purpose));
104
- if (!purpose) {
105
- console.error(`kf make: ์šฉ๋„๋ฅผ ๋ชป ์ •ํ–ˆ๋‹ค(ํ™•์‹  ${det.confidence}). --purpose ๋กœ ๊ณจ๋ผ ๋‹ค์‹œ: ${det.ranked.slice(0, 4).map((r) => `${r.purpose}(${r.p})`).join(" ยท ")}`);
106
- return 3;
107
- }
108
- } else purpose = det.purpose;
199
+ at.model = own;
200
+ // ๊ทธ ์ผ์„ ๋งŒ๋“  ๋ชจ๋ธ์ด ๋น„์‹ผ ๊ณ„์—ด์ด๋ฉด ๋‹ค์‹œ ์“ฐ๊ธฐ๋„ ๊ฐ™์€ ๋ฌธ์—์„œ ๋ง‰๋Š”๋‹ค
201
+ if (premiumBlock(own)) {
202
+ console.error(`kf make --rewrite: ${premiumBlock(own)}`);
203
+ return 2;
109
204
  }
110
- const chosen = det.ranked.find((r) => r.purpose === purpose);
111
- // ๊ณก์„ ์•ˆ ์คฌ์œผ๋ฉด ๊ณก์ด ๋Œ๊ณ  ๊ฐ€๋Š” ์šฉ๋„(ํ™๋ณดยท๊ณต๊ฐœ)์ผ ๋•Œ๋งŒ ๊ณก ๋ชจ์Œ์—์„œ ๊ณ ๋ฅธ๋‹ค โ€” ์‡ผ์ธ ยท์„ค๋ช…์— ์•„๋ฌด ๊ณก์ด๋‚˜ ๊น”๋ฆฌ์ง€ ์•Š๊ฒŒ
112
- const music = given ?? (loadTemplate(purpose).music === "required" ? findLibraryMusic(request) : undefined);
113
- console.error(`์šฉ๋„ ${purpose} (ํ™•์‹  ${chosen?.p ?? 1}${chosen?.reasons.length ? ` โ€” ${chosen.reasons.join(", ")}` : ""})`);
114
-
115
- const dir = resolve(str("out") ?? `${purpose}-${stamp()}`);
116
- const newArgs = [kfBin, "new", dir, "--template", purpose, "--request", request];
117
- if (music) newArgs.push("--music", music);
118
- if (str("title")) newArgs.push("--title", str("title")!);
119
- // ๋ง๋กœ๋งŒ ์‹œ์ž‘ํ•ด๋„ ํ…Œ๋งˆ๊นŒ์ง€ ์•Œ์•„์„œ โ€” ์•ˆ ์ฃผ๋ฉด auto(์š”์ฒญ์ด ์งš์€ ํ…Œ๋งˆ โ†’ ๋‚ด์šฉ์— ๋งž๋Š” approved ํ›„๋ณด). ํ…Œ๋งˆ ์—†์ด ํ”„๋ฆฌ์…‹๋งŒ ์“ฐ๋ ค๋ฉด --theme none
120
- const theme = str("theme") ?? "auto";
121
- if (theme !== "none") newArgs.push("--theme", theme);
122
- if (opts.force) newArgs.push("--force");
123
- const made = spawnSync(process.execPath, newArgs, { stdio: ["ignore", "inherit", "inherit"] });
124
- if (made.status !== 0) return made.status ?? 1;
125
-
126
- const studio = await attachStudio({ dir, request, model, purpose });
205
+ writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
206
+ await writeScenes(dir, own, ids);
207
+ return finish(dir, { model: own, vision, fixes, scenes: ids });
208
+ }
209
+ if (opts.finish) {
210
+ const dir = resolve(typeof opts.finish === "string" ? opts.finish : (args[0] ?? "."));
211
+ Object.assign(at, { dir, command: "finish" });
212
+ const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
213
+ const gen = { approve: !!opts.yes || videoApproved(plan), maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
214
+ const studio = await attachStudio({ dir, request: plan.request ?? dir, model, purpose: plan.purpose });
127
215
  const unmirror = studio?.mirror();
128
216
  if (studio) console.log(`์ž‘์—…์‹ค: ${studio.url}`);
129
- const plan: Plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
130
- // ์ƒˆ ํ”„๋กœ์ ํŠธ๋Š” ์ด ์˜์ƒ์˜ ๊ธฐ๋ณธ์œผ๋กœ ์ ๋Š”๋‹ค โ€” ๊ณ„ํš ๋‹ค๋“ฌ๊ธฐ๊ฐ€ ๋‚ด๋ ˆ์ด์…˜์„ ์“ธ์ง€๋„ ์ด๊ฑธ ๋ณธ๋‹ค
131
- if (captions) {
132
- const kfPath = join(dir, "kf.json");
133
- writeJson(kfPath, { ...JSON.parse(readFileSync(kfPath, "utf8")), captions });
134
- plan.captions = captionMode(captions);
135
- }
136
- if (!str("purpose")) {
137
- plan.confidence = chosen?.p ?? det.confidence;
138
- plan.alternatives = det.ranked.filter((r) => r.purpose !== purpose).slice(0, 2).map((r) => ({ purpose: r.purpose, p: r.p }));
139
- plan.reasons = [...(chosen?.reasons ?? []), ...plan.reasons.filter((r) => r !== "์‚ฌ๋žŒ์ด ์ •ํ•จ")];
140
- }
141
- if (assetFiles.length) {
142
- mkdirSync(join(dir, "assets", "in"), { recursive: true });
143
- for (const f of assetFiles) if (!AUDIO.test(f)) copyFileSync(f, join(dir, "assets", "in", basename(f)));
217
+ await studio?.send({ phase: "render" });
218
+ const scenes = str("scenes")?.split(",").filter(Boolean);
219
+ const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen, captions, suffix, ...(scenes?.length ? { scenes } : {}) });
220
+ await unmirror?.();
221
+ await studio?.send({ done: true, ok: code === 0 });
222
+ return code;
223
+ }
224
+
225
+ const request = args.join(" ").trim();
226
+ if (!request) {
227
+ console.error('kf make: ์š”์ฒญ ๊ธ€์ด ํ•„์š”ํ•˜๋‹ค โ€” kf make "์นด์‚ฌํ…€ ํ™๋ณด ์˜์ƒ 1๋ถ„"');
228
+ return 2;
229
+ }
230
+ const assetFiles = str("assets") ? listFiles(resolve(str("assets")!)) : [];
231
+ // ์‹œ๋ฆฌ์ฆˆ๊ฐ€ ๋จผ์ € โ€” ๊ฐ™์€ ์‹œ๋ฆฌ์ฆˆ๋ฉด ๋งค๋ฒˆ ๊ฐ™์€ ๋ฌถ์Œ. ์‚ฌ๋žŒ์ด ๋”ฐ๋กœ ์ค€ ํ”Œ๋ž˜๊ทธ๋Š” ๊ทธ๋Œ€๋กœ ์ด๊ธด๋‹ค
232
+ const series: Series | null = str("series") === "none" ? null : str("series") ? loadSeries(str("series")!) : seriesFromRequest(request);
233
+ if (series) console.error(`์‹œ๋ฆฌ์ฆˆ ${series.id}(${series.label}) โ€” ์šฉ๋„ยทํ…Œ๋งˆยท๋ชฉ์†Œ๋ฆฌยท๊ณกยท์ž๋ง‰ยท๊ธธ์ดยท๋ ์นด๋“œ๋ฅผ ๋ฌถ์Œ๋Œ€๋กœ`);
234
+ // --music ์€ ๊ณก ํŒŒ์ผ, ์•„๋‹ˆ๋ฉด ์Œ์•… ์งˆ๋ฌธ์˜ ๋‹ต(gen:<๊ณต๊ธ‰์ž>ยทnone) โ€” ๋‹ต์€ ํŒŒ์ผ๋กœ ์ฝ์ง€ ์•Š๋Š”๋‹ค
235
+ const musicFlag = str("music");
236
+ const musicAnswer = musicFlag !== undefined && /^(gen:|none$)/.test(musicFlag);
237
+ const given = musicFlag && !musicAnswer ? resolve(musicFlag) : (assetFiles.find((f) => AUDIO.test(f)) ?? (series?.music?.file ? resolve(repo, series.music.file) : undefined));
238
+
239
+ let purpose = (str("purpose") ?? series?.purpose) as Purpose | undefined;
240
+ const det = detect({ request, assets: assetFiles, music: given }, listTemplates());
241
+ if (!purpose) {
242
+ if (det.ambiguous) {
243
+ purpose = await ask(det.ranked.slice(0, 4).map((r) => r.purpose));
244
+ if (!purpose) {
245
+ console.error(`kf make: ์šฉ๋„๋ฅผ ๋ชป ์ •ํ–ˆ๋‹ค(ํ™•์‹  ${det.confidence}). --purpose ๋กœ ๊ณจ๋ผ ๋‹ค์‹œ: ${det.ranked.slice(0, 4).map((r) => `${r.purpose}(${r.p})`).join(" ยท ")}`);
246
+ return 3;
247
+ }
248
+ } else purpose = det.purpose;
249
+ }
250
+ const chosen = det.ranked.find((r) => r.purpose === purpose);
251
+ // ๊ณก์„ ์•ˆ ์คฌ์œผ๋ฉด ๊ณก์ด ๋Œ๊ณ  ๊ฐ€๋Š” ์šฉ๋„(ํ™๋ณดยท๊ณต๊ฐœ)์ผ ๋•Œ๋งŒ ๊ณก ๋ชจ์Œ์—์„œ ๊ณ ๋ฅธ๋‹ค โ€” ์‡ผ์ธ ยท์„ค๋ช…์— ์•„๋ฌด ๊ณก์ด๋‚˜ ๊น”๋ฆฌ์ง€ ์•Š๊ฒŒ
252
+ // ๋ถ„์œ„๊ธฐ๋ฅผ ์ •ํ•œ ํ‹€(์‡ผ์ธ ยท์„ค๋ช…)์€ ๊ณก์„ ๋ฌป๊ธฐ ํ‹€์ด ๊ณ ๋ฅธ๋‹ค(๋ถ„์œ„๊ธฐ ๋งž๋Š” ๊ณก ๋ชจ์Œ ๊ณก โ†’ ์ƒ์„ฑ โ†’ ์—†์Œ)
253
+ const tplOf = loadTemplate(purpose);
254
+ const music = given ?? (!musicAnswer && tplOf.music === "required" && !tplOf.mood ? findLibraryMusic(request) : undefined);
255
+ console.error(`์šฉ๋„ ${purpose} (${series && !str("purpose") ? `์‹œ๋ฆฌ์ฆˆ ${series.id}` : `ํ™•์‹  ${chosen?.p ?? 1}${chosen?.reasons.length ? ` โ€” ${chosen.reasons.join(", ")}` : ""}`})`);
256
+
257
+ // ๋งŒ๋“ค๊ธฐ ์ „์— ๋ฌป๋Š”๋‹ค โ€” ๋ผˆ๋Œ€๋ฅผ ๋งŒ๋“ค๊ณ  ๋‚˜์„œ ๋ฉˆ์ถ”๋ฉด ๋ฐ˜์ฏค ๋งŒ๋“  ํด๋”๊ฐ€ ๋‚จ๋Š”๋‹ค
258
+ const caps = await capabilities({ probe: true });
259
+ const draft = makePlan({ request, purpose, assets: assetFiles, music: given ?? null, series: series?.id ?? "none" });
260
+ const { questions, auto } = planQuestions(draft, caps);
261
+ const flags = { ...seriesAnswers(series), ...opts };
262
+ const settled = settle(questions, { given: flags, yes: !!opts.yes, noGen: !!opts["no-gen"], allow: parseAllow(opts.allow) });
263
+ // ๋ฌป์ง€ ์•Š๊ณ  ์ •ํ•œ ๋‹ต(์žˆ๋Š” ๊ณต์งœ ๋Šฅ๋ ฅ)๋„ ์‚ฌ๋žŒ์ด ์ค€ ํ”Œ๋ž˜๊ทธยท์‹œ๋ฆฌ์ฆˆ ๋‹ต์ด ์ด๊ธด๋‹ค. --no-gen ์ด๋ฉด ๊ณต์งœ๋ผ๋„ ์ƒ์„ฑ ๋ชจ๋ธ(๊ทธ๋ฆผยท์˜์ƒ)์€ ๋บ€๋‹ค
264
+ const forced = Object.fromEntries(
265
+ Object.keys(auto).flatMap((id) => (typeof flags[id] === "string" ? [[id, flags[id] as string]] : opts["no-gen"] && (id === "image" || id === "video") ? [[id, "none"]] : [])),
266
+ );
267
+ let answers: Record<string, string> = { ...auto, ...forced, ...settled.answers };
268
+ let open = settled.open;
269
+ // ๋ฐ•์žยท๊ฐ€์‚ฌ ๋„๊ตฌ๋Š” ๋ผˆ๋Œ€๋ฅผ ์„ธ์šธ ๋•Œ ์“ด๋‹ค โ€” ์—†์œผ๋ฉด ์กฐ์šฉํžˆ ๋ฐ• ์—†์ดยท๋‚ฑ๋ง๋กœ ๊ฐ€์ง€ ์•Š๊ณ , ๋ชจ๋“œ์™€ ์ƒ๊ด€์—†์ด ๋ผˆ๋Œ€ ์ „์— ๋‹ต์„ ๋ฐ›๋Š”๋‹ค
270
+ const willSong = !!(music ?? (answers.music && !/^(gen:|none$|wait$)/.test(answers.music) ? answers.music : undefined));
271
+ const tools = toolQuestions(purpose, willSong);
272
+ const toolSettled = settle(tools, { given: flags, yes: !!opts.yes, noGen: !!opts["no-gen"], allow: parseAllow(opts.allow) });
273
+ questions.push(...tools);
274
+ answers = { ...answers, ...toolSettled.answers };
275
+ Object.assign(settled.how, toolSettled.how);
276
+ if (toolSettled.open.length) {
277
+ const asked = await askTerminal(toolSettled.open);
278
+ if (!asked) {
279
+ console.error(`kf make: ๋ผˆ๋Œ€ ์ „์— ์ •ํ•  ๊ฒƒ์ด ${toolSettled.open.length}๊ฐ€์ง€ โ€” ์‚ฌ๋žŒ์—๊ฒŒ ๋ฌป๊ณ  ํ”Œ๋ž˜๊ทธ๋กœ ๋‹ตํ•ด ๋‹ค์‹œ ๋ถ€๋ฅธ๋‹ค\n\n${questionsText(toolSettled.open)}`);
280
+ return 3;
144
281
  }
145
- writeJson(join(dir, "plan.json"), plan);
146
- if (plan.theme) {
147
- const pick = plan.themeCandidates?.find((c) => c.id === plan.theme);
148
- console.error(`ํ…Œ๋งˆ ${plan.theme}${pick ? ` โ€” ${pick.why}` : ""}${plan.themeCandidates && plan.themeCandidates.length > 1 ? ` (๋‹ค๋ฅธ ํ›„๋ณด: ${plan.themeCandidates.filter((c) => c.id !== plan.theme).map((c) => c.id).join(" ยท ")})` : ""}`);
282
+ answers = { ...answers, ...asked };
283
+ }
284
+ if (answers.beats === "none" && answers.music && !/^(gen:|none$)/.test(answers.music)) answers.music = "none";
285
+ if (open.length && model !== "claude-code") {
286
+ const asked = await askTerminal(open);
287
+ if (!asked) {
288
+ console.error(`kf make: ๋งŒ๋“ค๊ธฐ ์ „์— ์ •ํ•  ๊ฒƒ์ด ${open.length}๊ฐ€์ง€ โ€” ํ”Œ๋ž˜๊ทธ๋กœ ๋‹ตํ•ด ๋‹ค์‹œ ๋ถ€๋ฅธ๋‹ค(--yes ๋ฉด ๊ถŒ์žฅ, --no-gen ์ด๋ฉด ๋ˆ ์•ˆ ๋“œ๋Š” ๊ธธ)\n\n${questionsText(open)}`);
289
+ return 3;
149
290
  }
150
- if (plan.themeCandidates && plan.themeCandidates.length > 1) themeStills(dir, plan.themeCandidates.map((c) => c.id));
291
+ answers = { ...answers, ...asked };
292
+ open = [];
293
+ }
294
+ for (const [id, v] of Object.entries(answers)) console.error(`๋Šฅ๋ ฅ ${id}: ${v}(${settled.how[id] === "flag" || forced[id] ? "ํ”Œ๋ž˜๊ทธ" : settled.how[id] ? `--${settled.how[id]}` : auto[id] ? "์žˆ์–ด์„œ ๋ฐ”๋กœ" : "๋ฌผ์–ด์„œ"})`);
295
+ const stops = questions.filter((q) => answers[q.id] === WAIT);
296
+ if (stops.length) {
297
+ console.error(`kf make: ์ผœ๊ณ  ๋‹ค์‹œ ๋ถ€๋ฅธ๋‹ค โ€”\n${stops.map((q) => ` ${q.ask}: ${q.choices.find((c) => c.value === WAIT)?.note}`).join("\n")}`);
298
+ return 3;
299
+ }
151
300
 
152
- if (model !== "claude-code") await refinePlan(dir, model, request);
301
+ const dir = resolve(str("out") ?? `${purpose}-${stamp()}`);
302
+ Object.assign(at, { dir, command: "make" });
303
+ const newArgs = [kfBin, "new", dir, "--template", purpose, "--request", request, "--series", series?.id ?? "none"];
304
+ if (series?.end?.line) newArgs.push("--line", series.end.line);
305
+ // ๋ฐ•์ž ๋ถ„์„์„ ๋นผ๊ธฐ๋กœ ํ–ˆ์œผ๋ฉด ๊ณก๋„ ๊น”์ง€ ์•Š๋Š”๋‹ค โ€” ๋ฐ• ์—†์ด ๊ณก๋งŒ ๊นŒ๋Š” ๊ธธ์€ ์—†๋‹ค
306
+ const song = answers.beats === "none" ? undefined : (music ?? (answers.music && !/^(gen:|none$|wait$)/.test(answers.music) ? answers.music : undefined));
307
+ if (song) newArgs.push("--music", song);
308
+ if (str("title")) newArgs.push("--title", str("title")!);
309
+ // ํ…Œ๋งˆ ์งˆ๋ฌธ์˜ ๋‹ต(ํ”Œ๋ž˜๊ทธยทํ„ฐ๋ฏธ๋„) โ†’ ์•„์ง ๋ฌป๋Š” ์ค‘์ด๊ฑฐ๋‚˜ ๋ฌป์ง€ ์•Š์€ ํ…Œ๋งˆ(์‹œ๋ฆฌ์ฆˆยท์š”์ฒญ์ด ์งš์€ ๊ฒƒ)๋Š” auto. ํ…Œ๋งˆ ์—†์ด ํ”„๋ฆฌ์…‹๋งŒ ์“ฐ๋ ค๋ฉด --theme none
310
+ newArgs.push("--theme", answers.theme && answers.theme !== WAIT ? answers.theme : (str("theme") ?? "auto"));
311
+ if (opts.force) newArgs.push("--force");
312
+ const made = spawnSync(process.execPath, newArgs, { stdio: ["ignore", "inherit", "inherit"] });
313
+ if (made.status !== 0) return made.status ?? 1;
153
314
 
154
- const gen = { approve: !!opts.yes, maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
155
- // claude-code ๋ชจ๋“œ๋Š” ๋ˆ์ด ๋“œ๋Š” ๊ทธ๋ฆผ์„ ๋ฌป์ง€ ์•Š๊ณ  ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค โ€” ๋ง‰์—ฐํ•œ ์ž๋™ ํ”„๋กฌํ”„ํŠธ๋กœ ์—‰๋šฑํ•œ ๊ทธ๋ฆผ์ด ๋‚˜์™”๋‹ค. ์ง€์‹œ์„œ์— ๋ชฉ๋ก์œผ๋กœ ๋‚ธ๋‹ค
156
- if (!opts["no-gen"] && model !== "claude-code") await genImages(dir);
315
+ const studio = await attachStudio({ dir, request, model, purpose });
316
+ const unmirror = studio?.mirror();
317
+ if (studio) console.log(`์ž‘์—…์‹ค: ${studio.url}`);
318
+ const plan: Plan = { ...applyAnswers(JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")), answers).plan, asks: open };
319
+ // ์ƒˆ ํ”„๋กœ์ ํŠธ๋Š” ์ด ์˜์ƒ์˜ ๊ธฐ๋ณธ์œผ๋กœ ์ ๋Š”๋‹ค โ€” ๊ณ„ํš ๋‹ค๋“ฌ๊ธฐ๊ฐ€ ๋‚ด๋ ˆ์ด์…˜์„ ์“ธ์ง€๋„ ์ด๊ฑธ ๋ณธ๋‹ค
320
+ if (captions) {
321
+ const kfPath = join(dir, "kf.json");
322
+ writeJson(kfPath, { ...JSON.parse(readFileSync(kfPath, "utf8")), captions });
323
+ plan.captions = captionMode(captions);
324
+ }
325
+ if (!str("purpose") && !series) {
326
+ plan.confidence = chosen?.p ?? det.confidence;
327
+ plan.alternatives = det.ranked.filter((r) => r.purpose !== purpose).slice(0, 2).map((r) => ({ purpose: r.purpose, p: r.p }));
328
+ plan.reasons = [...(chosen?.reasons ?? []), ...plan.reasons.filter((r) => r !== "์‚ฌ๋žŒ์ด ์ •ํ•จ")];
329
+ }
330
+ // ์žฅ๋ฉด ๋‹ค์‹œ ์“ฐ๊ธฐ(--rewrite)๊ฐ€ ๊ฐ™์€ ๋ชจ๋ธ์„ ๋ถ€๋ฅธ๋‹ค
331
+ (plan as Plan & { model?: string }).model = model;
332
+ if (assetFiles.length) {
333
+ mkdirSync(join(dir, "assets", "in"), { recursive: true });
334
+ for (const f of assetFiles) if (!AUDIO.test(f)) copyFileSync(f, join(dir, "assets", "in", basename(f)));
335
+ }
336
+ writeJson(join(dir, "plan.json"), plan);
337
+ if (plan.theme) {
338
+ const pick = plan.themeCandidates?.find((c) => c.id === plan.theme);
339
+ console.error(`ํ…Œ๋งˆ ${plan.theme}${pick ? ` โ€” ${pick.why}` : ""}${plan.themeCandidates && plan.themeCandidates.length > 1 ? ` (๋‹ค๋ฅธ ํ›„๋ณด: ${plan.themeCandidates.filter((c) => c.id !== plan.theme).map((c) => c.id).join(" ยท ")})` : ""}`);
340
+ }
341
+ if (plan.themeCandidates && plan.themeCandidates.length > 1) themeStills(dir, plan.themeCandidates.map((c) => c.id));
157
342
 
158
- writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
343
+ // ๊ฐ๋… ๋ชจ๋“œ โ€” ๋ฎค๋น„๋Š” ๊ฐ€์‚ฌ ์ธต์ด ํ™”๋ฉด์„ ๋งก์•„ ์žฅ๋ฉด ๋ฌธ๋ฒ•์„ ์•ˆ ์“ด๋‹ค
344
+ const direct = !opts["no-direct"] && purpose !== "mv";
345
+ if (direct) {
346
+ writeJson(join(dir, "plan.json"), { ...plan, direct: true });
347
+ writeFileSync(join(dir, "MAKE.md"), directBrief(dir));
159
348
  if (model === "claude-code") {
160
- const flags = [gen.approve ? "--yes" : "", gen.videoProvider ? `--video-provider ${gen.videoProvider}` : ""].filter(Boolean).join(" ");
161
- console.log(`\n${dir}/MAKE.md ๋ฅผ ์ฝ๊ณ  scenes/*.ts ๋ฅผ ์ฑ„์šด๋‹ค. ๋‹ค ์ฑ„์šฐ๋ฉด(์ด์Œ๋งค ์˜์ƒ์€ ๊ทธ๋•Œ ์•ž๋’ค ์žฅ๋ฉด ํ”„๋ ˆ์ž„์œผ๋กœ ๋งŒ๋“ ๋‹ค):\n kf make --finish ${dir}${flags ? ` ${flags}` : ""}`);
162
- // ์žฅ๋ฉด์€ ์„ธ์…˜์ด ์“ด๋‹ค โ€” ์ž‘์—…์‹ค ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ๋”ฐ๋ผ์˜จ๋‹ค
163
- console.error("์žฅ๋ฉด ์“ฐ๋Š” ์ค‘(์„ธ์…˜) โ€” ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ์ž‘์—…์‹ค ๋ฏธ๋ฆฌ๋ณด๊ธฐ์— ๋ฐ”๋กœ ๋ณด์ธ๋‹ค");
349
+ if (open.length) console.log(`\n๋จผ์ € ์‚ฌ๋žŒ์—๊ฒŒ ๋ฌผ์„ ๊ฒƒ ${open.length}๊ฐ€์ง€ โ€” ${dir}/MAKE.md ๋งจ ์œ„ ใ€Œ์‚ฌ์šฉ์ž์—๊ฒŒ ๋จผ์ € ๋ฌผ์„ ๊ฒƒใ€(AskUserQuestion), ๋‹ต์€ kf make --answer ${dir} ${open.map((q) => `${q.flag} <๊ฐ’>`).join(" ")}`);
350
+ console.log(`\n๊ฐ๋… ๋ชจ๋“œ โ€” ${dir}/MAKE.md ๋ฅผ ์ฝ๊ณ  ${dir}/content.json ์„ ์“ด๋‹ค(์žฅ๋ฉด ์ฝ”๋“œ๋Š” ์•ˆ ์“ด๋‹ค). ๊ทธ๋‹ค์Œ:\n kf direct ${dir}\n kf look ${dir}\n kf make --finish ${dir}`);
164
351
  await unmirror?.();
165
352
  await studio?.send({ phase: "scenes" });
166
353
  return 0;
167
354
  }
168
- // ์ด์Œ๋งค๋Š” ํ‹€์˜ 3D ์ „ํ™˜์ด ๋งก๋Š”๋‹ค โ€” ๋ชจ๋ธ์ด ๋‹ค์‹œ ์“ฐ์ง€ ์•Š๋Š”๋‹ค
169
- await writeScenes(dir, model, readProject(dir).scenes.filter((s) => s.role !== "seam").map((s) => s.id));
355
+ if (!(await writeContent(dir, model))) {
356
+ console.error(`kf make: ${model} ์ด ๋งž๋Š” content.json ์„ ๋ชป ๋ƒˆ๋‹ค โ€” ${dir}/content.json ์„ ์†๋ณด๊ณ  kf direct ${dir}`);
357
+ return 1;
358
+ }
359
+ const d = spawnSync(process.execPath, [kfBin, "direct", dir], { stdio: ["ignore", "inherit", "inherit"] });
360
+ if (d.status !== 0 && d.status !== 1) return d.status ?? 1;
361
+ const gen = { approve: !!opts.yes || videoApproved(plan), maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
170
362
  const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen });
171
363
  await unmirror?.();
172
364
  await studio?.send({ done: true, ok: code === 0 });
173
365
  return code;
174
- },
175
- };
176
- export default cmd;
366
+ }
367
+
368
+ if (model !== "claude-code") {
369
+ await refinePlan(dir, model, request);
370
+ // ๋‚ด๋ ˆ์ด์…˜ ๋จผ์ € โ€” ๊ณ„ํš ๋‹ค๋“ฌ๊ธฐ๊ฐ€ ์“ด ์žฅ๋ฉด๋ณ„ ๋ฌธ์žฅ์„ ์ฝํ˜€ ์žฅ๋ฉด ๊ธธ์ด๋ฅผ ๋ชฉ์†Œ๋ฆฌ์— ๋งž์ถ˜ ๋’ค ์žฅ๋ฉด์„ ์“ด๋‹ค
371
+ const code = narrateStep(dir);
372
+ if (code !== 0) return code;
373
+ }
374
+
375
+ const gen = { approve: !!opts.yes || videoApproved(plan), maxUsd: num(opts["max-usd"], 1), videoProvider: str("video-provider") ?? process.env.KF_VIDEO_PROVIDER };
376
+ // claude-code ๋ชจ๋“œ๋Š” ๊ทธ๋ฆผยท3D ์ƒท์„ ์„ธ์…˜์ด ๋งŒ๋“ ๋‹ค โ€” ๋ง‰์—ฐํ•œ ์ž๋™ ํ”„๋กฌํ”„ํŠธ๋กœ ์—‰๋šฑํ•œ ๊ทธ๋ฆผ์ด ๋‚˜์™”๋‹ค. ์ง€์‹œ์„œ์— ๋ชฉ๋ก๊ณผ ๊ณ ๋ฅธ ๊ณต๊ธ‰์ž๋กœ ๋‚ธ๋‹ค
377
+ if (model !== "claude-code") {
378
+ await genImages(dir);
379
+ await writeShots(dir, model);
380
+ }
381
+
382
+ writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
383
+ if (model === "claude-code") {
384
+ const flags = [opts.yes ? "--yes" : "", gen.videoProvider ? `--video-provider ${gen.videoProvider}` : ""].filter(Boolean).join(" ");
385
+ if (open.length) console.log(`\n๋จผ์ € ์‚ฌ๋žŒ์—๊ฒŒ ๋ฌผ์„ ๊ฒƒ ${open.length}๊ฐ€์ง€ โ€” ${dir}/MAKE.md ๋งจ ์œ„ ใ€Œ์‚ฌ์šฉ์ž์—๊ฒŒ ๋จผ์ € ๋ฌผ์„ ๊ฒƒใ€(AskUserQuestion), ๋‹ต์€ kf make --answer ${dir} ${open.map((q) => `${q.flag} <๊ฐ’>`).join(" ")}`);
386
+ console.log(`\n${dir}/MAKE.md ๋ฅผ ์ฝ๊ณ  scenes/*.ts ๋ฅผ ์ฑ„์šด๋‹ค. ๋‹ค ์ฑ„์šฐ๋ฉด(์ด์Œ๋งค ์˜์ƒ์€ ๊ทธ๋•Œ ์•ž๋’ค ์žฅ๋ฉด ํ”„๋ ˆ์ž„์œผ๋กœ ๋งŒ๋“ ๋‹ค):\n kf make --finish ${dir}${flags ? ` ${flags}` : ""}`);
387
+ // ์žฅ๋ฉด์€ ์„ธ์…˜์ด ์“ด๋‹ค โ€” ์ž‘์—…์‹ค ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ๋”ฐ๋ผ์˜จ๋‹ค
388
+ console.error("์žฅ๋ฉด ์“ฐ๋Š” ์ค‘(์„ธ์…˜) โ€” ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ์ž‘์—…์‹ค ๋ฏธ๋ฆฌ๋ณด๊ธฐ์— ๋ฐ”๋กœ ๋ณด์ธ๋‹ค");
389
+ await unmirror?.();
390
+ await studio?.send({ phase: "scenes" });
391
+ return 0;
392
+ }
393
+ // ์ด์Œ๋งค๋Š” ํ‹€์˜ 3D ์ „ํ™˜์ด ๋งก๋Š”๋‹ค โ€” ๋ชจ๋ธ์ด ๋‹ค์‹œ ์“ฐ์ง€ ์•Š๋Š”๋‹ค
394
+ await writeScenes(dir, model, readProject(dir).scenes.filter((s) => s.role !== "seam").map((s) => s.id));
395
+ const code = await finish(dir, { model, vision, fixes, gen: opts["no-gen"] ? undefined : gen });
396
+ await unmirror?.();
397
+ await studio?.send({ done: true, ok: code === 0 });
398
+ return code;
399
+ }
400
+
401
+ // --- ๊ฐ๋… ๋ชจ๋“œ: ๋ชจ๋ธ์€ content.json ํ•˜๋‚˜๋งŒ โ€” ์นธ์ด ํ‹€๋ฆฌ๋ฉด ์–ด๋А ์นธ์ธ์ง€ ๋Œ๋ ค์ค˜ ๋‘ ๋ฒˆ๊นŒ์ง€ ๋‹ค์‹œ
402
+
403
+ function isDirect(dir: string): boolean {
404
+ const p = join(dir, "plan.json");
405
+ return existsSync(p) && JSON.parse(readFileSync(p, "utf8")).direct === true;
406
+ }
407
+
408
+ async function writeContent(dir: string, model: string): Promise<boolean> {
409
+ const plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
410
+ let fix: string | undefined;
411
+ for (let attempt = 0; attempt < 3; attempt++) {
412
+ // ์ƒ๊ฐํ•˜๋Š” ๋ชจ๋ธ์€ ์ƒ๊ฐ์— ํ† ํฐ์„ ๋จผ์ € ์“ด๋‹ค โ€” ์ƒํ•œ์ด ๋‚ฎ์œผ๋ฉด ๋‹ต JSON ์ด ์ž˜๋ฆฐ๋‹ค
413
+ const r = await chat(model, contentPrompt(dir, fix), { json: true, temperature: 0.5, maxTokens: 16000, stage: "content" });
414
+ const j = jsonBlock<Record<string, unknown>>(r.text);
415
+ if (!j) {
416
+ fix = "JSON ์„ ๋ชป ์ฝ์—ˆ๋‹ค โ€” JSON ํ•˜๋‚˜๋กœ๋งŒ ๋‹ตํ•œ๋‹ค";
417
+ continue;
418
+ }
419
+ const c = checkContent({ ...j, request: plan.request });
420
+ console.error(`content.json ${attempt + 1}๋ฒˆ์งธ(${model}, ${(r.ms / 1000).toFixed(1)}์ดˆ) โ€” ${c.errors.length ? `์นธ ์˜ค๋ฅ˜ ${c.errors.length}` : `์žฅ๋ฉด ${c.content.scenes.length}`}`);
421
+ if (c.errors.length) {
422
+ fix = c.errors.map((e) => `${e.at}: ${e.msg}`).join("\n");
423
+ continue;
424
+ }
425
+ writeJson(join(dir, "content.json"), { ...j, request: plan.request });
426
+ return true;
427
+ }
428
+ return false;
429
+ }
177
430
 
178
431
  // --- ๊ณ„ํš ๋‹ค๋“ฌ๊ธฐ: ํ‹€์ด ์ •ํ•œ ๋ผˆ๋Œ€(์‹œ๊ฐยท์—ญํ• )๋Š” ๊ทธ๋Œ€๋กœ, ์ด๋ฆ„ยท๋ ํ•œ ์ค„ยท์žฅ๋ฉด ํ•  ์ผยท์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ๋งŒ ์š”์ฒญ์— ๋งž์ถ˜๋‹ค
179
432
 
@@ -193,12 +446,12 @@ async function refinePlan(dir: string, model: string, request: string): Promise<
193
446
  ์žฅ๋ฉด: ${JSON.stringify(plan.scenes.map((s) => ({ id: s.id, role: s.role, dur: s.dur, idea: s.idea })))}
194
447
  ์ƒ์„ฑ ์ž์‚ฐ: ${JSON.stringify(plan.gen.map((g) => ({ id: g.id, kind: g.kind, scene: g.scene, prompt: g.prompt })))}
195
448
 
196
- ๋‹ค์Œ ๊ผด๋กœ: {"title": "์ œํ’ˆยท์ฃผ์ œ ์ด๋ฆ„(์งง๊ฒŒ)", "line": "๋ ์นด๋“œ ํ•œ ์ค„(์—†์œผ๋ฉด ๋นˆ ๊ธ€)", "scenes": [{"id": "...", "idea": "์ด ์š”์ฒญ์— ๋งž์ถ˜ ๊ตฌ์ฒด์ ์ธ ํ™”๋ฉดยท์›€์ง์ž„ ํ•œ๋‘ ๋ฌธ์žฅ"}], "gen": [{"id": "...", "prompt": "์˜์–ด ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ, ๊ธ€์žยท๋กœ๊ณ  ์—†์ด"}]${captionMode(kf.captions) !== "none" ? ', "captions": [{"scene": "์žฅ๋ฉด id", "text": "๊ทธ ์žฅ๋ฉด์—์„œ ๋“ค๋ฆด ํ•œ๊ตญ์–ด ๋‚ด๋ ˆ์ด์…˜ โ€” ์งง์€ ๋งˆ๋””๋ฅผ ์‰ผํ‘œ๋กœ ์ด์–ด ์žฅ๋ฉด ๊ธธ์ด์— ๋งž๊ฒŒ(1์ดˆ์— 4~5๊ธ€์ž)"}]' : ""}}`,
449
+ ๋‹ค์Œ ๊ผด๋กœ: {"title": "์ œํ’ˆยท์ฃผ์ œ ์ด๋ฆ„(์งง๊ฒŒ)", "line": "๋ ์นด๋“œ ํ•œ ์ค„(์—†์œผ๋ฉด ๋นˆ ๊ธ€)", "scenes": [{"id": "...", "idea": "์ด ์š”์ฒญ์— ๋งž์ถ˜ ๊ตฌ์ฒด์ ์ธ ํ™”๋ฉดยท์›€์ง์ž„ ํ•œ๋‘ ๋ฌธ์žฅ"}], "gen": [{"id": "...", "prompt": "์˜์–ด ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ, ๊ธ€์žยท๋กœ๊ณ  ์—†์ด"}]${captionMode(kf.captions) !== "none" ? ', "captions": [{"scene": "์žฅ๋ฉด id", "text": "๊ทธ ์žฅ๋ฉด์—์„œ ๋“ค๋ฆด ํ•œ๊ตญ์–ด ๋‚ด๋ ˆ์ด์…˜ โ€” ์ฐจ๋ถ„ํ•œ ์„ค๋ช…์ฒด, ์งง์€ ๋งˆ๋””๋ฅผ ์‰ผํ‘œ๋กœ, ๋ฌธ์žฅ์€ ๋งˆ์นจํ‘œ๋กœ. ๋ชฉ์†Œ๋ฆฌ๊ฐ€ ์žฅ๋ฉด ๊ธธ์ด๋ฅผ ์ •ํ•˜๋‹ˆ 1์ดˆ์— 4โ€“5๊ธ€์ž(์ฐจ๋ถ„ํ•œ ๋‚ญ๋…)๋กœ ์žฅ๋ฉด ๊ธธ์ด์— ๊ฐ€๊น๊ฒŒ, ํ›…์€ ํ•œ ๋งˆ๋””(๋ ์žฅ๋ฉดยท์ด์Œ๋งค๋Š” ๋น„์›Œ๋„ ๋œ๋‹ค)"}]' : ""}}`,
197
450
  },
198
451
  ];
199
452
  try {
200
453
  // ์ƒ๊ฐํ•˜๋Š” ๋ชจ๋ธ์€ ์ƒ๊ฐ์— ํ† ํฐ์„ ๋จผ์ € ์“ด๋‹ค โ€” ์ƒํ•œ์ด ๋‚ฎ์œผ๋ฉด ๋‹ต JSON ์ด ์ž˜๋ฆฐ๋‹ค
201
- const r = await chat(model, ask, { json: true, temperature: 0.5, maxTokens: 16000 });
454
+ const r = await chat(model, ask, { json: true, temperature: 0.5, maxTokens: 16000, stage: "plan" });
202
455
  const j = jsonBlock<{ title?: string; line?: string; scenes?: { id: string; idea: string }[]; gen?: { id: string; prompt: string }[]; captions?: { scene: string; text: string }[] }>(r.text);
203
456
  if (!j) throw new Error(`JSON ์„ ๋ชป ์ฝ์—ˆ๋‹ค(${r.text.length}์ž: ${r.text.slice(0, 160).replace(/\s+/g, " ")})`);
204
457
  if (j.title) plan.title = kf.title = j.title;
@@ -221,19 +474,24 @@ async function refinePlan(dir: string, model: string, request: string): Promise<
221
474
  }
222
475
  }
223
476
 
224
- // --- ์žฅ๋ฉด ์“ฐ๊ธฐ: ์žฅ๋ฉด๋งˆ๋‹ค ํŒŒ์ผ ํ•˜๋‚˜๋ฅผ ๋ฐ›์•„ ๋†“๊ณ , ์‹ค์ œ๋กœ ์—ด์–ด ์˜ค๋ฅ˜ยท์ด์ƒํ•œ ๊ธ€์ด ์—†์„ ๋•Œ๊นŒ์ง€ ๋Œ๋ ค์ค€๋‹ค
477
+ // --- ์žฅ๋ฉด ์“ฐ๊ธฐ: ์žฅ๋ฉด๋งˆ๋‹ค ํŒŒ์ผ ํ•˜๋‚˜๋ฅผ ๋ฐ›์•„ ๋†“๊ณ , ์‹ค์ œ๋กœ ์—ด์–ด ์˜ค๋ฅ˜ยท์ด์ƒํ•œ ๊ธ€์ด ์—†์„ ๋•Œ๊นŒ์ง€ ๋Œ๋ ค์ค€๋‹ค.
478
+ // ์žฅ๋ฉด๋ผ๋ฆฌ๋Š” ๋™์‹œ์— ์“ด๋‹ค(๋ชจ๋ธ ํ˜ธ์ถœ์ด ๋Œ€๋ถ€๋ถ„ ๊ธฐ๋‹ค๋ฆผ). ์—ด์–ด ๋ณด๊ธฐ๋Š” ํ•œ ๋ฒˆ์— ํ•˜๋‚˜ โ€” ํŽ˜์ด์ง€๋Š” ๋ชจ๋“  ์žฅ๋ฉด์„ ๊ฐ™์ด ์‹ฃ๋Š”๋‹ค.
479
+ // ๊ทธ๋ž˜์„œ ๋–จ์–ด์ง„ ํŒ์€ ์—ด์–ด ๋ณธ ๋’ค ๋ฐ”๋กœ ์•ž ํŒŒ์ผ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค: ์˜† ์žฅ๋ฉด์˜ ์—ด์–ด ๋ณด๊ธฐ๊ฐ€ ๋‚จ์˜ ๊นจ์ง„ ํŒ์— ๊ฑธ๋ฆฌ์ง€ ์•Š๊ฒŒ
480
+
481
+ const probing = gate(1);
225
482
 
226
483
  async function writeScenes(dir: string, model: string, ids: string[], feedback: Record<string, string> = {}): Promise<void> {
227
484
  const brief = readFileSync(join(dir, "MAKE.md"), "utf8");
228
485
  const kf = readProject(dir);
229
- for (const id of ids) {
230
- const s = kf.scenes.find((x) => x.id === id);
231
- if (!s) continue;
486
+ const scenes = ids.map((id) => kf.scenes.find((x) => x.id === id)).filter((s): s is NonNullable<typeof s> => !!s);
487
+ await pool(scenes, jobLimit(3), async (s) => {
232
488
  const file = join(dir, s.file);
233
- let note = feedback[id] ?? "";
234
- console.error(`์žฅ๋ฉด ${s.id} ์“ฐ๋Š” ์ค‘(${model})`);
489
+ const before = existsSync(file) ? readFileSync(file, "utf8") : "";
490
+ let note = feedback[s.id] ?? "";
491
+ let tried = "";
492
+ console.error(`์žฅ๋ฉด ${s.id}: ์“ฐ๋Š” ์ค‘ (${model})`);
235
493
  for (let attempt = 0; attempt < 3; attempt++) {
236
- const current = existsSync(file) ? readFileSync(file, "utf8") : "";
494
+ const current = tried || before;
237
495
  const messages: Message[] = [
238
496
  { role: "system", content: `๋„ˆ๋Š” kasaframe ์žฅ๋ฉด ํŒŒ์ผ์„ ์“ด๋‹ค. ์•„๋ž˜ ์ง€์‹œ์„œ๋ฅผ ์ง€ํ‚จ๋‹ค. ๋‹ต์€ TypeScript ์ฝ”๋“œ ๋ธ”๋ก ํ•˜๋‚˜๋ฟ.\n\n${brief}` },
239
497
  {
@@ -241,7 +499,7 @@ async function writeScenes(dir: string, model: string, ids: string[], feedback:
241
499
  content: `์žฅ๋ฉด ${s.id}(${s.file}, ${s.dur.toFixed(2)}์ดˆ, ์—ญํ•  ${s.role})์˜ ํŒŒ์ผ ์ „์ฒด๋ฅผ ์จ๋ผ.\nํ•  ์ผ: ${s.idea}\n\n์ง€๊ธˆ ํŒŒ์ผ:\n\`\`\`ts\n${current}\`\`\`${note ? `\n\n๊ณ ์น  ๊ฒƒ:\n${note}` : ""}`,
242
500
  },
243
501
  ];
244
- const r = await chat(model, messages, { temperature: 0.6, maxTokens: 32000 });
502
+ const r = await chat(model, messages, { temperature: 0.6, maxTokens: 32000, stage: `scene:${s.id}` });
245
503
  const code = r.finish === "length" ? null : codeBlock(r.text);
246
504
  if (r.finish === "length") {
247
505
  note = "๋‹ต์ด ๊ธธ์ด ์ƒํ•œ์—์„œ ์ž˜๋ ธ๋‹ค. ๊ตฐ๋”๋”๊ธฐ ์—†์ด ๋” ์งง๊ฒŒ, ```ts ๋ธ”๋ก ํ•˜๋‚˜๋กœ ํŒŒ์ผ ์ „์ฒด๋ฅผ ๋‹ตํ•˜๋ผ.";
@@ -252,13 +510,25 @@ async function writeScenes(dir: string, model: string, ids: string[], feedback:
252
510
  note = "์ฝ”๋“œ ๋ธ”๋ก์„ ๋ชป ์ฐพ์•˜๋‹ค. ```ts ๋ธ”๋ก ํ•˜๋‚˜๋กœ ํŒŒ์ผ ์ „์ฒด๋ฅผ ๋‹ตํ•˜๋ผ.";
253
511
  continue;
254
512
  }
255
- writeFileSync(file, code);
256
- const problems = await probeScene(dir, s.id);
513
+ tried = code;
514
+ const problems = await probing(async () => {
515
+ writeFileSync(file, code);
516
+ const p = await probeScene(dir, s.id);
517
+ if (p.length) writeFileSync(file, before);
518
+ return p;
519
+ });
257
520
  console.error(` ${s.id} ${attempt + 1}๋ฒˆ์งธ ${(r.ms / 1000).toFixed(1)}์ดˆ โ€” ${problems.length ? problems.join(" / ") : "ํ†ต๊ณผ"}`);
258
- if (!problems.length) break;
521
+ if (!problems.length) {
522
+ console.error(`์žฅ๋ฉด ${s.id}: ์”€`);
523
+ return;
524
+ }
259
525
  note = problems.join("\n");
260
526
  }
261
- }
527
+ // ์„ธ ๋ฒˆ ๋‹ค ๋–จ์–ด์ง€๋ฉด ์•ž ํŒŒ์ผ ๊ทธ๋Œ€๋กœ โ€” ๊นจ์ง„ ํŒ์ด ํ†ต์งธ ๋ Œ๋”๋ฅผ ๋ฉˆ์ถ”์ง€ ์•Š๊ฒŒ. ๋งˆ์ง€๋ง‰ ํŒ์€ out/failed ์— ๋‚จ๊ธด๋‹ค
528
+ mkdirSync(join(dir, "out", "failed"), { recursive: true });
529
+ if (tried) writeFileSync(join(dir, "out", "failed", `${s.id}.ts`), tried);
530
+ console.error(`์žฅ๋ฉด ${s.id}: ์‹คํŒจ ์„ธ ๋ฒˆ ๋‹ค ๊ฒ€์‚ฌ์— ๊ฑธ๋ฆผ โ€” ์•ž ํŒŒ์ผ ๊ทธ๋Œ€๋กœ(๋งˆ์ง€๋ง‰ ํŒ out/failed/${s.id}.ts)`);
531
+ });
262
532
  }
263
533
 
264
534
  // ๋ Œ๋” ์ „์— ์žฅ๋ฉด ํ•˜๋‚˜๋ฅผ ์„ธ ์‹œ๊ฐ์—์„œ ์—ด์–ด ๋ณธ๋‹ค โ€” ํ†ต์งœ ๋ Œ๋” 1๋ถ„์„ ์“ฐ๊ธฐ ์ „์— ์žก์„ ๊ฒƒ์„ ์žก๋Š”๋‹ค
@@ -341,20 +611,29 @@ interface GenOpts {
341
611
 
342
612
  const genLog = (l: string) => console.error(` ${l}`);
343
613
 
614
+ // ์ƒ์„ฑ ์ž์‚ฐ๋„ make-stats.json ์žฅ๋ถ€์— โ€” ์บ์‹œ์—์„œ ๊บผ๋‚ธ ๊ฒƒ์€ ์ด๋ฒˆ์— ๋ˆ์„ ์•ˆ ์ผ๋‹ค
615
+ function genSpent(a: Awaited<ReturnType<typeof genImage>>, stage: string): void {
616
+ if (a.cached) return;
617
+ spend({ model: `${a.meta.provider}/${a.meta.model}`, stage, ms: a.meta.elapsedMs, prompt: 0, completion: 0, cached: 0, usd: a.meta.costUsd, og: a.meta.provider.startsWith("og-") });
618
+ }
619
+
344
620
  async function genImages(dir: string): Promise<void> {
345
621
  const plan: Plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
346
622
  const imgs = plan.gen.filter((g) => g.kind === "img");
347
623
  if (!imgs.length) return;
348
624
  console.error(`์ƒ์„ฑ ์ž์‚ฐ ๊ทธ๋ฆผ ${imgs.length}๊ฐœโ€ฆ`);
349
- for (const g of imgs) {
625
+ // ์žฅ๋‹น ์ƒํ•œยท๊ณ ๋ฅธ ๊ณต๊ธ‰์ž๋Š” ๊ทธ๋Œ€๋กœ, ๋ถ€๋ฅด๊ธฐ๋งŒ ๊ฒน์นœ๋‹ค
626
+ await pool(imgs, jobLimit(3), async (g) => {
627
+ if (g.scene) console.error(`์žฅ๋ฉด ${g.scene}: ์žฌ๋ฃŒ โ€” ๊ทธ๋ฆผ ${g.id}`);
350
628
  try {
351
629
  const size = g.size ?? (plan.aspect === "9:16" ? "1080x1920" : plan.aspect === "1:1" ? "1080x1080" : "1920x1080");
352
630
  const a = await genImage(g.id, { prompt: g.prompt, size, transparent: g.transparent, quality: g.quality, refs: g.refs?.map((r) => join(dir, r)) }, { outDir: join(dir, "assets", "gen"), log: genLog }, g.provider);
631
+ genSpent(a, `gen:img:${g.id}`);
353
632
  console.error(` gen:img:${g.id} ${a.cached ? "cached" : "made"} ${a.path} $${(a.cached ? 0 : a.meta.costUsd).toFixed(3)}`);
354
633
  } catch (e) {
355
634
  console.error(` gen:img:${g.id} ${e instanceof NeedsApproval ? "needs-approval" : "error"} ${e instanceof Error ? e.message : e}`);
356
635
  }
357
- }
636
+ });
358
637
  }
359
638
 
360
639
  async function genVideos(dir: string, o: GenOpts): Promise<void> {
@@ -363,25 +642,179 @@ async function genVideos(dir: string, o: GenOpts): Promise<void> {
363
642
  if (!vids.length) return;
364
643
  console.error(`์ƒ์„ฑ ์ž์‚ฐ ์˜์ƒ ${vids.length}๊ฐœโ€ฆ`);
365
644
  const ctx = { outDir: join(dir, "assets", "gen"), approve: o.approve, maxUsd: o.maxUsd, log: genLog };
366
- for (const g of vids) {
645
+ // ์˜์ƒ ํ•˜๋‚˜๋งˆ๋‹ค ์Šน์ธยท์ƒํ•œ(maxUsd)์„ ๋”ฐ๋กœ ๋ณธ๋‹ค โ€” ๊ฒน์ณ ๋ถˆ๋Ÿฌ๋„ ๋ฌป๊ธฐ ํ‹€์€ ๊ทธ๋Œ€๋กœ
646
+ await pool(vids, jobLimit(2), async (g) => {
367
647
  const provider = o.videoProvider ?? g.provider;
648
+ console.error(`์žฅ๋ฉด ${g.scene ?? g.id}: ์žฌ๋ฃŒ โ€” ์˜์ƒ ${g.id}`);
368
649
  try {
369
650
  const a = g.seam
370
651
  ? await seam({ project: dir, scene: g.id, prompt: g.prompt, provider }, ctx)
371
652
  : await genVideo(g.id, { prompt: g.prompt, duration: g.dur ?? 5, aspect: plan.aspect, firstFrame: g.first && join(dir, g.first), lastFrame: g.last && join(dir, g.last) }, ctx, provider);
653
+ genSpent(a, `gen:vid:${g.id}`);
372
654
  console.error(` gen:vid:${g.id} ${a.cached ? "cached" : "made"} ${a.path} $${(a.cached ? 0 : a.meta.costUsd).toFixed(3)}`);
373
655
  } catch (e) {
374
656
  const tail = e instanceof NeedsApproval ? `needs-approval ๊ฒฌ์  $${e.estimateUsd.toFixed(3)}(${e.provider}) โ€” --yes ๋กœ ์Šน์ธํ•˜๋ฉด ๋งŒ๋“ ๋‹ค, ์ง€๊ธˆ์€ ๋Œ€์ฒด ํ™”๋ฉด` : `error ${e instanceof Error ? e.message : e}`;
375
657
  console.error(` gen:vid:${g.id} ${tail}`);
376
658
  }
659
+ });
660
+ }
661
+
662
+ // ์˜์ƒ ๋ชจ๋ธ์€ ์‚ฌ๋žŒ์ด ๊ณต๊ธ‰์ž๋ฅผ ๊ณจ๋ž์„ ๋•Œ(๋˜๋Š” --yes)๋งŒ ๋ˆ์„ ์“ด๋‹ค โ€” ์•„์ง ๋ฌป๋Š” ์ค‘์ด๋ฉด ๊ฒฌ์ ๋งŒ
663
+ function videoApproved(plan: Partial<Plan>): boolean {
664
+ const v = plan.answers?.video;
665
+ return !!v && v !== "none" && v !== WAIT && !(plan.asks ?? []).some((q) => q.id === "video");
666
+ }
667
+
668
+ // --- ์ฃผ์ธ๊ณต 3D ์ƒท(OG ๋ชจ๋“œ): ๋ชจ๋ธ์ด kfb ํ—ฌํผ๋กœ ์ƒท ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์“ฐ๊ณ , ๋ช‡ ์žฅ ์ฐ์–ด ๋ณด์•„ ๋Œ๋ฉด ๊ตฝ๋Š”๋‹ค. ์„ธ ๋ฒˆ ์•ˆ์— ๋ชป ์„œ๋ฉด three ๋กœ ๋ฌผ๋Ÿฌ๋‚œ๋‹ค
669
+
670
+ function shotGuide(): string {
671
+ const lib = blenderLibDir();
672
+ const readme = readFileSync(join(lib, "README.md"), "utf8");
673
+ const how = readme.slice(readme.indexOf("## ์ƒท ์Šคํฌ๋ฆฝํŠธ ์งœ๊ธฐ"), readme.indexOf("## ํŒ๋งˆ๋‹ค ๋‹ค๋ฅธ ๊ณณ"));
674
+ const api = readFileSync(join(lib, "kfb.py"), "utf8").split("\n").filter((l) => /^(def [a-z]\w*\(|class [A-Z])/.test(l)).join("\n");
675
+ return `${how}\n\nkfb ํ•จ์ˆ˜(์ด๊ฒƒ๋งŒ ์“ด๋‹ค):\n${api}\n\n์˜ˆ(๋“ฑ๋Œ€ ์„ฌ):\n\`\`\`python\n${readFileSync(join(lib, "shots", "lighthouse.py"), "utf8")}\n\`\`\``;
676
+ }
677
+
678
+ function blenderRun(dir: string, file: string, scene: string, id: string, extra: string[]): { ok: boolean; tail: string } {
679
+ const r = spawnSync(process.execPath, [kfBin, "blender", join(dir, file), "--project", dir, "--scene", scene, "--id", id, ...extra], { encoding: "utf8" });
680
+ const tail = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().split("\n").slice(-25).join("\n");
681
+ return { ok: r.status === 0, tail };
682
+ }
683
+
684
+ async function writeShots(dir: string, model: string): Promise<void> {
685
+ const plan: Plan = JSON.parse(readFileSync(join(dir, "plan.json"), "utf8"));
686
+ const shots = (plan.shots ?? []).filter((s) => s.tool === "blender");
687
+ if (!shots.length) return;
688
+ const kf = readProject(dir);
689
+ const style = JSON.parse(readFileSync(join(dir, "style.json"), "utf8"));
690
+ const guide = shotGuide();
691
+ for (const shot of shots) {
692
+ const sc = kf.scenes.find((x) => x.id === shot.scene);
693
+ if (!sc) continue;
694
+ mkdirSync(join(dir, "shots"), { recursive: true });
695
+ const peek = [0.15, 0.5, 0.9].map((k) => +(sc.from + sc.dur * k).toFixed(2)).join(",");
696
+ let note = "";
697
+ let ok = false;
698
+ console.error(`3D ์ƒท ${shot.id} ์“ฐ๋Š” ์ค‘(${model}) โ€” ์žฅ๋ฉด ${sc.id} ${sc.dur.toFixed(2)}์ดˆ`);
699
+ for (let attempt = 0; attempt < 3 && !ok; attempt++) {
700
+ const r = await chat(model, [
701
+ { role: "system", content: `๋„ˆ๋Š” Blender ํˆฐ 3D ์ƒท ์Šคํฌ๋ฆฝํŠธ(ํŒŒ์ด์ฌ, tools/blender/kfb.py ํ—ฌํผ)๋ฅผ ์“ด๋‹ค. ๋‹ต์€ python ์ฝ”๋“œ ๋ธ”๋ก ํ•˜๋‚˜๋ฟ.\n\n${guide}` },
702
+ {
703
+ role: "user",
704
+ content: `์ƒท: ${shot.idea}\n์žฅ๋ฉด ${sc.id}: ํ”„๋กœ์ ํŠธ ${sc.from.toFixed(2)}โ€“${(sc.from + sc.dur).toFixed(2)}์ดˆ(update(t) ์˜ t ๋Š” ํ”„๋กœ์ ํŠธ ์ดˆ), ํ™”๋ฉด ${kf.width}x${kf.height}\n์ƒ‰(style.json): ${JSON.stringify(style.colors ?? {})}\n๊ธ€์ž๋Š” ๋„ฃ์ง€ ์•Š๋Š”๋‹ค(์žฅ๋ฉด์ด ์œ„์— ์–น๋Š”๋‹ค). ์ฃผ์ธ๊ณต ํ•˜๋‚˜๋ฅผ ํ™”๋ฉด ๊ฐ€์šด๋ฐ ํฌ๊ฒŒ, ์‰ฌ์ง€ ์•Š๊ฒŒ ๋ฐ•์— ๋›ฐ๊ฑฐ๋‚˜ ๋„๋Š” ๋ฌผ์ฒด, ์นด๋ฉ”๋ผ ์—ด์‡ ๋Š” S.downbeatsยทS.drops ์— ๋‘๊ณ  ๋„์ฐฉํ•˜๋ฉด ์„ ๋‹ค.${note ? `\n\n๊ณ ์น  ๊ฒƒ:\n${note}` : ""}`,
705
+ },
706
+ ], { temperature: 0.5, maxTokens: 16000, stage: `shot:${shot.id}` });
707
+ const code = codeBlock(r.text, "python|py");
708
+ if (!code) {
709
+ note = "์ฝ”๋“œ ๋ธ”๋ก์„ ๋ชป ์ฐพ์•˜๋‹ค. ```python ๋ธ”๋ก ํ•˜๋‚˜๋กœ ํŒŒ์ผ ์ „์ฒด๋ฅผ ๋‹ตํ•˜๋ผ.";
710
+ continue;
711
+ }
712
+ writeFileSync(join(dir, shot.file), code);
713
+ // ์Šคํ‹ธ ๋ช‡ ์žฅ์€ kf still ์ฒ˜๋Ÿผ ์ž ๊ธˆ ์—†์ด โ€” ๋‚จ์˜ ๊ธด ๋ Œ๋” ๋’ค์—์„œ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค(๊ตฝ๊ธฐ๋Š” ์ž ๊ธˆ์„ ์“ด๋‹ค)
714
+ const t = blenderRun(dir, shot.file, sc.id, shot.id, ["--still", peek, "--res", "50", "--no-lock"]);
715
+ ok = t.ok;
716
+ note = ok ? "" : `kf blender --still ์ด ์‹คํŒจํ–ˆ๋‹ค:\n${t.tail}`;
717
+ console.error(` ${shot.id} ${attempt + 1}๋ฒˆ์งธ ${(r.ms / 1000).toFixed(1)}์ดˆ โ€” ${ok ? "์Šคํ‹ธ ํ†ต๊ณผ" : "์‹คํŒจ"}`);
718
+ }
719
+ // ์ ˆ๋ฐ˜ ํฌ๊ธฐยท๋‘ ์žฅ์— ํ•œ ์žฅ โ€” ๋ฌด๊ฑฐ์šด ๋ Œ๋”๋ฅผ ์งง๊ฒŒ(ํŽ˜์ด์ง€๊ฐ€ ๋Š˜๋ ค ๋ณด์ธ๋‹ค)
720
+ const baked = ok && blenderRun(dir, shot.file, sc.id, shot.id, ["--res", "50", "--step", "2"]).ok;
721
+ if (!baked) {
722
+ console.error(` ${shot.id} ๋ฅผ ๋ชป ๊ตฌ์› ๋‹ค โ€” ์žฅ๋ฉด ${sc.id} ๋Š” three.js(stage3d)๋กœ 3D ๋ฅผ ์ง“๋Š”๋‹ค`);
723
+ plan.shots = (plan.shots ?? []).map((s) => (s.id === shot.id ? { ...s, tool: "three" } : s));
724
+ writeJson(join(dir, "plan.json"), plan);
725
+ } else console.error(` ${shot.id} โ†’ assets/blender/${shot.id}.mp4`);
726
+ }
727
+ }
728
+
729
+ // --- ๋ฌป๊ธฐ: Claude Code ๋ชจ๋“œ์—์„œ ์„ธ์…˜์ด ์‚ฌ๋žŒ์—๊ฒŒ ๋ฌผ์€ ๋‹ต์„ ๋„ฃ๋Š”๋‹ค โ€” ์งˆ๋ฌธ์€ plan.json ์˜ asks ๋ฅผ ์ง€๊ธˆ ๋Šฅ๋ ฅ์œผ๋กœ ๋‹ค์‹œ ๋งŒ๋“ค์–ด(ํ•จ์ˆ˜ ์นธ๊นŒ์ง€) ๊ณ ๋ฅธ๋‹ค
730
+ async function answerProject(dir: string, opts: Record<string, string | boolean | undefined>): Promise<number> {
731
+ const file = join(dir, "plan.json");
732
+ if (!existsSync(file)) {
733
+ console.error(`kf make --answer: ${dir} ์— plan.json ์ด ์—†๋‹ค`);
734
+ return 2;
735
+ }
736
+ const plan: Plan = JSON.parse(readFileSync(file, "utf8"));
737
+ const waiting = new Set((plan.asks ?? []).map((q) => q.id));
738
+ const fresh = planQuestions(plan, await capabilities({ probe: true })).questions.filter((q) => waiting.has(q.id));
739
+ const settled = settle(fresh, { given: opts, yes: !!opts.yes, noGen: !!opts["no-gen"], allow: parseAllow(opts.allow) });
740
+ const { plan: next, stop } = applyAnswers(plan, settled.answers, fresh);
741
+ next.asks = (plan.asks ?? []).filter((q) => !(q.id in settled.answers));
742
+ writeJson(file, next);
743
+ if (settled.answers.theme) {
744
+ const was = readProject(dir).theme ?? null;
745
+ const now = retheme(dir, settled.answers.theme);
746
+ console.log(now === was ? `ํ…Œ๋งˆ ${now ?? "์—†์Œ(ํ”„๋ฆฌ์…‹)"} ๊ทธ๋Œ€๋กœ` : `ํ…Œ๋งˆ ${was ?? "์—†์Œ"} โ†’ ${now ?? "์—†์Œ(ํ”„๋ฆฌ์…‹)"} โ€” style.jsonยทkf.jsonยท์žฅ๋ฉด ๋ผˆ๋Œ€ DRESS ๋ฅผ ๋ฐ”๊ฟจ๋‹ค`);
747
+ }
748
+ writeFileSync(join(dir, "MAKE.md"), isDirect(dir) ? directBrief(dir) : projectBrief(dir));
749
+ for (const [id, v] of Object.entries(settled.answers)) console.log(`${id}: ${v}`);
750
+ if (stop.length) {
751
+ console.error(`์ผœ๊ณ  ๋‹ค์‹œ ๋ถ€๋ฅธ๋‹ค โ€”\n${stop.map((x) => ` ${x}`).join("\n")}`);
752
+ return 3;
753
+ }
754
+ if (next.asks.length) {
755
+ console.error(`์•„์ง ๋‹ต์ด ์—†๋‹ค: ${next.asks.map((q) => `${q.flag} <${q.choices.map((c) => c.value).join("|")}>`).join(" ")}`);
756
+ return 3;
757
+ }
758
+ console.log(`${dir}/MAKE.md ๋ฅผ ๊ณ ์ณ ์ผ๋‹ค โ€” ๋‹ต๋Œ€๋กœ ๊ทธ๋ฆผยท3D ์ƒทยท์˜์ƒ ์ปท์„ ๋งŒ๋“ค๊ณ  ์žฅ๋ฉด์„ ์ฑ„์šด๋‹ค`);
759
+ return 0;
760
+ }
761
+
762
+ // ๋งจ ์œ„ ์งˆ๋ฌธ์˜ ํ…Œ๋งˆ ๋‹ต โ€” kf new ๊ฐ€ auto ๋กœ ๊น ํ…Œ๋งˆ๋ฅผ ๋ฐ”๊พผ๋‹ค(plan.jsonยทkf.jsonยทstyle.json, ์žฅ๋ฉด ๋ผˆ๋Œ€์˜ DRESS ํ•œ ์ค„).
763
+ // ๊ฐ๋… ๋ชจ๋“œ๋Š” kf direct ๊ฐ€ kf.json ์˜ ํ…Œ๋งˆ๋กœ ์žฅ๋ฉด์„ ์ง€์œผ๋‹ˆ ์—ฌ๊ธฐ๊นŒ์ง€๋ฉด ๋œ๋‹ค. ๋ฐ”๋€ ํ…Œ๋งˆ id(ํ”„๋ฆฌ์…‹์ด๋ฉด null)๋ฅผ ๋Œ๋ ค์ค€๋‹ค
764
+ function retheme(dir: string, answer: string): string | null {
765
+ const plan = readPlan(dir) as Plan;
766
+ const kfPath = join(dir, "kf.json");
767
+ const kf = JSON.parse(readFileSync(kfPath, "utf8"));
768
+ const id: string | null = answer === "auto" ? (plan.themeCandidates?.[0]?.id ?? kf.theme ?? null) : answer === "none" ? null : answer;
769
+ if ((kf.theme ?? null) === id) return id;
770
+ const tpl = loadTemplate(plan.purpose);
771
+ // ํ”„๋ฆฌ์…‹์œผ๋กœ ๋Œ์•„๊ฐ€๋ฉด kf new ๊ฐ€ ํ…Œ๋งˆ ์—†์ด ๊ณจ๋ž์„ ํ”„๋ฆฌ์…‹ โ€” ์‹œ๋ฆฌ์ฆˆยท์š”์ฒญ ๋‚ฑ๋งยทํ‹€ ์ฐจ๋ก€
772
+ const preset = id ? null : makePlan({ request: plan.request, purpose: plan.purpose, theme: "none", series: plan.series ?? "none" }).style;
773
+ const style = id ? themeStyle(id) : JSON.parse(readFileSync(join(repo, "presets", `${existsSync(join(repo, "presets", `${preset}.json`)) ? preset : "neutral-dark"}.json`), "utf8"));
774
+ style.motion = { ...style.motion, ...tpl.motion };
775
+ writeJson(join(dir, "style.json"), style);
776
+ if (tpl.moves) {
777
+ const dress = pickDress(dressSeed(plan.request || plan.title, plan.purpose, id ?? preset!), plan.scenes, bundleFor(tpl.moves, id ?? undefined));
778
+ const byId = new Map(plan.scenes.map((s, i) => [s.id, dress[i]]));
779
+ for (const s of kf.scenes as { id: string; file: string; dress?: Record<string, string> }[]) {
780
+ const next = byId.get(s.id);
781
+ if (!s.dress || !next) continue;
782
+ const file = join(dir, s.file);
783
+ // ๋ผˆ๋Œ€๊ฐ€ ์ฑ„์šด ๊ฐ’๋งŒ ๊ฐˆ์•„ ๋ผ์šด๋‹ค โ€” ์„ธ์…˜์ด ์†๋Œ„ ์ค„ยท๋ผˆ๋Œ€์— ๋ฐ•ํžŒ ๊ธ€์ž ๊ฐ’์€ ๊ทธ๋Œ€๋กœ
784
+ if (existsSync(file))
785
+ writeFileSync(file, readFileSync(file, "utf8").replace(/^const DRESS: Dress = \{.*\};$/m, (line) => Object.entries(next).reduce((l, [k, v]) => l.replace(`${k}: "${s.dress![k]}"`, `${k}: "${v}"`), line)));
786
+ s.dress = { ...next };
787
+ }
377
788
  }
789
+ if (id) kf.theme = id;
790
+ else delete kf.theme;
791
+ writeJson(kfPath, kf);
792
+ writeJson(join(dir, "plan.json"), { ...readPlan(dir), theme: id, style: id ?? preset });
793
+ return id;
794
+ }
795
+
796
+ // ๊ฒฐ๊ณผ ๋ฉ”ํƒ€ โ€” ์ด ์˜์ƒ์— ๋“ค์–ด๊ฐ„ ์ƒ์„ฑ ์ž์‚ฐ๊ณผ ํšŒ์‚ฌยท๋ฐ”๊นฅ ๊ฒฐ๊ณผ๋ฌผ์— ๊ฑธ๋ฆฌ๋Š” ์ œํ•œ(H3 ๊ณต๊ฐœ ๊ฐ€์ค‘์น˜ ๋“ฑ)์„ ํ•œ ํŒŒ์ผ์—
797
+ function writeCredits(dir: string, video: string): void {
798
+ const read = (sub: string) =>
799
+ existsSync(join(dir, "assets", sub))
800
+ ? readdirSync(join(dir, "assets", sub)).filter((f) => f.endsWith(".json")).map((f) => ({ file: join("assets", sub, f), ...JSON.parse(readFileSync(join(dir, "assets", sub, f), "utf8")) }))
801
+ : [];
802
+ const gen = read("gen").map((m) => ({ id: m.id, kind: m.kind, provider: m.provider, model: m.model, costUsd: m.costUsd, license: m.license, region: m.region, ...(m.restrict ? { restrict: m.restrict } : {}) }));
803
+ const blender = read("blender").map((m) => ({ id: m.file.replace(/^.*\/|\.json$/g, ""), kind: "3d", provider: "blender", model: `Blender ${m.blender}`, costUsd: 0, license: "์ถœ๋ ฅ๋ฌผ์€ ๋งŒ๋“  ์‚ฌ๋žŒ ๊ฒƒ(Blender GPL ์€ ์ถœ๋ ฅ๋ฌผ์— ๊ฑธ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค)" }));
804
+ const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
805
+ const restricted = gen.filter((g) => g.restrict);
806
+ const credits = { video: basename(video), answers: plan.answers ?? {}, assets: [...gen, ...blender], costUsd: Number(gen.reduce((n, g) => n + (g.costUsd ?? 0), 0).toFixed(4)), restricted: restricted.map((g) => ({ id: g.id, provider: g.provider, restrict: g.restrict })) };
807
+ const path = join(dir, "out", `${basename(video, ".mp4")}.credits.json`);
808
+ writeJson(path, credits);
809
+ if (restricted.length) console.log(`\n์ œํ•œ ์žˆ๋Š” ์ž์‚ฐ ${restricted.length}๊ฐœ โ€” ํšŒ์‚ฌยท๋ฐ”๊นฅ ๊ฒฐ๊ณผ๋ฌผ์— ๋‚ด๊ธฐ ์ „์— ๋ฐ”๊พผ๋‹ค:\n${restricted.map((g) => ` ${g.id}(${g.provider}): ${g.restrict}`).join("\n")}`);
810
+ console.log(`๊ฒฐ๊ณผ ๋ฉ”ํƒ€: ${path}`);
378
811
  }
379
812
 
380
813
  // ์ด์Œ๋งค ์žฅ๋ฉด์˜ ๋ชจ์…˜๊ทธ๋ž˜ํ”ฝ ์ „ํ™˜(transition3d)์€ ์•ž ์žฅ๋ฉด ๋งˆ์ง€๋ง‰ ํ™”๋ฉดยท๋’ค ์žฅ๋ฉด ์ฒซ ํ™”๋ฉด ๋‘ ์žฅ์„ ๋ฐ›์•„ ๊ทธ ์‚ฌ์ด๋ฅผ ์ž‡๋Š”๋‹ค.
381
814
  // ์žฅ๋ฉด์„ ๋‹ค ์“ด ๋’ค์— ์ฐ์–ด์•ผ ์ง„์งœ ํ™”๋ฉด์ด ๋œ๋‹ค โ€” ๊ทธ๋ฆผ์ด ์—†์œผ๋ฉด ์ „ํ™˜์ด style ์ƒ‰์œผ๋กœ ๋Œ€์ฒด ํ™”๋ฉด์„ ๊ทธ๋ฆฐ๋‹ค
382
- async function seamStills(dir: string): Promise<void> {
815
+ async function seamStills(dir: string, only?: string[]): Promise<void> {
383
816
  const kf = JSON.parse(readFileSync(join(dir, "kf.json"), "utf8"));
384
- const seams = (kf.scenes as { id: string; role: string }[]).filter((s) => s.role === "seam");
817
+ const seams = (kf.scenes as { id: string; role: string }[]).filter((s) => s.role === "seam" && (!only || only.includes(s.id)));
385
818
  if (!seams.length) return;
386
819
  mkdirSync(join(dir, "assets", "seam"), { recursive: true });
387
820
  for (const s of seams) {
@@ -399,28 +832,46 @@ async function seamStills(dir: string): Promise<void> {
399
832
  // --- ๋งˆ๋ฌด๋ฆฌ: ๋ Œ๋” โ†’ ๊ฒ€์‚ฌ โ†’ (OG ๋ชจ๋“œ๋ฉด) ๊ณ ์น˜๊ธฐ โ†’ ํ‘œ์ง€
400
833
 
401
834
  // captions ๋ฅผ ์ฃผ๋ฉด ๊ทธ ํŒ๋งŒ ํ™”๋ฉด ์ž๋ง‰์„ ์ผœ๊ณ  ๋ˆ๋‹ค(kf.json ์€ ๊ทธ๋Œ€๋กœ) โ€” ๊ตฝ๊ธฐยท๊ฒ€์‚ฌยท์ž๋ง‰ ํŠธ๋ž™์ด ๊ฐ™์€ ๊ฐ’์„ ๋ณธ๋‹ค
402
- async function finish(dir: string, o: { model: string; vision: string; fixes: number; gen?: GenOpts; captions?: "on" | "off" | "fit"; suffix?: string }): Promise<number> {
835
+ // scenes ๋ฅผ ์ฃผ๋ฉด ๊ทธ ์žฅ๋ฉด ๋‘˜๋ ˆ(๊ทธ ์žฅ๋ฉด + ๋ถ™์€ ์ด์Œ๋งค)๋งŒ โ€” ์ด์Œ ํ™”๋ฉดยท๊ฒ€์‚ฌ๋ฅผ ๊ทธ ๊ตฌ๊ฐ„์—์„œ. ๊ตฝ๊ธฐ๋Š” kf render ๊ฐ€ ๋ฐ”๋€ ์กฐ๊ฐ๋งŒ ๋‹ค์‹œ ์ฐ๋Š”๋‹ค
836
+ async function finish(dir: string, o: { model: string; vision: string; fixes: number; gen?: GenOpts; captions?: "on" | "off" | "fit"; suffix?: string; scenes?: string[] }): Promise<number> {
403
837
  if (!existsSync(join(dir, "kf.json"))) {
404
838
  console.error(`kf make: ${dir} ์— kf.json ์ด ์—†๋‹ค`);
405
839
  return 2;
406
840
  }
841
+ // ์„ธ์…˜์ด kf narrate ๋ฅผ ๊ฑด๋„ˆ๋›ฐ์—ˆ์œผ๋ฉด ์—ฌ๊ธฐ์„œ โ€” ๋ชฉ์†Œ๋ฆฌยท๊ณก์„ ๊น”๊ณ  ์žฅ๋ฉด ๊ธธ์ด๋ฅผ ๋งž์ถ˜ ๋’ค ๊ตฝ๋Š”๋‹ค
842
+ if (readProject(dir).voice === undefined) {
843
+ const code = narrateStep(dir);
844
+ if (code !== 0) return code;
845
+ }
407
846
  if (o.gen) await genVideos(dir, o.gen);
408
- await seamStills(dir);
847
+ const touched = o.scenes ? around(dir, o.scenes) : undefined;
848
+ await seamStills(dir, touched);
409
849
  const out = join(dir, "out", `video${o.suffix ?? ""}.mp4`);
410
850
  const capFlag = o.captions ? ["--captions", o.captions] : [];
411
851
  let verdict = { code: 0, report: "" };
412
852
  for (let round = 0; ; round++) {
413
- if (round > 0) await seamStills(dir);
414
- const r = spawnSync(process.execPath, [kfBin, "render", dir, "--out", out, "--tabs", "2", ...capFlag], { stdio: ["ignore", "inherit", "inherit"] });
415
- if (r.status !== 0) {
853
+ if (round > 0) await seamStills(dir, touched);
854
+ // ๊ตฝ๊ธฐ์™€ ๊ฒ€์‚ฌ ์‚ฌ์ด์— ์ž ๊ธˆ์„ ๋†“์œผ๋ฉด ๊ธฐ๋‹ค๋ฆฌ๋˜ ๋‚จ์˜ ๊ธด ๋ Œ๋”๊ฐ€ ๋ผ์–ด๋“ ๋‹ค โ€” ๋‘˜์„ ํ•œ ๋ฒˆ์— ์ฅ๊ณ  ์ž์‹์€ ์ž ๊ธˆ ์—†์ด
855
+ const release = await acquireRenderLock(dir, 3600);
856
+ const r = await relay(["render", dir, "--out", out, "--tabs", "2", "--no-lock", ...capFlag]);
857
+ if (r !== 0) {
858
+ release();
416
859
  console.error("kf make: ๋ Œ๋”๊ฐ€ ์‹คํŒจํ–ˆ๋‹ค");
417
- return r.status ?? 1;
860
+ return r;
418
861
  }
419
- verdict = check(dir, capFlag);
862
+ // ๊ณ ์นœ ๋‘˜๋ ˆ + ์‹ค์ œ๋กœ ๋‹ค์‹œ ๊ตฌ์šด ์กฐ๊ฐ(๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌยทํ…Œ๋งˆ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์•ˆ ๊ณ ์นœ ์žฅ๋ฉด๋„ ๋‹ค์‹œ ๊ตฝํžŒ๋‹ค) โ€” ๋‹ค ๋‹ค์‹œ ๊ตฌ์› ์œผ๋ฉด ํ†ต์งธ ๊ฒ€์‚ฌ
863
+ const scope = touched && redone(out, touched);
864
+ for (const id of scope ?? readProject(dir).scenes.map((x) => x.id)) console.error(`์žฅ๋ฉด ${id}: ๊ฒ€์‚ฌ`);
865
+ verdict = scope ? checkPart(dir, scope, [...capFlag, "--no-lock"]) : check(dir, [...capFlag, "--no-lock"]);
866
+ release();
867
+ sceneVerdicts(dir, scope);
420
868
  const review = o.vision !== "none" ? await visionReview(dir, o.vision) : {};
421
869
  const toFix: Record<string, string> = { ...review };
422
870
  for (const [id, note] of Object.entries(findingsByScene(dir))) toFix[id] = [toFix[id], note].filter(Boolean).join("\n");
423
- if ((verdict.code === 0 && !Object.keys(review).length) || o.model === "claude-code" || round >= o.fixes) break;
871
+ // ๊ณ ์นœ ์žฅ๋ฉด๋งŒ ๋‹ค์‹œ ๊ตฝ๋Š” ๊ธธ์ด๋ฉด ๊ณ ์น˜๊ธฐ๋„ ๊ทธ ๋‘˜๋ ˆ ์•ˆ์—์„œ โ€” ์•ˆ ๊ฑด๋“œ๋ฆฐ ์žฅ๋ฉด์˜ ์˜› ์ง€์ ์œผ๋กœ ๋‚จ์˜ ์žฅ๋ฉด์„ ๋‹ค์‹œ ์“ฐ์ง€ ์•Š๋Š”๋‹ค
872
+ if (touched) for (const id of Object.keys(toFix)) if (!touched.includes(id)) delete toFix[id];
873
+ // ๊ฐ๋… ํ”„๋กœ์ ํŠธ๋Š” kf direct ๊ฐ€ ๋–จ์–ด์ง„ ์žฅ๋ฉด์„ ๋‹ค์Œ ํ›„๋ณด๋กœ ์ด๋ฏธ ๋‹ค์‹œ ์ง€์—ˆ๋‹ค โ€” ๋ชจ๋ธ์ด ์žฅ๋ฉด ์ฝ”๋“œ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š”๋‹ค
874
+ if ((verdict.code === 0 && !Object.keys(review).length) || o.model === "claude-code" || isDirect(dir) || round >= o.fixes) break;
424
875
  if (!Object.keys(toFix).length) break;
425
876
  console.error(`๊ณ ์น˜๊ธฐ ${round + 1}/${o.fixes}: ${Object.keys(toFix).join(" ")}`);
426
877
  writeFileSync(join(dir, "MAKE.md"), projectBrief(dir));
@@ -428,19 +879,134 @@ async function finish(dir: string, o: { model: string; vision: string; fixes: nu
428
879
  }
429
880
  const subs = writeCaptions(dir, o.captions && captionMode(o.captions));
430
881
  const cover = coverTime(dir);
431
- spawnSync(process.execPath, [kfBin, "still", dir, "--t", String(cover), "--out", join(dir, "out", "cover.png")], { stdio: "ignore" });
882
+ if (!touched || touched.includes(cover.scene) || !existsSync(join(dir, "out", "cover.png")))
883
+ spawnSync(process.execPath, [kfBin, "still", dir, "--t", String(cover.t), "--out", join(dir, "out", "cover.png")], { stdio: "ignore" });
432
884
  console.log(`\n${[out, join(dir, "out", "cover.png"), ...subs].join("\n")}`);
433
- if (o.vision === "none") lookHint(dir);
885
+ writeCredits(dir, out);
886
+ if (o.vision === "none" && !touched) lookHint(dir);
887
+ else if (o.vision === "none") console.log(`\n์žฅ๋ฉด ๋ชจ์Œ: kf look ${dir}`);
434
888
  if (verdict.report) console.log(`\n๊ฒ€์‚ฌ:\n${verdict.report}`);
435
889
  return verdict.code;
436
890
  }
437
891
 
892
+ // ํ‹€์ด ๋‚ด๋ ˆ์ด์…˜ยท๋ถ„์œ„๊ธฐ ๊ณก์„ ์ •ํ•œ ์šฉ๋„(์‡ผ์ธ ยท์„ค๋ช…)๋งŒ. ์žฅ๋ฉด๋ณ„ ๋ฌธ์žฅ(captions.json)์ด ์—†์œผ๋ฉด ๊ณก๋งŒ ๊น๋‹ค
893
+ function narrateStep(dir: string): number {
894
+ const tpl = loadTemplate(readProject(dir).template);
895
+ if (!tpl.narration?.on && !tpl.mood) return 0;
896
+ const lines = existsSync(join(dir, "captions.json")) ? JSON.parse(readFileSync(join(dir, "captions.json"), "utf8")) : [];
897
+ const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
898
+ const voice = plan.voice ?? plan.answers?.voice;
899
+ // ๋ฌธ์žฅ์ด ์—†๋‹ค๊ณ  ๋ชฉ์†Œ๋ฆฌ๋ฅผ ์กฐ์šฉํžˆ ๋นผ์ง€ ์•Š๋Š”๋‹ค โ€” ์‚ฌ๋žŒ์ด ๋ชฉ์†Œ๋ฆฌ๋ฅผ none ์œผ๋กœ ๊ณ ๋ฅธ ๋•Œ๋งŒ ๊ณก๋งŒ ๊น๋‹ค
900
+ if (!lines.length && tpl.narration?.on && voice !== "none") {
901
+ console.error(`kf make: ๋‚ด๋ ˆ์ด์…˜ ๋ฌธ์žฅ(captions.json)์ด ์—†๋‹ค โ€” ์žฅ๋ฉด๋ณ„ ๋ฌธ์žฅ์„ ์“ฐ๊ณ  kf narrate ${dir}, ๋ชฉ์†Œ๋ฆฌ๋ฅผ ๋นผ๋ ค๋ฉด kf make --answer ${dir} --voice none`);
902
+ return 3;
903
+ }
904
+ const r = spawnSync(process.execPath, [kfBin, "narrate", dir], { stdio: ["ignore", "inherit", "inherit"] });
905
+ if (r.status !== 0) console.error("kf make: kf narrate ๊ฐ€ ์‹คํŒจํ–ˆ๋‹ค โ€” ์œ„ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๊ณ  ๊ณ ์นœ ๋’ค kf narrate <ํด๋”>");
906
+ return r.status ?? 1;
907
+ }
908
+
909
+ // ์ž์‹ kf ์˜ ์ค„์„ ์ด ํ”„๋กœ์„ธ์Šค console ๋กœ ๋‹ค์‹œ ์ฐ๋Š”๋‹ค โ€” ์ž‘์—…์‹ค ๊ฑฐ์šธ(attachStudio.mirror)์€ console.error ๋งŒ ๋ด์„œ, ๋ฌผ๋ ค์ค€ ์ถœ๋ ฅ(inherit)์€ ์ž‘์—…์‹ค์— ์•ˆ ๋‹ฟ๋Š”๋‹ค
910
+ function relay(args: string[]): Promise<number> {
911
+ return new Promise((done) => {
912
+ const p = spawn(process.execPath, [kfBin, ...args], { stdio: ["ignore", "pipe", "pipe"] });
913
+ for (const [stream, out] of [[p.stdout, console.log], [p.stderr, console.error]] as const) {
914
+ let buf = "";
915
+ stream.setEncoding("utf8");
916
+ stream.on("data", (chunk: string) => {
917
+ buf += chunk;
918
+ const parts = buf.split(/\r?\n/);
919
+ buf = parts.pop() ?? "";
920
+ for (const l of parts) out(l);
921
+ });
922
+ stream.on("end", () => buf && out(buf));
923
+ }
924
+ p.on("close", (code) => done(code ?? 1));
925
+ });
926
+ }
927
+
438
928
  // ํด๋”๋ฅผ ์ฃผ๋ฉด kf check ๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์—ด์–ด ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ›‘๋Š”๋‹ค โ€” mp4 ๋ฅผ OCR ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ •ํ™•ํ•˜๋‹ค
439
929
  function check(dir: string, extra: string[] = []): { code: number; report: string } {
440
930
  const r = spawnSync(process.execPath, [kfBin, "check", dir, ...extra], { encoding: "utf8" });
441
931
  return { code: r.status ?? 1, report: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
442
932
  }
443
933
 
934
+ // ๊ตฝ๊ธฐ ๋ณด๊ณ ์„œ(kf render ๊ฐ€ ์˜์ƒ ์˜†์— ๋‚จ๊ธด .json)์˜ ๋‹ค์‹œ ๊ตฌ์šด ์กฐ๊ฐ์„ ๋”ํ•œ๋‹ค. ์ „๋ถ€๋ฉด undefined โ€” ํ†ต์งธ๋กœ
935
+ function redone(video: string, touched: string[]): string[] | undefined {
936
+ const all = readProject(dirname(dirname(video))).scenes.map((s) => s.id);
937
+ let rendered: string[] = [];
938
+ try {
939
+ rendered = JSON.parse(readFileSync(video.replace(/\.[^.]+$/, ".json"), "utf8")).segments?.rendered ?? [];
940
+ } catch {}
941
+ const scope = all.filter((id) => touched.includes(id) || rendered.includes(id));
942
+ return scope.length === all.length ? undefined : scope;
943
+ }
944
+
945
+ // ๊ณ ์นœ ์žฅ๋ฉด๊ณผ ๊ฑฐ๊ธฐ ๋ถ™์€ ์ด์Œ๋งค โ€” ์ด์Œ๋งค๋Š” ์•ž๋’ค ์žฅ๋ฉด์˜ ํ™”๋ฉด์„ ์ž‡๋Š”๋‹ค
946
+ function around(dir: string, ids: string[]): string[] {
947
+ const scenes = readProject(dir).scenes;
948
+ const out = new Set(ids);
949
+ for (const [i, s] of scenes.entries()) {
950
+ if (!ids.includes(s.id)) continue;
951
+ for (const n of [scenes[i - 1], scenes[i + 1]]) if (n?.role === "seam") out.add(n.id);
952
+ }
953
+ return scenes.filter((s) => out.has(s.id)).map((s) => s.id);
954
+ }
955
+
956
+ interface CheckJson {
957
+ ok: boolean;
958
+ counts: { error: number; warn: number };
959
+ findings: { rule: string; severity: string; from: number; to?: number; message: string }[];
960
+ [k: string]: unknown;
961
+ }
962
+
963
+ // ๊ณ ์นœ ๊ตฌ๊ฐ„๋งŒ ํ›‘๊ณ  out/check.json ์˜ ๊ทธ ๊ตฌ๊ฐ„ ์ง€์ ๋งŒ ๊ฐˆ์•„ ๋ผ์šด๋‹ค โ€” ๋‚˜๋จธ์ง€ ์žฅ๋ฉด์˜ ์•ž ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋Š” ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค
964
+ function checkPart(dir: string, ids: string[], extra: string[] = []): { code: number; report: string } {
965
+ const scenes = readProject(dir).scenes;
966
+ const file = join(dir, "out", "check.json");
967
+ const before: CheckJson | null = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null;
968
+ // ๋ถ™์–ด ์žˆ๋Š” ์žฅ๋ฉด๋ผ๋ฆฌ ํ•œ ๊ตฌ๊ฐ„์œผ๋กœ โ€” ํŽ˜์ด์ง€๋ฅผ ์—ฌ๋Š” ๊ฐ’์ด ์ปค์„œ ์žฅ๋ฉด๋งˆ๋‹ค ๋”ฐ๋กœ ์—ด์ง€ ์•Š๋Š”๋‹ค
969
+ const ranges: [number, number][] = [];
970
+ for (const s of scenes) {
971
+ if (!ids.includes(s.id)) continue;
972
+ const last = ranges.at(-1);
973
+ if (last && Math.abs(last[1] - s.from) < 1e-6) last[1] = s.from + s.dur;
974
+ else ranges.push([s.from, s.from + s.dur]);
975
+ }
976
+ const found: CheckJson["findings"] = [];
977
+ const reports: string[] = [];
978
+ let failed = 0;
979
+ let last: CheckJson | null = null;
980
+ for (const [a, b] of ranges) {
981
+ const part = join(dir, "out", "check-part.json");
982
+ const r = spawnSync(process.execPath, [kfBin, "check", dir, "--from", a.toFixed(4), "--to", b.toFixed(4), "--json", part, ...extra], { encoding: "utf8" });
983
+ reports.push(`${r.stdout ?? ""}${r.stderr ?? ""}`.trim());
984
+ if (r.status === 2 || !existsSync(part)) failed = 2;
985
+ else {
986
+ last = JSON.parse(readFileSync(part, "utf8"));
987
+ found.push(...last!.findings);
988
+ rmSync(part, { force: true });
989
+ }
990
+ }
991
+ if (failed) return { code: 2, report: reports.join("\n\n") };
992
+ const inside = (t: number) => ranges.some(([a, b]) => t >= a - 1e-6 && t < b - 1e-6);
993
+ const findings = [...(before?.findings ?? []).filter((f) => !inside(f.from)), ...found].sort((x, y) => x.from - y.from);
994
+ const counts = { error: findings.filter((f) => f.severity === "error").length, warn: findings.filter((f) => f.severity === "warn").length };
995
+ writeJson(file, { ...(before ?? last), findings, counts, ok: counts.error === 0, part: ranges });
996
+ return { code: counts.error ? 1 : 0, report: reports.join("\n\n") };
997
+ }
998
+
999
+ // ์žฅ๋ฉด๋งˆ๋‹ค ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ํ•œ ์ค„ โ€” ์ž‘์—…์‹ค ํƒ€์ž„๋ผ์ธ์ด ใ€Œ๋ใ€ยทใ€Œ์‹คํŒจใ€๋กœ ์ฝ๋Š”๋‹ค
1000
+ function sceneVerdicts(dir: string, only?: string[]): void {
1001
+ const file = join(dir, "out", "check.json");
1002
+ const findings: CheckJson["findings"] = existsSync(file) ? (JSON.parse(readFileSync(file, "utf8")).findings ?? []) : [];
1003
+ for (const s of readProject(dir).scenes) {
1004
+ if (only && !only.includes(s.id)) continue;
1005
+ const errs = findings.filter((f) => f.severity === "error" && f.from >= s.from - 1e-6 && f.from < s.from + s.dur);
1006
+ console.error(errs.length ? `์žฅ๋ฉด ${s.id}: ์‹คํŒจ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜ ${errs.length} โ€” ${[...new Set(errs.map((f) => f.rule))].join(", ")}` : `์žฅ๋ฉด ${s.id}: ๋`);
1007
+ }
1008
+ }
1009
+
444
1010
  // out/check.json ์˜ ์˜ค๋ฅ˜๋ฅผ ์‹œ๊ฐ์œผ๋กœ ์žฅ๋ฉด์— ๋ฌถ๋Š”๋‹ค โ€” ์žฅ๋ฉด๋งˆ๋‹ค ์ž๊ธฐ ๋ชซ์˜ ์ง€์ ๋งŒ ๋ฐ›์•„ ๋‹ค์‹œ ์“ด๋‹ค
445
1011
  function findingsByScene(dir: string): Record<string, string> {
446
1012
  const file = join(dir, "out", "check.json");
@@ -479,10 +1045,10 @@ async function visionReview(dir: string, model: string): Promise<Record<string,
479
1045
  }
480
1046
  }
481
1047
 
482
- function coverTime(dir: string): number {
1048
+ function coverTime(dir: string): { scene: string; t: number } {
483
1049
  const kf = readProject(dir);
484
1050
  const pick = kf.scenes.find((s) => s.role === "drop") ?? kf.scenes.find((s) => s.role === "hook" || s.role === "reveal") ?? kf.scenes[0];
485
- return +(pick.from + Math.min(0.8, pick.dur / 2)).toFixed(3);
1051
+ return { scene: pick.id, t: +(pick.from + Math.min(0.8, pick.dur / 2)).toFixed(3) };
486
1052
  }
487
1053
 
488
1054
  // --- ๊ทธ ๋ฐ–
@@ -537,6 +1103,19 @@ function stamp(): string {
537
1103
  return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
538
1104
  }
539
1105
 
1106
+ // content.json ์˜ say ์™€ ์ง€๊ธˆ ๊น”๋ฆฐ ๋ชฉ์†Œ๋ฆฌ ๋ฌธ์žฅ(captions.json)์ด ๋‹ค๋ฅธ ์žฅ๋ฉด์ด ์žˆ๋‚˜
1107
+ function sayChanged(dir: string, ids: string[]): boolean {
1108
+ const read = (f: string) => (existsSync(join(dir, f)) ? JSON.parse(readFileSync(join(dir, f), "utf8")) : null);
1109
+ const content = read("content.json") as { scenes?: { id: string; say?: string }[] } | null;
1110
+ const lines = (read("captions.json") ?? []) as { scene?: string; text?: string }[];
1111
+ return ids.some((id) => (content?.scenes?.find((s) => s.id === id)?.say ?? "").trim() !== lines.filter((l) => l.scene === id).map((l) => l.text?.trim() ?? "").join(" ").trim());
1112
+ }
1113
+
1114
+ function readPlan(dir: string): Partial<Plan> & { model?: string } {
1115
+ const p = join(dir, "plan.json");
1116
+ return existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : {};
1117
+ }
1118
+
540
1119
  function writeJson(path: string, v: unknown): void {
541
1120
  writeFileSync(path, `${JSON.stringify(v, null, 2)}\n`);
542
1121
  }