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.
- package/NOTICE +10 -8
- package/README.md +16 -1
- package/app/server/engine.ts +133 -15
- package/app/server/main.ts +7 -3
- package/app/web/main.ts +218 -36
- package/app/web/style.css +37 -4
- package/bin/hooks.mjs +14 -1
- package/bin/kf.mjs +20 -2
- package/docs/AGENTS.md +64 -16
- package/music/README.md +31 -7
- package/package.json +2 -2
- package/packages/adapters/src/project.ts +1 -1
- package/packages/check/src/check.ts +10 -3
- package/packages/check/src/rules/assets.ts +4 -1
- package/packages/check/src/rules/frames.ts +2 -1
- package/packages/check/src/rules/layout.ts +30 -6
- package/packages/cli/package.json +2 -1
- package/packages/cli/src/blur.ts +15 -3
- package/packages/cli/src/commands/bench.ts +123 -0
- package/packages/cli/src/commands/blender.ts +72 -49
- package/packages/cli/src/commands/direct.ts +210 -0
- package/packages/cli/src/commands/doctor.ts +9 -1
- package/packages/cli/src/commands/look.ts +9 -2
- package/packages/cli/src/commands/lyrics.ts +47 -1
- package/packages/cli/src/commands/make.ts +729 -150
- package/packages/cli/src/commands/music.ts +78 -0
- package/packages/cli/src/commands/narrate.ts +238 -0
- package/packages/cli/src/commands/new.ts +14 -5
- package/packages/cli/src/commands/render.ts +94 -60
- package/packages/cli/src/commands/series.ts +110 -0
- package/packages/cli/src/commands/setup.ts +42 -3
- package/packages/cli/src/commands/theme.ts +89 -2
- package/packages/cli/src/deps.ts +6 -6
- package/packages/cli/src/kf.ts +14 -3
- package/packages/cli/src/mix.ts +131 -0
- package/packages/cli/src/pool.ts +34 -0
- package/packages/cli/src/segments.ts +402 -0
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/project.ts +11 -0
- package/packages/director/package.json +23 -0
- package/packages/director/src/assemble.ts +361 -0
- package/packages/director/src/brief.ts +122 -0
- package/packages/director/src/catalog.ts +49 -0
- package/packages/director/src/content.ts +343 -0
- package/packages/director/src/fallback.ts +58 -0
- package/packages/director/src/index.ts +13 -0
- package/packages/director/src/pattern.ts +90 -0
- package/packages/director/src/patterns/fill/cells.ts +115 -0
- package/packages/director/src/patterns/formula/stack.ts +93 -0
- package/packages/director/src/patterns/util.ts +58 -0
- package/packages/director/src/scene.ts +62 -0
- package/packages/director/src/select.ts +163 -0
- package/packages/director/src/transitions.ts +115 -0
- package/packages/gen/PROVIDERS.md +39 -4
- package/packages/gen/src/ask.ts +132 -0
- package/packages/gen/src/audio.ts +91 -0
- package/packages/gen/src/blender.ts +54 -0
- package/packages/gen/src/capabilities.ts +151 -0
- package/packages/gen/src/generate.ts +4 -2
- package/packages/gen/src/image/codex.ts +117 -0
- package/packages/gen/src/index.ts +10 -2
- package/packages/gen/src/keys.ts +26 -0
- package/packages/gen/src/music/elevenlabs.ts +41 -0
- package/packages/gen/src/music/index.ts +120 -0
- package/packages/gen/src/music/library.ts +73 -0
- package/packages/gen/src/music/lyria.ts +52 -0
- package/packages/gen/src/music/types.ts +28 -0
- package/packages/gen/src/registry.ts +6 -3
- package/packages/gen/src/tts/dashscope.ts +64 -0
- package/packages/gen/src/tts/elevenlabs.ts +84 -0
- package/packages/gen/src/tts/gemini.ts +73 -0
- package/packages/gen/src/tts/index.ts +241 -0
- package/packages/gen/src/tts/local.ts +42 -0
- package/packages/gen/src/tts/types.ts +44 -0
- package/packages/gen/src/types.ts +4 -0
- package/packages/gen/src/video/h3.ts +5 -0
- package/packages/gen/src/video/http.ts +1 -1
- package/packages/jizura/src/index.ts +2 -2
- package/packages/jizura/src/install.ts +198 -155
- package/packages/jizura/src/layer.ts +75 -32
- package/packages/jizura/src/pin.ts +4 -1
- package/packages/jizura/src/vendor.ts +140 -0
- package/packages/jizura/src/vite.ts +34 -7
- package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
- package/packages/jizura/vendor/NOTICE +43 -0
- package/packages/jizura/vendor/engine.json +47 -0
- package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
- package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
- package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
- package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
- package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
- package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
- package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
- package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
- package/packages/jizura/vendor/fonts/fonts.json +52 -0
- package/packages/jizura/vendor/jizura_engine.js +35024 -0
- package/packages/motion/README.md +11 -0
- package/packages/motion/package.json +1 -0
- package/packages/motion/src/captions.ts +32 -24
- package/packages/motion/src/moves/brushWipe.ts +14 -1
- package/packages/motion/src/moves/dress.ts +191 -0
- package/packages/motion/src/moves/echoStack.ts +3 -3
- package/packages/motion/src/moves/fit.ts +36 -0
- package/packages/motion/src/moves/ghostStack.ts +14 -5
- package/packages/motion/src/moves/imageSwarm.ts +8 -4
- package/packages/motion/src/moves/index.ts +23 -4
- package/packages/motion/src/moves/karaokeLine.ts +17 -3
- package/packages/motion/src/moves/menu.ts +138 -0
- package/packages/motion/src/moves/neonSign.ts +2 -1
- package/packages/motion/src/moves/slamWords.ts +14 -7
- package/packages/motion/src/moves/sparkTrail.ts +16 -0
- package/packages/motion/src/moves/splitFlap.ts +4 -2
- package/packages/motion/src/moves/stepWords.ts +19 -8
- package/packages/motion/src/moves/tickerRows.ts +13 -9
- package/packages/motion/src/moves/tokenPicker.ts +4 -3
- package/packages/motion/src/moves/wordWall.ts +23 -21
- package/packages/motion/src/moves/worldRoll.ts +2 -1
- package/packages/motion/src/transition3d/index.ts +10 -3
- package/packages/motion/src/transition3d/rig.ts +107 -9
- package/packages/motion/src/transition3d/shatter.ts +5 -2
- package/packages/motion/src/transition3d/space.ts +173 -0
- package/packages/motion/src/transition3d/whip.ts +19 -6
- package/packages/motion/src/transition3d/world.ts +54 -0
- package/packages/motion/src/transitions2d/catalog.ts +1 -1
- package/packages/patterns/package.json +15 -0
- package/packages/patterns/src/bars/rank.ts +119 -0
- package/packages/patterns/src/before-after/shrink.ts +127 -0
- package/packages/patterns/src/before-after/strike.ts +125 -0
- package/packages/patterns/src/code/terminal.ts +133 -0
- package/packages/patterns/src/compare/bars.ts +142 -0
- package/packages/patterns/src/compare/duel.ts +103 -0
- package/packages/patterns/src/end/lockup.ts +85 -0
- package/packages/patterns/src/end/slam.ts +91 -0
- package/packages/patterns/src/flow/graph.ts +250 -0
- package/packages/patterns/src/hook-number/counter.ts +101 -0
- package/packages/patterns/src/hook-number/roll.ts +179 -0
- package/packages/patterns/src/hook-number/slam.ts +91 -0
- package/packages/patterns/src/index.ts +38 -0
- package/packages/patterns/src/kit.ts +449 -0
- package/packages/patterns/src/limit/gauge.ts +117 -0
- package/packages/patterns/src/list/cards.ts +131 -0
- package/packages/patterns/src/list/cycle.ts +22 -0
- package/packages/patterns/src/media/panels.ts +100 -0
- package/packages/patterns/src/objects.ts +234 -0
- package/packages/patterns/src/quote/karaoke.ts +126 -0
- package/packages/patterns/src/quote/slam.ts +22 -0
- package/packages/patterns/src/screen/focus.ts +85 -0
- package/packages/patterns/src/steps/path.ts +137 -0
- package/packages/patterns/src/steps/stack.ts +111 -0
- package/packages/patterns/src/term-reveal/chips.ts +92 -0
- package/packages/patterns/src/term-reveal/expand.ts +127 -0
- package/packages/patterns/src/timeline/axis.ts +144 -0
- package/packages/patterns/src/title/mask.ts +85 -0
- package/packages/patterns/src/title/slam.ts +23 -0
- package/packages/patterns/src/toggle-sim/switch.ts +92 -0
- package/packages/patterns/src/trend/columns.ts +108 -0
- package/packages/patterns/src/trend/draw.ts +140 -0
- package/packages/patterns/src/types.ts +67 -0
- package/packages/patterns/src/words/moves.ts +42 -0
- package/packages/patterns/src/words/slam.ts +4 -0
- package/packages/plan/src/asks.ts +256 -0
- package/packages/plan/src/brief.ts +136 -7
- package/packages/plan/src/captions.ts +5 -2
- package/packages/plan/src/index.ts +6 -2
- package/packages/plan/src/llm.ts +64 -2
- package/packages/plan/src/look.ts +3 -1
- package/packages/plan/src/plan.ts +67 -13
- package/packages/plan/src/series.ts +76 -0
- package/packages/plan/src/types.ts +53 -2
- package/packages/themes/package.json +5 -2
- package/packages/themes/src/index.ts +9 -0
- package/packages/themes/src/slotkit.ts +79 -0
- package/packages/themes/src/slots.ts +46 -0
- package/packages/themes/src/stage3d.ts +20 -0
- package/series/og-shorts.json +14 -0
- package/skills/kasaframe/SKILL.md +4 -3
- package/templates/_shared/project/lib/stage.ts +70 -50
- package/templates/_shared/scenes/default.ts +10 -7
- package/templates/_shared/scenes/drop.ts +11 -8
- package/templates/_shared/scenes/end.ts +9 -6
- package/templates/_shared/scenes/hook.ts +8 -6
- package/templates/_shared/scenes/step.ts +13 -4
- package/templates/ad/template.json +20 -1
- package/templates/demo/template.json +11 -0
- package/templates/explainer/template.json +54 -3
- package/templates/launch/template.json +34 -1
- package/templates/mv/scenes/default.ts +11 -8
- package/templates/mv/template.json +4 -2
- package/templates/promo/template.json +42 -1
- package/templates/shorts/scenes/hook.ts +8 -5
- package/templates/shorts/template.json +72 -3
- package/templates/square/template.json +21 -1
- package/themes/debug-hud/slots.ts +99 -0
- package/themes/og-measure/slots.ts +88 -0
- 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
38
|
-
|
|
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 ์์ฑ ๋ชจ๋ธ์ ๋ถ๋ฅด์ง
|
|
103
|
+
--no-gen ์์ฑ ๋ชจ๋ธ์ ๋ถ๋ฅด์ง ์๋๋ค โ ๋ฌป์ง ์๊ณ ๊ทธ๋ฆผยท์์ ๋ชจ๋ธ์ ๋นผ๊ณ , Blender ๊ฐ ์์ผ๋ฉด three ๋ก ๋์ ํ๋ค
|
|
45
104
|
|
|
46
105
|
์ฉ๋๋ฅผ ๋ชป ์ ํ๋ฉด ํฐ๋ฏธ๋์์ ๋ฌป๊ณ , ํฐ๋ฏธ๋์ด ์๋๋ฉด ๋ณด๊ธฐ๋ฅผ ์ฐ๊ณ ์ข
๋ฃ ์ฝ๋ 3.`,
|
|
47
106
|
async run(argv) {
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
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
|
|
92
|
-
if (!
|
|
93
|
-
console.error(
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const dir = resolve(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
console.log(`\n${dir}/MAKE.md ๋ฅผ ์ฝ๊ณ
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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": "๊ทธ ์ฅ๋ฉด์์ ๋ค๋ฆด ํ๊ตญ์ด ๋ด๋ ์ด์
โ ์งง์ ๋ง๋๋ฅผ
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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 =
|
|
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
|
-
|
|
256
|
-
const problems = await
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
|
860
|
+
return r;
|
|
418
861
|
}
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|