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
package/NOTICE
CHANGED
|
@@ -112,15 +112,17 @@ GPL 파일에 들이는 것은 허용되고, 아래 MIT 고지를 지킨다.
|
|
|
112
112
|
SOFTWARE.
|
|
113
113
|
|
|
114
114
|
----------------------------------------------------------------------
|
|
115
|
-
JIZURA 字面 — packages/jizura (
|
|
116
|
-
https://github.com/852wa/JIZURA (고정 커밋
|
|
115
|
+
JIZURA 字面 — packages/jizura/vendor/jizura_engine.js (실려 있다)
|
|
116
|
+
https://github.com/852wa/JIZURA (고정 커밋 fc16bfe43ea4a6c25a21f1caf04d17326de14f00, v0.10.1)
|
|
117
117
|
MIT — Copyright (c) 2026 hakoniwa
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
고정 커밋의 src/*.js(편집기 UI 12_ui.js 빼고)를 업스트림 build.py 와 같은 순서로 한 파일로 이었다. 업스트림 파일은
|
|
119
|
+
고치지 않았다. 원본 LICENSE 는 옆 JIZURA_LICENSE.txt, 출처·고정 커밋·MIT 원문은 packages/jizura/vendor/NOTICE.
|
|
120
|
+
다시 만들기: node packages/jizura/src/vendor.ts. 가사 층으로 낸 영상에는 "JIZURA 字面 (c) 2026 hakoniwa (github.com/852wa), MIT" 를 적는다.
|
|
121
|
+
글꼴 — google/fonts 고정 커밋 51303ca 의 SIL Open Font License 1.1 글꼴. 자주 쓰는 Noto Sans KR·Noto Serif KR·Black Han Sans·
|
|
122
|
+
Gowun Batang·DotGothic16 은 한글·영숫자·문장부호만 남긴 서브셋으로, IBM Plex Mono 는 원본 그대로 packages/jizura/vendor/fonts 에
|
|
123
|
+
OFL-<폴더>.txt 원문과 함께 실려 있다. 그 밖의 얼굴은 처음 쓸 때 같은 커밋의 원본을 ~/.cache/kasaframe/jizura/fonts 로 받는다.
|
|
124
|
+
|
|
125
|
+
vocaloid-style-mv-pipeline — packages/jizura/src/layer.ts·deny.ts·vendor.ts, tools/lyrics/align.py
|
|
124
126
|
https://github.com/EGSECDA/vocaloid-style-mv-pipeline (로컬 클론 커밋 13b380d)
|
|
125
127
|
MIT — Copyright (c) 2026 NikusonP
|
|
126
128
|
엔진 묶기 순서(template/tools/build_jizura_bundle.mjs), 플랜 만들기와 색 구성 고르기 schemes 'dark'·'light'
|
package/README.md
CHANGED
|
@@ -53,6 +53,19 @@ kf skill install # ~/.claude/skills/kasaframe 에 스킬 링
|
|
|
53
53
|
|
|
54
54
|
키는 환경 변수 `OPENGATEWAY_API_KEY` 또는 파일 `~/.config/opengateway.key`(한 줄). 없으면 이 셋만 빠지고 — 생성 대신 키를 둘 자리를 알려 준다 — 장면·렌더·검사는 그대로 돈다.
|
|
55
55
|
|
|
56
|
+
## 목소리·곡
|
|
57
|
+
|
|
58
|
+
쇼츠·설명은 차분한 TTS 내레이션과 깊은 앰비언트 곡(`deep`)이 기본이다. 목소리가 장면 길이를 정하고, 렌더가 곡을 말 아래로 덕킹해 -14 LUFS 로 섞는다. 곡은 틀이 박은 분위기(`deep`)대로 매번 같고, 목소리는 만들기 전에 묻는다(로컬 Qwen3·ElevenLabs·자막만·그 밖 차례). 엔진·곡이 없으면 다른 것으로 넘어가지 않고 키 넣기·다른 엔진·빼기를 묻는다.
|
|
59
|
+
|
|
60
|
+
| 쓰는 것 | 키 | 상태 |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Gemini TTS(`gemini-3.8-flash-tts`) · Lyria 3.5 음악 | `GEMINI_API_KEY` 또는 `~/.config/gemini.key` | 실호출로 확인 |
|
|
63
|
+
| ElevenLabs TTS(with-timestamps — 낱말 시각까지) · Eleven Music | `ELEVENLABS_API_KEY` | 문서대로 짰고 **실호출 안 함** |
|
|
64
|
+
| Qwen TTS(DashScope `qwen3-tts-flash`) | `DASHSCOPE_API_KEY` | 문서대로 짰고 **실호출 안 함** |
|
|
65
|
+
| 로컬 Qwen3-TTS(`tools/tts/qwen3_tts_server.py`) | `KF_QWEN3_TTS_URL` | 서버 주소가 있을 때만 — **실호출 안 함** |
|
|
66
|
+
|
|
67
|
+
키가 하나도 없으면 목소리 없이 자막 트랙만 간다. 단가·라이선스: `packages/gen/PROVIDERS.md`.
|
|
68
|
+
|
|
56
69
|
## 명령
|
|
57
70
|
|
|
58
71
|
| 명령 | 하는 일 |
|
|
@@ -65,8 +78,10 @@ kf skill install # ~/.claude/skills/kasaframe 에 스킬 링
|
|
|
65
78
|
| `kf still` · `kf sheet` · `kf look` | 한 장 · 장면 모음 · 장면별 확인표 |
|
|
66
79
|
| `kf beats` | 곡을 재서 `beats.json` — 박·마디·프레이즈·구간·드롭·대역 에너지 |
|
|
67
80
|
| `kf gen` | 그림·영상 생성(OG · fal · MiniMax · Veo · LTX) |
|
|
81
|
+
| `kf narrate` | 내레이션 먼저 — 장면별 문장을 TTS 로 읽혀 장면 길이를 목소리에 맞추고(곡이 있으면 박에), 자막을 목소리 시각으로 |
|
|
82
|
+
| `kf music` | 곡 모음(분위기 태그) 보기·넣기, 음악 생성(Lyria · Eleven Music) |
|
|
68
83
|
| `kf transitions` | 전환 목록 — 이름·출처·라이선스 |
|
|
69
|
-
| `kf theme list` | 테마 — `riso-zine` · `debug-hud` · `og-measure
|
|
84
|
+
| `kf theme list` · `kf theme sheet` | 테마 — `riso-zine` · `debug-hud` · `og-measure`, 같은 문장 한 장면을 테마마다 찍은 견본 격자 |
|
|
70
85
|
| `kf app` | 작업실 — 요청 칸·실시간 미리보기·렌더·검사 보고를 브라우저로(:5150) |
|
|
71
86
|
| `kf doctor` · `kf setup` | 바깥 도구 점검 · 깔기 |
|
|
72
87
|
| `kf skill` | Claude Code 스킬 링크 |
|
package/app/server/engine.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, extname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { checkContent } from "@kasaframe/director";
|
|
7
|
+
import { capabilities } from "@kasaframe/gen";
|
|
8
|
+
import { listTemplates, makePlan, type Plan, type Purpose, planQuestions, suggestThemes } from "@kasaframe/plan";
|
|
7
9
|
import { loadTheme, themeCards, type ThemeStatus } from "@kasaframe/themes";
|
|
8
10
|
import { captionMode } from "@kasaframe/motion/captions";
|
|
9
11
|
|
|
@@ -18,6 +20,11 @@ const PREVIEW_PORTS = [5151, 5152, 5153, 5154, 5155, 5156, 5157, 5158];
|
|
|
18
20
|
|
|
19
21
|
export type Phase = "plan" | "scaffold" | "gen" | "scenes" | "render" | "check" | "done" | "waiting" | "failed";
|
|
20
22
|
|
|
23
|
+
// 장면마다 따로 흐르는 상태 — kf make·direct·render 가 찍는 「장면 <id>: <상태>」 줄에서 읽는다
|
|
24
|
+
export type SceneState = "writing" | "written" | "assets" | "render" | "rendered" | "reused" | "check" | "done" | "failed";
|
|
25
|
+
const SCENE_WORDS: Record<string, SceneState> = { "쓰는 중": "writing", 씀: "written", 재료: "assets", 렌더: "render", 구움: "rendered", 그대로: "reused", 검사: "check", 끝: "done", 실패: "failed" };
|
|
26
|
+
const SCENE_LINE = /^장면 (\S+): (쓰는 중|씀|재료|렌더|구움|그대로|검사|끝|실패)(?:\s*(?:—\s*)?(.*))?$/;
|
|
27
|
+
|
|
21
28
|
export interface JobInput {
|
|
22
29
|
request: string;
|
|
23
30
|
purpose?: Purpose | "auto";
|
|
@@ -28,6 +35,7 @@ export interface JobInput {
|
|
|
28
35
|
approve?: boolean;
|
|
29
36
|
title?: string;
|
|
30
37
|
captions?: "on" | "off"; // 화면 자막 — 안 주면 틀 기본
|
|
38
|
+
answers?: Record<string, string>; // 계획의 asks 에 고른 답 { 질문 id: 값 } — kf make --<id> <값> 로 넘겨 묻지 않고 간다
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export interface Job {
|
|
@@ -43,20 +51,37 @@ export interface Job {
|
|
|
43
51
|
log: string[];
|
|
44
52
|
error?: string;
|
|
45
53
|
checkPassed?: boolean;
|
|
54
|
+
scenes?: Record<string, { state: SceneState; note?: string; at: number }>;
|
|
55
|
+
// 사람이 고쳐 다시 굽는 장면 — 다 되면 화면이 그 장면 시각부터 튼다
|
|
56
|
+
focus?: string;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
export interface JobView extends Omit<Job, "log"> {
|
|
49
60
|
plan?: Plan;
|
|
50
61
|
kf?: unknown;
|
|
51
62
|
// alt — 자막을 반대로 다시 구운 판(video-자막.mp4 · video-자막없음.mp4). captions — 들릴 말(captions.json)이 있다
|
|
52
|
-
files: { video?: string; cover?: string; check?: string; alt?: string; captions?: string };
|
|
63
|
+
files: { video?: string; cover?: string; check?: string; alt?: string; captions?: string; videoAt?: number };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SceneDetail {
|
|
67
|
+
scene: Record<string, unknown> & { id: string; from: number; dur: number; role?: string; idea?: string };
|
|
68
|
+
direct: boolean;
|
|
69
|
+
model: string | null;
|
|
70
|
+
content: Record<string, unknown> | null;
|
|
71
|
+
pattern: string | null;
|
|
72
|
+
candidates: { id: string; score: number; why: string[] }[];
|
|
73
|
+
pinned: { pattern?: string; seed?: string } | null;
|
|
74
|
+
assets: { path: string; url: string; kind: "image" | "video" }[];
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
const PHASES: [RegExp, Phase][] = [
|
|
56
78
|
[/^용도 /, "scaffold"],
|
|
57
79
|
[/^생성 자산/, "gen"],
|
|
58
80
|
[/^계획 다듬/, "scaffold"],
|
|
59
|
-
[/^장면 \S
|
|
81
|
+
[/^장면 \S+: 쓰는 중/, "scenes"],
|
|
82
|
+
[/^장면 \S+: 재료/, "gen"],
|
|
83
|
+
[/^장면 \S+: 렌더/, "render"],
|
|
84
|
+
[/^장면 \S+: 검사/, "check"],
|
|
60
85
|
[/^\s+\S+ \d+번째/, "scenes"],
|
|
61
86
|
[/^\s+\d+\/\d+ \(\d+%\)/, "render"],
|
|
62
87
|
[/^고치기 /, "scenes"],
|
|
@@ -99,9 +124,12 @@ export class Engine extends EventEmitter {
|
|
|
99
124
|
}));
|
|
100
125
|
}
|
|
101
126
|
|
|
102
|
-
// 만들기 전에 보여 줄 계획 — 곡 분석 없이 길이·장면 비율만. 사람이 용도를 고르면 판별을 건너뛰고, "auto" 거나 비우면
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
// 만들기 전에 보여 줄 계획 — 곡 분석 없이 길이·장면 비율만. 사람이 용도를 고르면 판별을 건너뛰고, "auto" 거나 비우면 판별한다.
|
|
128
|
+
// asks 는 돈이 들거나 없는 생성 능력만(kf make 와 같은 질문) — 페이지가 고른 답을 일에 실어 보낸다
|
|
129
|
+
async plan(request: string, purpose?: Purpose | "auto", theme?: string): Promise<Plan> {
|
|
130
|
+
const plan = makePlan({ request, purpose: purpose && purpose !== "auto" ? purpose : undefined, theme });
|
|
131
|
+
const { questions, auto } = planQuestions(plan, await capabilities({ probe: true }));
|
|
132
|
+
return { ...plan, asks: questions, answers: auto };
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
// 목록은 approved 만(SPEC 9). all 이면 review·draft 도 — 테마 만드는 사람이 견본을 보는 자리
|
|
@@ -185,7 +213,8 @@ export class Engine extends EventEmitter {
|
|
|
185
213
|
const job: Job = { id, input, dir: join(dataDir, "jobs", id, "project"), phase: "plan", progress: 0, running: false, createdAt: Date.now(), log: [] };
|
|
186
214
|
this.jobs.set(id, job);
|
|
187
215
|
mkdirSync(join(dataDir, "jobs", id), { recursive: true });
|
|
188
|
-
|
|
216
|
+
// 모델을 안 고르면 kf make 의 기본 — 고른 모델은 plan.json 에 남아 장면 다시 쓰기가 같은 모델을 부른다
|
|
217
|
+
const args = ["make", input.request, "--out", job.dir, ...(input.model ? ["--model", input.model] : [])];
|
|
189
218
|
if (input.purpose && input.purpose !== "auto") args.push("--purpose", input.purpose);
|
|
190
219
|
if (input.music) args.push("--music", input.music);
|
|
191
220
|
const given = (input.assets ?? []).filter((a) => existsSync(a));
|
|
@@ -199,14 +228,33 @@ export class Engine extends EventEmitter {
|
|
|
199
228
|
if (input.theme) args.push("--theme", input.theme);
|
|
200
229
|
if (input.approve) args.push("--yes");
|
|
201
230
|
if (input.captions) args.push("--captions", input.captions);
|
|
231
|
+
// 질문 id 가 곧 kf make 플래그 이름이다(image·video·blender·voice·music)
|
|
232
|
+
for (const [id, v] of Object.entries(input.answers ?? {})) if (/^[a-z]+$/.test(id) && v) args.push(`--${id}`, v);
|
|
202
233
|
this.run(job, args);
|
|
203
234
|
return job;
|
|
204
235
|
}
|
|
205
236
|
|
|
206
|
-
// 장면 카드에서 고친 뒤: 고른 장면만 다시 쓰고
|
|
207
|
-
|
|
237
|
+
// 장면 카드에서 고친 뒤: 고른 장면만 다시 쓰고(감독 프로젝트는 다시 짓고) 그 조각만 다시 굽고 그 구간만 검사.
|
|
238
|
+
// 모델은 그 일의 모델 그대로 — 안 정했으면 kf make 가 plan.json 에 남은 모델을 쓴다. pattern·seed 는 감독 후보 고르기
|
|
239
|
+
rewrite(id: string, scenes: string[], o: { pattern?: Record<string, string>; seed?: Record<string, string> } = {}): Job {
|
|
208
240
|
const job = this.must(id);
|
|
209
|
-
|
|
241
|
+
if (!scenes.length) throw Object.assign(new Error("다시 쓸 장면이 없다"), { status: 400 });
|
|
242
|
+
const pairs = (m?: Record<string, string>) => Object.entries(m ?? {}).filter(([k, v]) => scenes.includes(k) && v).map(([k, v]) => `${k}=${v}`).join(",");
|
|
243
|
+
const args = ["make", "--rewrite", job.dir, "--scenes", scenes.join(",")];
|
|
244
|
+
if (job.input.model) args.push("--model", job.input.model);
|
|
245
|
+
if (pairs(o.pattern)) args.push("--pattern", pairs(o.pattern));
|
|
246
|
+
if (pairs(o.seed)) args.push("--scene-seed", pairs(o.seed));
|
|
247
|
+
this.focusOn(job, scenes);
|
|
248
|
+
this.run(job, args);
|
|
249
|
+
return job;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 장면 파일은 그대로, 바뀐 조각만 다시 굽고 그 구간만 검사 — 손으로 고친 장면·바꾼 자산
|
|
253
|
+
redo(id: string, scenes: string[]): Job {
|
|
254
|
+
const job = this.must(id);
|
|
255
|
+
if (!scenes.length) throw Object.assign(new Error("다시 구울 장면이 없다"), { status: 400 });
|
|
256
|
+
this.focusOn(job, scenes);
|
|
257
|
+
this.run(job, ["make", "--finish", job.dir, "--scenes", scenes.join(","), "--fix", "0"]);
|
|
210
258
|
return job;
|
|
211
259
|
}
|
|
212
260
|
|
|
@@ -214,10 +262,63 @@ export class Engine extends EventEmitter {
|
|
|
214
262
|
rerender(id: string, o: { captions?: "on" | "off" } = {}): Job {
|
|
215
263
|
const job = this.must(id);
|
|
216
264
|
const caps = o.captions === "on" || o.captions === "off" ? ["--captions", o.captions, "--suffix"] : [];
|
|
217
|
-
this.run(job, ["make", "--finish", job.dir, "--
|
|
265
|
+
this.run(job, ["make", "--finish", job.dir, "--fix", "0", ...caps]);
|
|
218
266
|
return job;
|
|
219
267
|
}
|
|
220
268
|
|
|
269
|
+
private focusOn(job: Job, scenes: string[]): void {
|
|
270
|
+
job.focus = scenes[0];
|
|
271
|
+
job.scenes = Object.fromEntries(Object.entries(job.scenes ?? {}).filter(([k]) => !scenes.includes(k)));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private sceneLine(job: Job, line: string): void {
|
|
275
|
+
const m = line.match(SCENE_LINE);
|
|
276
|
+
if (!m) return;
|
|
277
|
+
(job.scenes ??= {})[m[1]] = { state: SCENE_WORDS[m[2]], ...(m[3] ? { note: m[3].trim() } : {}), at: Date.now() };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 장면 하나 고치는 자리 — 감독 프로젝트는 content.json 의 그 장면 칸·패턴 후보·씨앗, 아니면 할 일·장면 모델. 자산은 그 장면이 쓰는 그림·영상
|
|
281
|
+
sceneDetail(id: string, sid: string): SceneDetail {
|
|
282
|
+
const job = this.must(id);
|
|
283
|
+
const read = (f: string) => (existsSync(join(job.dir, f)) ? JSON.parse(readFileSync(join(job.dir, f), "utf8")) : null);
|
|
284
|
+
const kf = read("kf.json");
|
|
285
|
+
const scene = kf?.scenes?.find((s: { id: string }) => s.id === sid);
|
|
286
|
+
if (!scene) throw Object.assign(new Error(`장면 ${sid} 가 없다`), { status: 404 });
|
|
287
|
+
const plan = read("plan.json") ?? {};
|
|
288
|
+
const director = read("director.json");
|
|
289
|
+
const ds = director?.scenes?.find((s: { id: string }) => s.id === sid);
|
|
290
|
+
const file = join(job.dir, scene.file ?? `scenes/${sid}.ts`);
|
|
291
|
+
const src = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
292
|
+
const assetsDir = join(job.dir, "assets");
|
|
293
|
+
const walk = (d: string): string[] => (existsSync(d) ? readdirSync(d, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? walk(join(d, e.name)) : [join(d, e.name)])) : []);
|
|
294
|
+
const assets = walk(assetsDir)
|
|
295
|
+
.filter((f) => /\.(png|jpe?g|webp|gif|mp4|webm|mov)$/i.test(f))
|
|
296
|
+
.map((f) => relative(assetsDir, f).split(sep).join("/"))
|
|
297
|
+
.filter((rel) => src.includes(rel) || basename(rel).startsWith(`${sid}.`) || basename(rel).startsWith(`${sid}-`))
|
|
298
|
+
.map((rel) => ({ path: rel, url: `/api/jobs/${id}/files/assets/${rel}`, kind: (/\.(mp4|webm|mov)$/i.test(rel) ? "video" : "image") as "image" | "video" }));
|
|
299
|
+
return {
|
|
300
|
+
scene,
|
|
301
|
+
direct: plan.direct === true,
|
|
302
|
+
model: job.input.model ?? plan.model ?? null,
|
|
303
|
+
content: read("content.json")?.scenes?.find((s: { id: string }) => s.id === sid) ?? null,
|
|
304
|
+
pattern: ds?.pattern ?? null,
|
|
305
|
+
candidates: ds?.order ?? [],
|
|
306
|
+
pinned: director?.pinned?.[sid] ?? null,
|
|
307
|
+
assets,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 사람이 올린 파일로 그 장면 자산 하나를 바꾼다 — 다음 굽기에서 그 자산을 쓰는 조각만 다시 찍힌다
|
|
312
|
+
replaceAsset(id: string, sid: string, o: { path: string; upload: string }): SceneDetail {
|
|
313
|
+
const job = this.must(id);
|
|
314
|
+
const detail = this.sceneDetail(id, sid);
|
|
315
|
+
if (!detail.assets.some((a) => a.path === o.path)) throw Object.assign(new Error(`장면 ${sid} 가 쓰는 자산이 아니다: ${o.path}`), { status: 400 });
|
|
316
|
+
if (!existsSync(o.upload)) throw Object.assign(new Error("올린 파일이 없다"), { status: 400 });
|
|
317
|
+
if (extname(o.upload).toLowerCase() !== extname(o.path).toLowerCase()) throw Object.assign(new Error(`같은 꼴(${extname(o.path)})로 올린다`), { status: 400 });
|
|
318
|
+
copyFileSync(o.upload, join(job.dir, "assets", o.path));
|
|
319
|
+
return this.sceneDetail(id, sid);
|
|
320
|
+
}
|
|
321
|
+
|
|
221
322
|
// 말로 시작한 kf make(Claude Code 등)가 스스로 등록한다 — 같은 폴더면 같은 일(--finish 가 다시 붙는다)
|
|
222
323
|
attach(o: { dir: string; request: string; model?: string; purpose?: Purpose }): Job {
|
|
223
324
|
const dir = resolve(o.dir);
|
|
@@ -237,6 +338,7 @@ export class Engine extends EventEmitter {
|
|
|
237
338
|
const job = this.must(id);
|
|
238
339
|
for (const line of p.lines ?? []) {
|
|
239
340
|
job.log.push(line);
|
|
341
|
+
this.sceneLine(job, line);
|
|
240
342
|
for (const [re, phase] of PHASES) if (re.test(line)) job.phase = phase;
|
|
241
343
|
const pct = line.match(/\((\d+)%\)/);
|
|
242
344
|
if (pct && job.phase === "render") job.progress = Number(pct[1]) / 100;
|
|
@@ -254,8 +356,20 @@ export class Engine extends EventEmitter {
|
|
|
254
356
|
this.emit("state", this.view(id));
|
|
255
357
|
}
|
|
256
358
|
|
|
257
|
-
editScene(id: string, sceneId: string, patch: { idea?: string; dur?: number }): unknown {
|
|
359
|
+
editScene(id: string, sceneId: string, patch: { idea?: string; dur?: number; content?: Record<string, unknown> }): unknown {
|
|
258
360
|
const job = this.must(id);
|
|
361
|
+
// 감독 프로젝트 — 화면에 보일 것은 content.json 이 정본이다. 칸이 틀리면 저장하지 않고 어느 칸인지 돌려준다
|
|
362
|
+
if (patch.content) {
|
|
363
|
+
const cPath = join(job.dir, "content.json");
|
|
364
|
+
if (!existsSync(cPath)) throw Object.assign(new Error("content.json 이 없다 — 감독 모드로 만든 일이 아니다"), { status: 400 });
|
|
365
|
+
const content = JSON.parse(readFileSync(cPath, "utf8"));
|
|
366
|
+
const at = content.scenes?.findIndex((s: { id: string }) => s.id === sceneId) ?? -1;
|
|
367
|
+
if (at < 0) throw Object.assign(new Error(`content.json 에 장면 ${sceneId} 가 없다`), { status: 404 });
|
|
368
|
+
content.scenes[at] = { ...patch.content, id: sceneId };
|
|
369
|
+
const c = checkContent(content);
|
|
370
|
+
if (c.errors.length) throw Object.assign(new Error(c.errors.map((e) => `${e.at}: ${e.msg}`).join("\n")), { status: 400 });
|
|
371
|
+
writeFileSync(cPath, `${JSON.stringify(content, null, 2)}\n`);
|
|
372
|
+
}
|
|
259
373
|
const kfPath = join(job.dir, "kf.json");
|
|
260
374
|
const kf = JSON.parse(readFileSync(kfPath, "utf8"));
|
|
261
375
|
const i = kf.scenes.findIndex((s: { id: string }) => s.id === sceneId);
|
|
@@ -289,7 +403,10 @@ export class Engine extends EventEmitter {
|
|
|
289
403
|
const { log: _log, ...rest } = job;
|
|
290
404
|
const kf = read("kf.json");
|
|
291
405
|
const alt = has(captionMode(kf?.captions) === "on" ? "out/video-자막없음.mp4" : "out/video-자막.mp4");
|
|
292
|
-
|
|
406
|
+
const video = has("out/video.mp4");
|
|
407
|
+
// 같은 이름으로 다시 구워도 화면이 새 영상을 받게 — 고친 시각을 붙인다
|
|
408
|
+
const videoAt = video ? Math.round(statSync(join(job.dir, video)).mtimeMs) : undefined;
|
|
409
|
+
return { ...rest, plan: read("plan.json"), kf, files: { video, cover: has("out/cover.png"), check: has("out/check.json"), alt, captions: has("captions.json"), videoAt } };
|
|
293
410
|
}
|
|
294
411
|
|
|
295
412
|
list(): JobView[] {
|
|
@@ -326,6 +443,7 @@ export class Engine extends EventEmitter {
|
|
|
326
443
|
if (!line.trim()) return;
|
|
327
444
|
job.log.push(line);
|
|
328
445
|
if (job.log.length > 800) job.log.splice(0, job.log.length - 800);
|
|
446
|
+
this.sceneLine(job, line);
|
|
329
447
|
for (const [re, phase] of PHASES) if (re.test(line)) job.phase = phase;
|
|
330
448
|
const pct = line.match(/\((\d+)%\)/);
|
|
331
449
|
if (pct && job.phase === "render") job.progress = Number(pct[1]) / 100;
|
package/app/server/main.ts
CHANGED
|
@@ -84,7 +84,7 @@ async function api(req: IncomingMessage, res: ServerResponse, path: string): Pro
|
|
|
84
84
|
if (path === "/api/plan" && method === "POST") {
|
|
85
85
|
const b = await body<{ request: string; purpose?: never; theme?: string }>(req);
|
|
86
86
|
// purpose: 틀 id 면 그 틀로, "auto" 거나 없으면 요청 글로 판별. theme: id 나 "auto"
|
|
87
|
-
return send(res, 200, engine.plan(b.request ?? "", b.purpose, b.theme));
|
|
87
|
+
return send(res, 200, await engine.plan(b.request ?? "", b.purpose, b.theme));
|
|
88
88
|
}
|
|
89
89
|
if (path === "/api/themes" && method === "GET") {
|
|
90
90
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
@@ -167,11 +167,15 @@ async function api(req: IncomingMessage, res: ServerResponse, path: string): Pro
|
|
|
167
167
|
}
|
|
168
168
|
if (rest === "render" && method === "POST") return send(res, 200, engine.view(engine.rerender(id, await body<{ captions?: "on" | "off" }>(req)).id));
|
|
169
169
|
if (rest === "rewrite" && method === "POST") {
|
|
170
|
-
const b = await body<{ scenes: string[] }>(req);
|
|
171
|
-
return send(res, 200, engine.view(engine.rewrite(id, b.scenes ?? []).id));
|
|
170
|
+
const b = await body<{ scenes: string[]; pattern?: Record<string, string>; seed?: Record<string, string> }>(req);
|
|
171
|
+
return send(res, 200, engine.view(engine.rewrite(id, b.scenes ?? [], { pattern: b.pattern, seed: b.seed }).id));
|
|
172
172
|
}
|
|
173
|
+
if (rest === "redo" && method === "POST") return send(res, 200, engine.view(engine.redo(id, (await body<{ scenes: string[] }>(req)).scenes ?? []).id));
|
|
173
174
|
const scene = rest.match(/^scenes\/([\w-]+)$/);
|
|
175
|
+
if (scene && method === "GET") return send(res, 200, engine.sceneDetail(id, scene[1]));
|
|
174
176
|
if (scene && method === "PATCH") return send(res, 200, engine.editScene(id, scene[1], await body(req)));
|
|
177
|
+
const sceneAsset = rest.match(/^scenes\/([\w-]+)\/asset$/);
|
|
178
|
+
if (sceneAsset && method === "PUT") return send(res, 200, engine.replaceAsset(id, sceneAsset[1], await body(req)));
|
|
175
179
|
const file = rest.match(/^files\/(.+)$/);
|
|
176
180
|
if (file && method === "GET") {
|
|
177
181
|
const dir = engine.view(id).dir;
|