kasaframe 0.0.0-stage → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +148 -0
- package/README.md +92 -2
- package/app/package.json +12 -0
- package/app/server/engine.ts +533 -0
- package/app/server/main.ts +209 -0
- package/app/web/env.d.ts +2 -0
- package/app/web/icons.ts +31 -0
- package/app/web/index.html +21 -0
- package/app/web/main.ts +1195 -0
- package/app/web/style.css +463 -0
- package/bin/hooks.mjs +64 -0
- package/bin/kf.mjs +32 -0
- package/docs/AGENTS.md +286 -0
- package/music/README.md +37 -0
- package/package.json +44 -4
- package/packages/adapters/package.json +36 -0
- package/packages/adapters/src/css.ts +11 -0
- package/packages/adapters/src/gsap.ts +26 -0
- package/packages/adapters/src/index.ts +9 -0
- package/packages/adapters/src/lottie.ts +22 -0
- package/packages/adapters/src/media.ts +59 -0
- package/packages/adapters/src/page.ts +229 -0
- package/packages/adapters/src/project.ts +214 -0
- package/packages/adapters/src/react.ts +36 -0
- package/packages/adapters/src/svelte.ts +22 -0
- package/packages/adapters/src/three.ts +18 -0
- package/packages/adapters/src/vue.ts +27 -0
- package/packages/check/package.json +19 -0
- package/packages/check/src/check.ts +280 -0
- package/packages/check/src/cli.ts +70 -0
- package/packages/check/src/image.ts +146 -0
- package/packages/check/src/index.ts +9 -0
- package/packages/check/src/ocr.swift +37 -0
- package/packages/check/src/probe.ts +328 -0
- package/packages/check/src/report.ts +44 -0
- package/packages/check/src/rules/assets.ts +45 -0
- package/packages/check/src/rules/beats.ts +212 -0
- package/packages/check/src/rules/camera.ts +217 -0
- package/packages/check/src/rules/captions.ts +49 -0
- package/packages/check/src/rules/flash.ts +235 -0
- package/packages/check/src/rules/frames.ts +136 -0
- package/packages/check/src/rules/layout.ts +281 -0
- package/packages/check/src/rules/page.ts +87 -0
- package/packages/check/src/rules/runs.ts +20 -0
- package/packages/check/src/rules/text.ts +326 -0
- package/packages/check/src/scan.ts +240 -0
- package/packages/check/src/serve.ts +67 -0
- package/packages/check/src/types.ts +80 -0
- package/packages/check/src/video.ts +137 -0
- package/packages/cli/package.json +30 -0
- package/packages/cli/src/blur.ts +80 -0
- package/packages/cli/src/browser.ts +203 -0
- package/packages/cli/src/command.ts +44 -0
- package/packages/cli/src/commands/app.ts +25 -0
- package/packages/cli/src/commands/beats.ts +36 -0
- package/packages/cli/src/commands/bench.ts +123 -0
- package/packages/cli/src/commands/blender.ts +293 -0
- package/packages/cli/src/commands/check.ts +25 -0
- package/packages/cli/src/commands/direct.ts +210 -0
- package/packages/cli/src/commands/doctor.ts +30 -0
- package/packages/cli/src/commands/gen.ts +173 -0
- package/packages/cli/src/commands/look.ts +46 -0
- package/packages/cli/src/commands/lyrics.ts +157 -0
- package/packages/cli/src/commands/make.ts +1121 -0
- 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 +185 -0
- package/packages/cli/src/commands/preview.ts +43 -0
- package/packages/cli/src/commands/render.ts +250 -0
- package/packages/cli/src/commands/series.ts +110 -0
- package/packages/cli/src/commands/setup.ts +102 -0
- package/packages/cli/src/commands/sheet.ts +114 -0
- package/packages/cli/src/commands/skill.ts +43 -0
- package/packages/cli/src/commands/still.ts +40 -0
- package/packages/cli/src/commands/theme.ts +172 -0
- package/packages/cli/src/commands/transitions.ts +40 -0
- package/packages/cli/src/deps.ts +122 -0
- package/packages/cli/src/ffmpeg.ts +81 -0
- package/packages/cli/src/index.ts +8 -0
- package/packages/cli/src/kf.ts +64 -0
- package/packages/cli/src/lock.ts +72 -0
- package/packages/cli/src/mix.ts +131 -0
- package/packages/cli/src/out.ts +13 -0
- package/packages/cli/src/pool.ts +34 -0
- package/packages/cli/src/segments.ts +402 -0
- package/packages/cli/src/serve.ts +124 -0
- package/packages/clock/package.json +14 -0
- package/packages/clock/src/index.ts +25 -0
- package/packages/clock/src/install.ts +351 -0
- package/packages/clock/src/types.ts +45 -0
- package/packages/core/package.json +9 -0
- package/packages/core/src/beats.ts +283 -0
- package/packages/core/src/ease.ts +87 -0
- package/packages/core/src/frame.ts +99 -0
- package/packages/core/src/index.ts +27 -0
- package/packages/core/src/project.ts +157 -0
- package/packages/core/src/rand.ts +56 -0
- package/packages/core/src/spring.ts +52 -0
- package/packages/core/src/style.ts +100 -0
- package/packages/core/src/time.ts +88 -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 +90 -0
- package/packages/gen/package.json +9 -0
- 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 +206 -0
- package/packages/gen/src/image/codex.ts +117 -0
- package/packages/gen/src/image/og.ts +133 -0
- package/packages/gen/src/index.ts +26 -0
- package/packages/gen/src/keys.ts +86 -0
- package/packages/gen/src/media.ts +150 -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/plan.ts +106 -0
- package/packages/gen/src/registry.ts +54 -0
- package/packages/gen/src/seam.ts +103 -0
- 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 +90 -0
- package/packages/gen/src/video/fal.ts +140 -0
- package/packages/gen/src/video/h3.ts +112 -0
- package/packages/gen/src/video/http.ts +44 -0
- package/packages/gen/src/video/ltx.ts +60 -0
- package/packages/gen/src/video/minimax.ts +55 -0
- package/packages/gen/src/video/veo.ts +71 -0
- package/packages/jizura/package.json +16 -0
- package/packages/jizura/src/deny.ts +73 -0
- package/packages/jizura/src/index.ts +7 -0
- package/packages/jizura/src/install.ts +269 -0
- package/packages/jizura/src/layer.ts +325 -0
- package/packages/jizura/src/lyrics.ts +81 -0
- package/packages/jizura/src/pin.ts +8 -0
- package/packages/jizura/src/vendor.ts +140 -0
- package/packages/jizura/src/vite.ts +63 -0
- 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 +153 -0
- package/packages/motion/package.json +30 -0
- package/packages/motion/src/camera.ts +97 -0
- package/packages/motion/src/captions.ts +487 -0
- package/packages/motion/src/enter.ts +128 -0
- package/packages/motion/src/hangul.ts +129 -0
- package/packages/motion/src/impact.ts +100 -0
- package/packages/motion/src/index.ts +17 -0
- package/packages/motion/src/moves/brushWipe.ts +103 -0
- package/packages/motion/src/moves/cloneArray.ts +111 -0
- package/packages/motion/src/moves/curtains.ts +81 -0
- package/packages/motion/src/moves/dress.ts +191 -0
- package/packages/motion/src/moves/echoStack.ts +70 -0
- package/packages/motion/src/moves/fit.ts +70 -0
- package/packages/motion/src/moves/flood.ts +61 -0
- package/packages/motion/src/moves/ghostStack.ts +88 -0
- package/packages/motion/src/moves/gridFloor.ts +100 -0
- package/packages/motion/src/moves/imageSwarm.ts +113 -0
- package/packages/motion/src/moves/index.ts +38 -0
- package/packages/motion/src/moves/irisBlades.ts +113 -0
- package/packages/motion/src/moves/karaokeLine.ts +108 -0
- package/packages/motion/src/moves/ledBoard.ts +94 -0
- package/packages/motion/src/moves/liquidTab.ts +72 -0
- package/packages/motion/src/moves/markerWrite.ts +63 -0
- package/packages/motion/src/moves/menu.ts +138 -0
- package/packages/motion/src/moves/morphBox.ts +96 -0
- package/packages/motion/src/moves/neonSign.ts +78 -0
- package/packages/motion/src/moves/panelPop.ts +78 -0
- package/packages/motion/src/moves/paperTear.ts +75 -0
- package/packages/motion/src/moves/path.ts +64 -0
- package/packages/motion/src/moves/pathText.ts +107 -0
- package/packages/motion/src/moves/pointSphere.ts +109 -0
- package/packages/motion/src/moves/ransomLetters.ts +73 -0
- package/packages/motion/src/moves/shapeMorph.ts +143 -0
- package/packages/motion/src/moves/slamWords.ts +96 -0
- package/packages/motion/src/moves/sparkTrail.ts +130 -0
- package/packages/motion/src/moves/speedLines.ts +91 -0
- package/packages/motion/src/moves/splitFlap.ts +149 -0
- package/packages/motion/src/moves/stepWords.ts +128 -0
- package/packages/motion/src/moves/strikeOut.ts +98 -0
- package/packages/motion/src/moves/sunburst.ts +58 -0
- package/packages/motion/src/moves/tickerRows.ts +91 -0
- package/packages/motion/src/moves/tokenPicker.ts +115 -0
- package/packages/motion/src/moves/truchet.ts +85 -0
- package/packages/motion/src/moves/wordWall.ts +78 -0
- package/packages/motion/src/moves/worldRoll.ts +32 -0
- package/packages/motion/src/particles.ts +102 -0
- package/packages/motion/src/react/index.tsx +21 -0
- package/packages/motion/src/react/parts.tsx +431 -0
- package/packages/motion/src/react/time.tsx +97 -0
- package/packages/motion/src/text.ts +103 -0
- package/packages/motion/src/transition.ts +82 -0
- package/packages/motion/src/transition3d/cards.ts +65 -0
- package/packages/motion/src/transition3d/chain.ts +84 -0
- package/packages/motion/src/transition3d/dive.ts +181 -0
- package/packages/motion/src/transition3d/fold.ts +110 -0
- package/packages/motion/src/transition3d/gl.ts +219 -0
- package/packages/motion/src/transition3d/index.ts +235 -0
- package/packages/motion/src/transition3d/live.ts +70 -0
- package/packages/motion/src/transition3d/portal.ts +115 -0
- package/packages/motion/src/transition3d/react.tsx +29 -0
- package/packages/motion/src/transition3d/rig.ts +600 -0
- package/packages/motion/src/transition3d/shatter.ts +104 -0
- package/packages/motion/src/transition3d/space.ts +173 -0
- package/packages/motion/src/transition3d/whip.ts +78 -0
- package/packages/motion/src/transition3d/world.ts +737 -0
- package/packages/motion/src/transition3d/zoomOut.ts +114 -0
- package/packages/motion/src/transitions2d/catalog.ts +127 -0
- package/packages/motion/src/transitions2d/css.d.ts +2 -0
- package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
- package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
- package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
- package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
- package/packages/motion/src/transitions2d/index.ts +2 -0
- package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
- package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
- package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
- package/packages/motion/src/transitions2d/react.ts +13 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
- package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
- package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
- package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
- package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
- package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
- package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
- package/packages/motion/src/type.ts +131 -0
- package/packages/motion/src/ui.ts +172 -0
- 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/package.json +15 -0
- package/packages/plan/src/asks.ts +256 -0
- package/packages/plan/src/brief.ts +383 -0
- package/packages/plan/src/captions.ts +78 -0
- package/packages/plan/src/detect.ts +93 -0
- package/packages/plan/src/index.ts +27 -0
- package/packages/plan/src/layout.ts +176 -0
- package/packages/plan/src/llm.ts +172 -0
- package/packages/plan/src/look.ts +222 -0
- package/packages/plan/src/plan.ts +179 -0
- package/packages/plan/src/request.ts +55 -0
- package/packages/plan/src/series.ts +76 -0
- package/packages/plan/src/studio.ts +53 -0
- package/packages/plan/src/templates.ts +42 -0
- package/packages/plan/src/themes.ts +48 -0
- package/packages/plan/src/types.ts +157 -0
- package/packages/studio/package.json +12 -0
- package/packages/studio/src/index.ts +1 -0
- package/packages/studio/src/plugin.ts +236 -0
- package/packages/studio/src/ui/main.ts +503 -0
- package/packages/studio/src/ui/studio.css +165 -0
- package/packages/studio/src/ui/timeline.ts +317 -0
- package/packages/themes/kit/THEME-MAKE.md +68 -0
- package/packages/themes/kit/THEME.md +30 -0
- package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
- package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
- package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
- package/packages/themes/kit/effects.ts +5 -0
- package/packages/themes/kit/sample/index.html +11 -0
- package/packages/themes/kit/sample/main.ts +9 -0
- package/packages/themes/kit/sample/scenes/scene.ts +15 -0
- package/packages/themes/package.json +19 -0
- package/packages/themes/src/factory.ts +296 -0
- package/packages/themes/src/fx.ts +14 -0
- package/packages/themes/src/index.ts +114 -0
- package/packages/themes/src/slotkit.ts +79 -0
- package/packages/themes/src/slots.ts +46 -0
- package/packages/themes/src/stage3d.ts +197 -0
- package/packages/themes/src/types.ts +58 -0
- package/presets/kawaii-pastel.json +94 -0
- package/presets/neutral-dark.json +82 -0
- package/presets/og.json +98 -0
- package/series/og-shorts.json +14 -0
- package/skills/kasaframe/SKILL.md +33 -0
- package/templates/_shared/project/index.html +11 -0
- package/templates/_shared/project/lib/react.ts +22 -0
- package/templates/_shared/project/lib/stage.ts +292 -0
- package/templates/_shared/project/main.ts +9 -0
- package/templates/_shared/scenes/default.ts +21 -0
- package/templates/_shared/scenes/drop.ts +33 -0
- package/templates/_shared/scenes/end.ts +26 -0
- package/templates/_shared/scenes/hook.ts +18 -0
- package/templates/_shared/scenes/seam.ts +41 -0
- package/templates/_shared/scenes/step.ts +49 -0
- package/templates/ad/preview.png +0 -0
- package/templates/ad/template.json +151 -0
- package/templates/demo/preview.png +0 -0
- package/templates/demo/template.json +141 -0
- package/templates/explainer/preview.png +0 -0
- package/templates/explainer/template.json +193 -0
- package/templates/launch/preview.png +0 -0
- package/templates/launch/template.json +163 -0
- package/templates/mv/preview.png +0 -0
- package/templates/mv/scenes/default.ts +98 -0
- package/templates/mv/template.json +178 -0
- package/templates/promo/preview.png +0 -0
- package/templates/promo/template.json +185 -0
- package/templates/shorts/preview.png +0 -0
- package/templates/shorts/scenes/hook.ts +37 -0
- package/templates/shorts/template.json +196 -0
- package/templates/square/preview.png +0 -0
- package/templates/square/template.json +133 -0
- package/themes/debug-hud/THEME.md +39 -0
- package/themes/debug-hud/effects.ts +389 -0
- package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
- package/themes/debug-hud/sample/beats.json +1 -0
- package/themes/debug-hud/sample/index.html +11 -0
- package/themes/debug-hud/sample/kf.json +59 -0
- package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
- package/themes/debug-hud/sample/lib/og.ts +29 -0
- package/themes/debug-hud/sample/main.ts +9 -0
- package/themes/debug-hud/sample/scenes/end.ts +62 -0
- package/themes/debug-hud/sample/scenes/gather.ts +67 -0
- package/themes/debug-hud/sample/scenes/models.ts +70 -0
- package/themes/debug-hud/sample/scenes/price.ts +79 -0
- package/themes/debug-hud/sample/style.json +91 -0
- package/themes/debug-hud/slots.ts +99 -0
- package/themes/debug-hud/still.png +0 -0
- package/themes/debug-hud/style.json +91 -0
- package/themes/debug-hud/theme.json +68 -0
- package/themes/og-measure/THEME.md +47 -0
- package/themes/og-measure/effects.ts +432 -0
- package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
- package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
- package/themes/og-measure/sample/assets/data/measure.json +353 -0
- package/themes/og-measure/sample/beats.json +1 -0
- package/themes/og-measure/sample/index.html +11 -0
- package/themes/og-measure/sample/kf.json +66 -0
- package/themes/og-measure/sample/lib/data.ts +62 -0
- package/themes/og-measure/sample/main.ts +9 -0
- package/themes/og-measure/sample/measure.ts +78 -0
- package/themes/og-measure/sample/scenes/end.ts +47 -0
- package/themes/og-measure/sample/scenes/gather.ts +51 -0
- package/themes/og-measure/sample/scenes/models.ts +34 -0
- package/themes/og-measure/sample/scenes/one.ts +39 -0
- package/themes/og-measure/sample/scenes/price.ts +56 -0
- package/themes/og-measure/sample/style.json +97 -0
- package/themes/og-measure/slots.ts +88 -0
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +97 -0
- package/themes/og-measure/theme.json +79 -0
- package/themes/riso-zine/THEME.md +43 -0
- package/themes/riso-zine/effects.ts +321 -0
- package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
- package/themes/riso-zine/sample/beats.json +1 -0
- package/themes/riso-zine/sample/index.html +11 -0
- package/themes/riso-zine/sample/kf.json +59 -0
- package/themes/riso-zine/sample/lib/og.ts +29 -0
- package/themes/riso-zine/sample/lib/shot.ts +86 -0
- package/themes/riso-zine/sample/lib/zine.ts +42 -0
- package/themes/riso-zine/sample/main.ts +9 -0
- package/themes/riso-zine/sample/scenes/end.ts +61 -0
- package/themes/riso-zine/sample/scenes/gather.ts +62 -0
- package/themes/riso-zine/sample/scenes/models.ts +52 -0
- package/themes/riso-zine/sample/scenes/price.ts +82 -0
- package/themes/riso-zine/sample/style.json +99 -0
- package/themes/riso-zine/slots.ts +81 -0
- package/themes/riso-zine/still.png +0 -0
- package/themes/riso-zine/style.json +99 -0
- package/themes/riso-zine/theme.json +75 -0
- package/tools/beats/beats.py +372 -0
- package/tools/beats/requirements.txt +3 -0
- package/tools/lyrics/align.py +164 -0
- package/tools/lyrics/requirements.txt +9 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { captionMode } from "@kasaframe/motion/captions";
|
|
3
|
+
import { bundleFor, MENU, type Dress } from "@kasaframe/motion/menu";
|
|
4
|
+
import { questionsMarkdown, type Question } from "@kasaframe/gen";
|
|
5
|
+
import { loadTheme } from "@kasaframe/themes";
|
|
6
|
+
import type { GenSpec, ShotSpec } from "./types.ts";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const repo = fileURLToPath(new URL("../../../", import.meta.url));
|
|
11
|
+
|
|
12
|
+
export interface ProjectScene {
|
|
13
|
+
id: string;
|
|
14
|
+
from: number;
|
|
15
|
+
dur: number;
|
|
16
|
+
file: string;
|
|
17
|
+
role: string;
|
|
18
|
+
idea: string;
|
|
19
|
+
assets?: string[];
|
|
20
|
+
// kf new 가 고른 연출 수 — 뼈대 장면 파일의 DRESS 와 같다
|
|
21
|
+
dress?: Dress;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProjectFile {
|
|
25
|
+
template: string;
|
|
26
|
+
theme?: string;
|
|
27
|
+
title: string;
|
|
28
|
+
fps: number;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
duration: number;
|
|
32
|
+
music: string | null;
|
|
33
|
+
beats: string | null;
|
|
34
|
+
captions: string;
|
|
35
|
+
render?: { motionBlur?: number; shutter?: number };
|
|
36
|
+
// kf narrate 가 적는다 — voice 가 있으면 내레이션 단계를 지났다
|
|
37
|
+
voice?: string;
|
|
38
|
+
narration?: string | null;
|
|
39
|
+
safeArea: { top: number; bottom: number; left: number; right: number };
|
|
40
|
+
text: { policy: string; maxOnScreen: number; minPx: number };
|
|
41
|
+
scenes: ProjectScene[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readProject(dir: string): ProjectFile {
|
|
45
|
+
return JSON.parse(readFileSync(join(dir, "kf.json"), "utf8"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 모델이 부품 이름을 지어내지 않게 실제로 내보낸 함수의 첫 줄(인자·반환 타입)만 모은다
|
|
49
|
+
function signatures(pkg: string): string[] {
|
|
50
|
+
const src = join(repo, "packages", pkg, "src");
|
|
51
|
+
if (!existsSync(src)) return [];
|
|
52
|
+
const out: string[] = [];
|
|
53
|
+
for (const f of readdirSync(src).filter((x) => x.endsWith(".ts") && !x.endsWith(".test.ts") && x !== "index.ts" && x !== "captions.ts")) {
|
|
54
|
+
const text = readFileSync(join(src, f), "utf8");
|
|
55
|
+
for (const m of text.matchAll(/^export (?:function|const) (\w+)[^\n]*/gm)) {
|
|
56
|
+
const line = m[0].replace(/\s*\{\s*$/, "").replace(/^export (function|const) /, "");
|
|
57
|
+
if (line.length < 220) out.push(line);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const TEXT_POLICY: Record<string, string> = {
|
|
64
|
+
none: "화면에 문장을 쓰지 않는다. 기능 낱말 하나·제품 이름·끝 카드만.",
|
|
65
|
+
"terms-only": "화면 글은 전문 낱말과 숫자만(한 번에 몇 개까지). 말은 자막 트랙으로 따로 간다.",
|
|
66
|
+
captions: "소리 없이도 읽히게 — 들릴 말은 captions.json 에 쓰면 엔진이 화면 아래 자막 판에 얹는다. 장면 글은 핵심 낱말만.",
|
|
67
|
+
lyrics: "가사는 키네틱 타이포로 — 부른 낱말이 부른 시각에 뜨고, 뜻을 그림·움직임으로 받친다. 슬로건 금지는 가사에 걸지 않는다.",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function sceneBeats(dir: string, kf: ProjectFile, s: ProjectScene): string {
|
|
71
|
+
if (!kf.beats || !existsSync(join(dir, kf.beats))) return "곡 없음";
|
|
72
|
+
const b = JSON.parse(readFileSync(join(dir, kf.beats), "utf8"));
|
|
73
|
+
const inside = (xs: number[] = []) => xs.filter((t) => t >= s.from && t < s.from + s.dur).map((t) => +(t - s.from).toFixed(2));
|
|
74
|
+
const drops = inside(b.drops);
|
|
75
|
+
const bars = inside(b.bars ?? b.downbeats);
|
|
76
|
+
return `${b.bpm?.toFixed?.(1) ?? "?"} BPM · 장면 안 드롭 ${drops.length ? drops.join(", ") + "초" : "없음"} · 마디 ${bars.slice(0, 12).join(", ")}${bars.length > 12 ? " …" : ""}초`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface PurposeRules {
|
|
80
|
+
staticSec: { warn: number; error: number };
|
|
81
|
+
beatMove: "every-beat" | "drop-sections";
|
|
82
|
+
maxFreezeSec: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 틀(templates/<용도>/template.json motion)이 기본값을 덮는다 — kf check 도 같은 값을 읽는다
|
|
86
|
+
function purposeRules(id: string): PurposeRules {
|
|
87
|
+
const file = join(repo, "templates", id, "template.json");
|
|
88
|
+
const m = existsSync(file) ? (JSON.parse(readFileSync(file, "utf8")).motion ?? {}) : {};
|
|
89
|
+
return { staticSec: m.staticSec ?? { warn: 1.5, error: 3 }, beatMove: m.beatMove ?? "every-beat", maxFreezeSec: m.maxFreezeSec ?? 0.1 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 용도별 표 — 같은 값을 쓰는 틀끼리 한 줄로
|
|
93
|
+
export function purposeTable(): string[] {
|
|
94
|
+
const dir = join(repo, "templates");
|
|
95
|
+
const ids = existsSync(dir) ? readdirSync(dir).filter((d) => existsSync(join(dir, d, "template.json"))) : [];
|
|
96
|
+
const groups = new Map<string, string[]>();
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
const r = purposeRules(id);
|
|
99
|
+
const key = `같은 구도 ${r.staticSec.warn}초 경고·${r.staticSec.error}초 오류 · 큰 움직임 ${r.beatMove === "every-beat" ? "박마다" : "후렴·드롭 구간만(벌스는 화면 안 움직임으로 충분)"} · 정지 ${r.maxFreezeSec}초 금지`;
|
|
100
|
+
groups.set(key, [...(groups.get(key) ?? []), id]);
|
|
101
|
+
}
|
|
102
|
+
return [...groups].map(([k, v]) => `- ${v.join("·")}: ${k}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function dropSections(dir: string, kf: ProjectFile): string {
|
|
106
|
+
if (!kf.beats || !existsSync(join(dir, kf.beats))) return "곡이 없어 드롭 구간도 없다 — 화면 안 움직임으로 간다";
|
|
107
|
+
const b = JSON.parse(readFileSync(join(dir, kf.beats), "utf8"));
|
|
108
|
+
const secs: { t: number; kind: string }[] = b.sections ?? [];
|
|
109
|
+
const spans: { t: number; to: number }[] = [];
|
|
110
|
+
secs.forEach((x, i) => {
|
|
111
|
+
if (x.kind !== "drop") return;
|
|
112
|
+
const to = secs[i + 1]?.t ?? kf.duration;
|
|
113
|
+
const last = spans[spans.length - 1];
|
|
114
|
+
if (last && Math.abs(last.to - x.t) < 0.05) last.to = to;
|
|
115
|
+
else spans.push({ t: x.t, to });
|
|
116
|
+
});
|
|
117
|
+
return spans.length ? spans.map((x) => `${x.t.toFixed(2)}–${x.to.toFixed(2)}초`).join(", ") : "드롭 구간이 잡히지 않았다";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 장면을 채우는 모델(Claude Code 든 OG 의 아무 모델이든)이 읽는 한 장. 사용서(docs/AGENTS.md)가 있으면 뒤에 붙인다
|
|
121
|
+
export function projectBrief(dir: string): string {
|
|
122
|
+
const kf = readProject(dir);
|
|
123
|
+
const style = JSON.parse(readFileSync(join(dir, "style.json"), "utf8"));
|
|
124
|
+
const plan = existsSync(join(dir, "plan.json")) ? JSON.parse(readFileSync(join(dir, "plan.json"), "utf8")) : {};
|
|
125
|
+
const rules = purposeRules(kf.template);
|
|
126
|
+
const tplFile = join(repo, "templates", kf.template, "template.json");
|
|
127
|
+
const tplMotion = existsSync(tplFile) ? (JSON.parse(readFileSync(tplFile, "utf8")).motion ?? {}) : {};
|
|
128
|
+
const colors = Object.keys(style.colors ?? {}).map((k: string) => `--kf-${k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`);
|
|
129
|
+
const core = signatures("core");
|
|
130
|
+
const motion = signatures("motion");
|
|
131
|
+
const agents = join(repo, "docs", "AGENTS.md");
|
|
132
|
+
const lines = [
|
|
133
|
+
`# ${kf.title || plan.request || "kasaframe 프로젝트"} — 장면 작성 지시서`,
|
|
134
|
+
"",
|
|
135
|
+
...questionsMarkdown(plan.asks ?? [], (flags) => `npx kf make --answer ${dir} ${flags}`),
|
|
136
|
+
...narrationSection(dir, kf, plan),
|
|
137
|
+
`**이 영상의 규칙 — 용도 ${kf.template}(틀이 기본값을 덮는다, kf check 가 같은 값으로 잡는다)**`,
|
|
138
|
+
`1. 화면은 ${rules.maxFreezeSec}초(60fps 6프레임) 넘게 멈추지 않는다 — 끝 카드·큰 숫자·로고도 예외 없이 화면 안 움직임(숫자 굴러가기·칸 채우기·반짝임·글자 흐름·바탕 흐름)으로 계속 살아 있게.`,
|
|
139
|
+
`2. 같은 구도는 ${rules.staticSec.warn}초 넘기지 않는다(${rules.staticSec.error}초 넘으면 오류) — 새 요소·칸 채우기·숫자 키우기·컷·전환으로 바꾼다.`,
|
|
140
|
+
rules.beatMove === "every-beat"
|
|
141
|
+
? "3. 미세하게 흔들리는 것만으로 때우지 않는다 — 눈에 보이는 큰 움직임이 박마다 하나 이상."
|
|
142
|
+
: `3. 눈에 보이는 큰 움직임을 박마다 요구하는 건 후렴·드롭 구간(beats.json sections 의 drop — ${dropSections(dir, kf)})뿐이다. 벌스·브레이크는 화면 안 움직임(가사·숫자·바탕 흐름)으로 충분하다 — 절제했다가 드롭에서 터뜨린다.`,
|
|
143
|
+
"4. **카메라는 이유가 있을 때만** 움직인다 — 기본은 정지, 주인공은 안전 영역 가운데. 드러냄에 다가가기(`stage.camera.pushIn`)·비교에 물러나기(`pullOut`)·주인공 따라가기(`follow`)·드롭에 한 번 툭(`impact`), 끝나면 선다. 화면 전체를 사인으로 흔들거나 박마다 자세를 바꾸지 않는다(kf check `camera.wander`·`layout.edge`). 회전·원근 기울기는 0.",
|
|
144
|
+
"5. **섬광은 1초에 3번까지**(kf check frame.flash — 빛 민감 발작). 화면 25% 넘게 번쩍이는 건 두 박·한 마디에 한 번, 빨강 전체 깜빡임·대비 큰 줄무늬 연타·어둠↔흰 화면 컷 이어 붙이기는 쓰지 않는다.",
|
|
145
|
+
"",
|
|
146
|
+
"용도별 기본값(templates/<용도>/template.json motion):",
|
|
147
|
+
...purposeTable(),
|
|
148
|
+
"",
|
|
149
|
+
"그림 보기: `npx kf look <폴더>` 가 찍는 장면 모음(out/look-sheet.png)을 Read 로 직접 보고 장면별 확인표(out/look.md)대로 고친다. **Read 로 그림이 안 보이면(눈 없는 모델)만** `npx kf look <폴더> --vision-model google/gemini-3.5-flash [--ask \"<볼 것>\"]` — 그림 보는 모델이 장면마다 글로 알려 준다.",
|
|
150
|
+
"",
|
|
151
|
+
`요청: ${plan.request ?? "(없음)"}`,
|
|
152
|
+
`용도 ${kf.template} · ${kf.width}×${kf.height} · ${kf.fps}fps · ${kf.duration}초 · 스타일 ${style.name}(${style.scheme}) · 곡 ${kf.music ?? "없음"} · 자막 ${kf.captions}`,
|
|
153
|
+
`안전 영역(글은 이 안에): 위 ${kf.safeArea.top}px · 아래 ${kf.safeArea.bottom}px · 왼쪽 ${kf.safeArea.left}px · 오른쪽 ${kf.safeArea.right}px`,
|
|
154
|
+
`화면 글: ${TEXT_POLICY[kf.text?.policy] ?? kf.text?.policy} 글자 크기 최소 ${kf.text?.minPx}px.`,
|
|
155
|
+
`움직임 규칙(style.json motion 위에 틀 motion): ${JSON.stringify({ ...style.motion, ...tplMotion })}`,
|
|
156
|
+
...(style.notes?.length ? ["디자인 메모: " + style.notes.join(" / ")] : []),
|
|
157
|
+
"",
|
|
158
|
+
"## 장면 파일 꼴 — 이 꼴만 쓴다",
|
|
159
|
+
"",
|
|
160
|
+
"```ts",
|
|
161
|
+
'import { applyStyle, clip, ease, type SceneCtx } from "@kasaframe/core";',
|
|
162
|
+
"export default {",
|
|
163
|
+
" build(root: HTMLElement, ctx: SceneCtx) {",
|
|
164
|
+
" const box = document.createElement(\"div\"); // root.style.display 는 엔진이 켜고 끈다 — 배치는 box 에",
|
|
165
|
+
" box.style.cssText = \"position:absolute;inset:0;overflow:hidden;background:var(--kf-bg)\";",
|
|
166
|
+
" root.append(box);",
|
|
167
|
+
" // DOM 은 여기서 한 번만 짓는다",
|
|
168
|
+
" return (t: number) => { /* t = 장면 안 초(0..ctx.dur). 같은 t 면 같은 화면 */ };",
|
|
169
|
+
" },",
|
|
170
|
+
"};",
|
|
171
|
+
"```",
|
|
172
|
+
"",
|
|
173
|
+
"- 시각 함수 밖에서 상태를 쌓지 않는다(앞 프레임 값을 기억하지 않는다). 탭 여러 개가 아무 시각이나 찍는다.",
|
|
174
|
+
"- `Math.random`·`Date.now`·`performance.now`·`setTimeout` 금지 — 난수는 `rand(seed, i)`, 시간은 `t` 만.",
|
|
175
|
+
"- 글자에 `undefined`·`NaN`·`null` 이 찍히지 않게 값은 미리 문자열로 만든다. 이모지 금지(아이콘은 인라인 SVG).",
|
|
176
|
+
"- 장면이 1초 넘게 멈추지 않게 한다(아주 느린 다가가기라도). 빈 화면으로 시작·끝내지 않는다.",
|
|
177
|
+
"- **화면을 채운다**(kf check layout.fill): 주 요소가 안전 영역을 크게 차지한다 — 세로 화면은 폭의 70% 넘게, 글은 큰 글자 단계(hero·title)로. 가운데 작은 그림 하나에 나머지가 흰 여백이면 실패다.",
|
|
178
|
+
"- 구도 판정(kf check layout.static)은 칸 평균 밝기와 화면 안 내용(글·칸의 센 가장자리)으로 본다 — 숫자가 구르고 칸이 차면 바뀐 것이다. 카메라 샷이나 떠다니는 장식으로 메우지 않는다.",
|
|
179
|
+
"- `../lib/stage.ts` 의 `makeStage(root, ctx, { dress })`(정지 바탕·연출 수·정지 카메라 `stage.camera`)·`stage.say(낱말)`(글 수 + 박마다 차는 강조 띠)·`makeCamera(layer, ctx, { frame })`(자기 카메라 층에)·`bigWord`·`fitPx`·`cutTimes`·`lastT` 를 써도 된다 — 위 규칙을 처음부터 넘게 짜 둔 도구다. 카메라가 요소를 겨누면 `stage.tick(t)` 은 내용을 칠한 뒤 맨 끝에.",
|
|
180
|
+
"- 박에 붙이는 움직임은 `ctx.beats`(장면 시작 0 기준: `ctx.beats.drops`, `ctx.beats.nearestBeat(t)`, `ctx.beats.pulse(t, {...})`). 시각을 BPM 으로 곱해 만들지 않는다.",
|
|
181
|
+
"- 자산은 `ctx.asset(\"gen/<id>.png\")` 처럼 assets/ 기준 경로로. 없는 파일을 가리키지 않는다.",
|
|
182
|
+
"- 색·글꼴·크기는 CSS 변수로: " + [...colors, "--kf-font-sans", "--kf-font-display", "--kf-font-mono", "--kf-size-hero", "--kf-size-title", "--kf-size-body", "--kf-size-caption", "--kf-radius-md", "--kf-shadow-soft"].join(" "),
|
|
183
|
+
"",
|
|
184
|
+
"## 쓸 수 있는 함수",
|
|
185
|
+
"",
|
|
186
|
+
"`@kasaframe/core`:",
|
|
187
|
+
...core.map((s) => `- ${s}`),
|
|
188
|
+
"",
|
|
189
|
+
...(motion.length ? ["`@kasaframe/motion`(움직임 부품 — 숫자만 넣으면 Style 이나 상태를 돌려준다. applyStyle 로 붙인다):", ...motion.map((s) => `- ${s}`), ""] : []),
|
|
190
|
+
...dressSection(kf),
|
|
191
|
+
"## 장면",
|
|
192
|
+
"",
|
|
193
|
+
...kf.scenes.flatMap((s) => [
|
|
194
|
+
`### ${s.id} — ${s.file} (${s.from.toFixed(2)}–${(s.from + s.dur).toFixed(2)}초, ${s.dur.toFixed(2)}초, 역할 ${s.role})`,
|
|
195
|
+
`- 할 일: ${s.idea}`,
|
|
196
|
+
...(s.role === "seam" ? ["- 이 장면 파일은 고치지 않는다 — 3D 전환(transition3d)이 앞뒤 화면을 이어 준다"] : []),
|
|
197
|
+
...(s.dress ? [`- 연출 수(뼈대에 걸려 있다): ${dressLine(s.dress)}`] : []),
|
|
198
|
+
`- 박: ${sceneBeats(dir, kf, s)}`,
|
|
199
|
+
...(s.assets?.length ? [`- 자산: ${s.assets.map((a) => a.replace(/^gen:(img|vid):(.+)$/, (_, k, id) => `assets/gen/${id}.${k === "img" ? "png" : "mp4"}`)).join(", ")}`] : []),
|
|
200
|
+
"",
|
|
201
|
+
]),
|
|
202
|
+
...genSection(dir, kf, plan),
|
|
203
|
+
...videoSection(dir, kf, plan),
|
|
204
|
+
...shotSection(dir, kf, plan),
|
|
205
|
+
...themeSection(kf.theme, plan),
|
|
206
|
+
...(existsSync(join(dir, "lyrics.json"))
|
|
207
|
+
? ["## 가사(lyrics.json — 낱말 시각까지)", "", '`import.meta.glob("../lyrics.json", { eager: true, import: "default" })` 로 읽는다(꼴 `{ lines: [{ text, start, end, words?: [{ w, start, end }] }] }`, 시각은 영상 전체 기준 초 — 장면 안 시각은 `start - ctx.from`). 낱말이 없으면 줄을 글자 수 비율로 나눈다.', ""]
|
|
208
|
+
: []),
|
|
209
|
+
...(existsSync(join(dir, "assets", "in"))
|
|
210
|
+
? ["## 받은 자산(사람이 준 것 — 쓸 수 있으면 쓴다)", "", ...readdirSync(join(dir, "assets", "in")).map((f) => `- \`ctx.asset("in/${f}")\``), ""]
|
|
211
|
+
: []),
|
|
212
|
+
...(captionMode(kf.captions) !== "none"
|
|
213
|
+
? [
|
|
214
|
+
"## 자막",
|
|
215
|
+
"",
|
|
216
|
+
'들릴 말은 프로젝트 `captions.json` 에 `[{"scene": "장면 id", "text": "짧은 마디를 쉼표로"}]` 로 쓴다 — `kf make --finish` 가 out/captions.ko.srt·vtt 로 만든다. 장면 파일에는 문장을 쓰지 않는다.',
|
|
217
|
+
captionMode(kf.captions) === "on"
|
|
218
|
+
? `이 영상은 자막을 화면에도 얹는다 — 엔진이 장면 위에 판을 깐다(안전 영역 아래쪽 가운데, 두 줄까지). 그 자리(아래 가장자리에서 ${kf.safeArea.bottom}px 위 언저리)에 장면의 큰 글을 두지 않는다.`
|
|
219
|
+
: "화면에는 얹지 않는다(kf render --captions on 이면 그 판만 얹는다).",
|
|
220
|
+
"",
|
|
221
|
+
]
|
|
222
|
+
: []),
|
|
223
|
+
...(existsSync(agents) ? ["## 사용서(docs/AGENTS.md)", "", readFileSync(agents, "utf8")] : []),
|
|
224
|
+
];
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 내레이션을 켠 틀(쇼츠·설명) — 장면보다 말이 먼저다. 목소리 길이가 장면 길이를 정하니 장면은 kf narrate 뒤에 쓴다
|
|
229
|
+
function narrationSection(dir: string, kf: ProjectFile, plan: { voice?: string; answers?: Record<string, string>; asks?: { id: string }[] }): string[] {
|
|
230
|
+
const tplFile = join(repo, "templates", kf.template, "template.json");
|
|
231
|
+
const tpl = existsSync(tplFile) ? JSON.parse(readFileSync(tplFile, "utf8")) : {};
|
|
232
|
+
if (!tpl.narration?.on) return [];
|
|
233
|
+
const asked = (id: string) => plan.asks?.some((q) => q.id === id);
|
|
234
|
+
const voice = kf.voice ?? plan.voice ?? plan.answers?.voice;
|
|
235
|
+
const music = kf.music ?? plan.answers?.music;
|
|
236
|
+
const done = kf.voice !== undefined;
|
|
237
|
+
const mix = tpl.audio ? `말할 때 곡은 목소리보다 ${-tpl.audio.musicDb}dB 안팎 아래(덕킹), 끝 ${tpl.audio.fadeOut}초 페이드, ${tpl.audio.lufs} LUFS — kf render 가 섞는다` : "";
|
|
238
|
+
return [
|
|
239
|
+
`## 내레이션 먼저 — 장면보다 앞${done ? "(됐다)" : ""}`,
|
|
240
|
+
"",
|
|
241
|
+
`목소리 ${asked("voice") ? "아직 — 맨 위 질문의 답" : (voice ?? "틀 기본")} · 곡 ${asked("music") ? "아직 — 맨 위 질문의 답" : (music ?? "없음")}${tpl.mood ? `(분위기 ${tpl.mood})` : ""}${mix ? ` · ${mix}` : ""}`,
|
|
242
|
+
"",
|
|
243
|
+
...(done
|
|
244
|
+
? [
|
|
245
|
+
`kf narrate 를 지났다 — 장면 시각(kf.json)이 목소리 길이에 맞춰졌고 captions.json 에 낱말 시각(\`words\`, 영상 기준 초)이 있다. 문장을 고치면 \`npx kf narrate ${dir}\` 를 다시 부른다(같은 문장은 캐시라 0원).`,
|
|
246
|
+
]
|
|
247
|
+
: [
|
|
248
|
+
`1. \`captions.json\` 에 장면마다 들릴 말을 쓴다 — \`[{"scene": "hook", "text": "..."}]\`. 차분한 설명체, 짧은 마디는 쉼표로, 문장은 마침표로. 한국어 차분한 낭독은 1초에 4–5글자(띄어쓰기·문장부호 빼고) — 지금 장면 길이에 가깝게, 훅은 한 마디(2초 안). 끝 장면·이음매는 비워도 된다. 말은 소리·자막으로 가고 화면 글 정책은 그대로다.`,
|
|
249
|
+
`2. \`npx kf narrate ${dir}\` — 장면마다 읽혀 장면 길이 = 앞 숨 ${tpl.narration.lead}초 + 말 + 뒤 숨 ${tpl.narration.tail}초 로 맞추고(곡이 있으면 다음 박에 붙인다), 자막 시각을 목소리 시각으로 적는다. 요청에 길이가 있으면 빠르기를 0.9–1.1 안에서만 맞춘다.`,
|
|
250
|
+
"3. 그다음 장면을 쓴다 — kf.json 장면 시각이 바뀌었으니 다시 읽는다(장면은 `ctx.dur` 로 짠다).",
|
|
251
|
+
]),
|
|
252
|
+
"- 말과 화면을 맞출 때: captions.json 의 `words` 가 영상 기준 초다 — 그 낱말이 들리는 순간에 같은 낱말·도식이 서게(`w.from - ctx.from` 이 장면 안 시각). `import.meta.glob(\"../captions.json\", { eager: true, import: \"default\" })` 로 읽는다.",
|
|
253
|
+
"",
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function dressLine(d: Dress): string {
|
|
258
|
+
const parts = [`바탕 \`${d.bg}\``, `글 \`${d.text}\``];
|
|
259
|
+
if (d.wipeIn !== "none") parts.push(`들어올 때 \`${d.wipeIn}\`가 걷힌다`);
|
|
260
|
+
if (d.wipeOut !== "none") parts.push(`나갈 때 \`${d.wipeOut}\`로 덮는다`);
|
|
261
|
+
return parts.join(" · ");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 대충 한 요청에서도 연출이 밋밋하지 않게 — 뼈대가 이미 건 수를 알리고, 바꿀 때 고를 메뉴(이 용도·테마 묶음 먼저)를 준다
|
|
265
|
+
function dressSection(kf: ProjectFile): string[] {
|
|
266
|
+
const tplFile = join(repo, "templates", kf.template, "template.json");
|
|
267
|
+
const tpl = existsSync(tplFile) ? JSON.parse(readFileSync(tplFile, "utf8")) : {};
|
|
268
|
+
if (!tpl.moves) return [];
|
|
269
|
+
const b = bundleFor(tpl.moves, kf.theme);
|
|
270
|
+
const blur = kf.render?.motionBlur ?? 0;
|
|
271
|
+
const kinds = [
|
|
272
|
+
["bg", "바탕 — 정지만(뜻 없이 떠도는 장식은 쓰지 않는다)", b.bg],
|
|
273
|
+
["text", "글 — 안전 영역 안, 박마다 내용이 바뀐다(낱말을 WORDS 에)", b.text],
|
|
274
|
+
["wipe", "경계 덮기 — 앞 장면 끝을 덮고 뒤 장면 처음에 걷힌다. 두 장면 파일의 wipeOut·wipeIn 을 같은 이름으로", b.wipe],
|
|
275
|
+
] as const;
|
|
276
|
+
return [
|
|
277
|
+
"## 연출 수 — 뼈대에 이미 걸려 있다",
|
|
278
|
+
"",
|
|
279
|
+
`\`kf new\` 가 장면마다 바탕·글·경계 덮기를 이 용도${kf.theme ? `·테마(${kf.theme})` : ""} 묶음에서 골라 장면 파일 \`DRESS\` 에 넣었다(요청 글이 씨앗 — 같은 요청이면 같은 수). 장면을 채울 때 **수는 두고 낱말(\`WORDS\`)과 내용을 바꾼다** — 수가 쉬지 않기·같은 구도·화면 채움 검사를 이미 넘는다. **화면에서 움직이는 것은 모두 내용 하나를 뜻한다** — 떠다니는 방울·판·큐브·흩뿌린 입자 같은 뜻 없는 장식은 넣지 않고, 쉬지 않기는 숫자 굴러가기·칸 채우기·연결선·강조로 채운다. 바꾸려면 아래 이름으로, 이음매(seam)는 3D 전환(dive 2판 — 화면 속으로 빠르게 다이브)이 맡는다.`,
|
|
280
|
+
...(blur > 1 ? [`굽기: 모션 블러 ${blur}장·셔터 ${kf.render?.shutter ?? 180}°(kf.json \`render\`) — 빠르게 움직이는 것만 번지고 서 있는 글은 그대로다. 손으로 끌 땐 \`kf render --motion-blur 0\`.`] : []),
|
|
281
|
+
"",
|
|
282
|
+
...kinds.flatMap(([kind, head, mine]) => [
|
|
283
|
+
`- ${head}: ${MENU.filter((m) => m.kind === kind)
|
|
284
|
+
.sort((x, y) => Number(!mine.includes(x.name)) - Number(!mine.includes(y.name)))
|
|
285
|
+
.map((m) => `${mine.includes(m.name) ? "**" : ""}\`${m.name}\`${mine.includes(m.name) ? "**" : ""} ${m.does}`)
|
|
286
|
+
.join(" · ")}`,
|
|
287
|
+
]),
|
|
288
|
+
"",
|
|
289
|
+
"굵은 이름이 이 영상의 묶음이다. 수만 따로 보려면 `examples/moves/<이름>/`, 메뉴 밖 수(곡선 글·도형 변신·만화 칸·찢기…)까지 전체는 `examples/moves/README.md`. 장면을 직접 짤 때도 `import { slamWords, sunburst, … } from \"@kasaframe/motion/moves\"` 로 같은 수를 쓴다.",
|
|
290
|
+
"",
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 테마를 골랐으면 그 비유·금지·효과 쓰는 꼴을 그대로 싣는다(SPEC 9). auto 인데 맞는 테마가 없으면 강한 모델이 새 테마를 draft 로 써 넣을 수 있다
|
|
295
|
+
function themeSection(id: string | undefined, plan: { themeCandidates?: { id: string; why: string }[]; request?: string; asks?: { id: string }[] }): string[] {
|
|
296
|
+
if (id) {
|
|
297
|
+
const t = loadTheme(id);
|
|
298
|
+
const pick = plan.themeCandidates?.find((c) => c.id === id);
|
|
299
|
+
return [
|
|
300
|
+
`## 테마 ${t.name}(${t.id}) — ${t.metaphor}`,
|
|
301
|
+
"",
|
|
302
|
+
...(plan.asks?.some((q) => q.id === "theme") ? ["지금은 auto 로 깔아 둔 테마다 — 맨 위 질문의 답을 넣으면(`kf make --answer … --theme <id>`) 테마와 이 지시서가 바뀐다. 장면은 그다음에 쓴다.", ""] : []),
|
|
303
|
+
...(pick ? [`고른 이유: ${pick.why}`, ""] : []),
|
|
304
|
+
`효과는 복사하지 말고 가져다 쓴다: \`import { fx } from "@kasaframe/themes/fx"; const { ${t.effects.map((e) => e.name).join(", ")} } = fx("${t.id}");\``,
|
|
305
|
+
"",
|
|
306
|
+
...t.effects.map((e) => `- \`${e.name}\`(${e.kind}) — ${e.does} · \`${e.use}\``),
|
|
307
|
+
"",
|
|
308
|
+
...(plan.themeCandidates && plan.themeCandidates.length > 1 ? [`다른 후보: ${plan.themeCandidates.slice(1).map((c) => c.id).join(" · ")} — 첫 장면을 각 테마로 찍은 그림은 out/theme-<id>.png`, ""] : []),
|
|
309
|
+
t.guide,
|
|
310
|
+
"",
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
if (plan.themeCandidates && !plan.themeCandidates.length) {
|
|
314
|
+
return [
|
|
315
|
+
"## 테마 — 맞는 approved 테마가 없다",
|
|
316
|
+
"",
|
|
317
|
+
"프리셋 style 로 간다. 이 내용에 맞는 비유가 뚜렷하면 SPEC 계약 9 대로 `themes/<새 id>/` 에 테마 팩(theme.json status draft · THEME.md · style.json · effects.ts 5–10개)을 써 넣어도 된다 — 사람이 견본을 보고 OK 하기 전까지는 draft 라 이 프로젝트에는 쓰지 않는다.",
|
|
318
|
+
"",
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type BriefPlan = { gen?: GenSpec[]; shots?: ShotSpec[]; asks?: Question[]; answers?: Record<string, string>; aspect?: string };
|
|
325
|
+
|
|
326
|
+
const pending = (plan: BriefPlan, id: string) => (plan.asks ?? []).some((q) => q.id === id);
|
|
327
|
+
|
|
328
|
+
// 막연한 자동 프롬프트는 엉뚱한 그림을 냈다 — 아직 없는 그림은 목록으로 내고 세션이 구체적인 프롬프트를 써서 고른 공급자로 만든다
|
|
329
|
+
function genSection(dir: string, kf: ProjectFile, plan: BriefPlan): string[] {
|
|
330
|
+
const todo = (plan.gen ?? []).filter((g) => g.kind === "img" && !existsSync(join(dir, "assets", "gen", `${g.id}.png`)));
|
|
331
|
+
if (!todo.length) return [];
|
|
332
|
+
const provider = plan.answers?.image;
|
|
333
|
+
return [
|
|
334
|
+
pending(plan, "image") ? "## 만들 그림 — 사람이 그림 생성을 고른 뒤에(맨 위 「사용자에게 먼저 물을 것」)" : `## 만들 그림 — ${provider ?? "기본 공급자"}로(고른 답)`,
|
|
335
|
+
"",
|
|
336
|
+
"받은 자산·실제 화면 오리기·도형으로 되면 만들지 않는다. 만들 땐 요청 내용으로 **구체적인** 프롬프트를 써서(무엇이·어떤 구도로·어떤 색·글자 없음):",
|
|
337
|
+
"",
|
|
338
|
+
...todo.map((g) => `- \`${g.id}\`(장면 ${g.scene ?? "-"}) — 틀이 제안한 뜻: ${g.prompt}\n \`npx kf gen img "<구체적 프롬프트>" --id ${g.id} --size ${g.size ?? `${kf.width}x${kf.height}`}${provider ? ` --provider ${provider}` : ""} --project ${dir}\` → \`ctx.asset("gen/${g.id}.png")\``),
|
|
339
|
+
"",
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 캐릭터가 실제로 움직여야 하는 컷 — 영상 모델은 돈이 들어 고른 답이 있을 때만 부른다. 이음매 영상은 kf make --finish 가 앞뒤 화면으로 만든다
|
|
344
|
+
function videoSection(dir: string, kf: ProjectFile, plan: BriefPlan): string[] {
|
|
345
|
+
const cuts = (plan.gen ?? []).filter((g) => g.kind === "vid" && !g.seam && !existsSync(join(dir, "assets", "gen", `${g.id}.mp4`)));
|
|
346
|
+
if (!cuts.length) return [];
|
|
347
|
+
const provider = plan.answers?.video;
|
|
348
|
+
const aspect = plan.aspect ?? (kf.width > kf.height ? "16:9" : kf.width < kf.height ? "9:16" : "1:1");
|
|
349
|
+
return [
|
|
350
|
+
pending(plan, "video") ? "## 영상 모델 컷 — 사람이 영상 모델을 고른 뒤에(맨 위 「사용자에게 먼저 물을 것」)" : `## 영상 모델 컷 — ${provider}로(고른 답, 돈이 든다)`,
|
|
351
|
+
"",
|
|
352
|
+
"캐릭터가 실제로 움직여야 하는 컷이다. 그림을 먼저 만들었으면 그걸 첫 프레임으로 준다. 받은 클립은 장면에 `<video data-kf-from=\"<장면 시작 초>\" muted playsinline>` 로 둔다(장면이 영상보다 길면 다른 화면 안 움직임으로 잇는다).",
|
|
353
|
+
"",
|
|
354
|
+
...cuts.map((g) => `- \`${g.id}\`(장면 ${g.scene}) — ${g.prompt}\n \`npx kf gen vid "<구체적 프롬프트>" --id ${g.id} --dur ${g.dur ?? 5} --aspect ${aspect}${g.first ? ` --first ${dir}/${g.first}` : ""}${provider ? ` --provider ${provider}` : ""} --project ${dir} --yes\` → \`ctx.asset("gen/${g.id}.mp4")\``),
|
|
355
|
+
"",
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 주인공 3D 장면 — Blender 가 있으면 툰 샷을 굽고, 없어서 three 로 대신하기로 했으면 장면 안에서 stage3d 로 짓는다
|
|
360
|
+
function shotSection(dir: string, kf: ProjectFile, plan: BriefPlan): string[] {
|
|
361
|
+
const shots = plan.shots ?? [];
|
|
362
|
+
if (!shots.length) return [];
|
|
363
|
+
const out = ["## 주인공 3D 장면", ""];
|
|
364
|
+
for (const s of shots) {
|
|
365
|
+
const sc = kf.scenes.find((x) => x.id === s.scene);
|
|
366
|
+
const span = sc ? `${sc.from.toFixed(2)}–${(sc.from + sc.dur).toFixed(2)}초` : "";
|
|
367
|
+
const meta = join(dir, "assets", "blender", `${s.id}.json`);
|
|
368
|
+
if (s.tool === "blender" && existsSync(meta)) {
|
|
369
|
+
const m = JSON.parse(readFileSync(meta, "utf8"));
|
|
370
|
+
out.push(`- \`${s.scene}\`(${span}) — 구워 둠: \`<video src="\${ctx.asset("blender/${s.id}.mp4")}" data-kf-from="${m.dataKfFrom}" muted playsinline>\` 를 장면 바탕(또는 주인공 판)으로 두고 그 위에 낱말·숫자를 얹는다`);
|
|
371
|
+
} else if (s.tool === "blender") {
|
|
372
|
+
out.push(
|
|
373
|
+
`- \`${s.scene}\`(${span}) — ${s.idea}`,
|
|
374
|
+
` 1. \`${dir}/${s.file}\` 를 \`tools/blender/kfb.py\` 헬퍼로 짠다 — 쓰는 법 \`tools/blender/README.md\`, 예 \`tools/blender/shots/lighthouse.py\`. 물체는 한 번 짓고 \`update(t)\` 는 t 만 본다, 카메라 열쇠는 \`S.downbeats\`·\`S.drops\` 에(도착하면 선다), 쉬지 않는 몫은 박에 뛰는 물체가 진다. 색은 style.json 잉크.`,
|
|
375
|
+
` 2. \`npx kf blender ${dir}/${s.file} --project ${dir} --scene ${s.scene} --id ${s.id} --still <장면 안 두세 시각>\` 로 몇 장 보고 고친다`,
|
|
376
|
+
` 3. \`npx kf blender ${dir}/${s.file} --project ${dir} --scene ${s.scene} --id ${s.id} --res 50 --step 2\` 로 굽는다(무겁다 — 한 번에) → 명령이 찍어 준 \`<video data-kf-from>\` 값으로 장면에 건다`,
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
out.push(`- \`${s.scene}\`(${span}) — ${s.idea}. Blender 가 없어 three.js 로 — \`import { stage3d, THREE } from "@kasaframe/themes/stage3d"\`: \`const st = stage3d(root, { ctx, shot: (t, cam) => { /* 카메라는 이유 있을 때만 */ } }); st.add((t) => { /* 물체 자세 */ });\` → \`(t) => st.paint(t)\``);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return [...out, ""];
|
|
383
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { captionMetrics, captionMode, toCues, type CaptionLine, type CaptionMode, type Cue } from "@kasaframe/motion/captions";
|
|
4
|
+
import { readProject } from "./brief.ts";
|
|
5
|
+
|
|
6
|
+
// 자막 트랙 — srt(유튜브)·vtt(사이트). OG 쇼츠 scripts/srt3d.py 의 관례를 따른다.
|
|
7
|
+
// 마디 나누기는 화면 자막 판(@kasaframe/motion/captions)과 같은 toCues 라 트랙과 화면이 한 마디도 어긋나지 않는다.
|
|
8
|
+
|
|
9
|
+
export { toCues };
|
|
10
|
+
export type { CaptionLine, Cue };
|
|
11
|
+
|
|
12
|
+
function ts(s: number, sep: string): string {
|
|
13
|
+
const ms = Math.round(s * 1000);
|
|
14
|
+
const p = (n: number, w = 2) => String(n).padStart(w, "0");
|
|
15
|
+
return `${p(Math.floor(ms / 3600000))}:${p(Math.floor(ms / 60000) % 60)}:${p(Math.floor(ms / 1000) % 60)}${sep}${p(ms % 1000, 3)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function srt(cues: Cue[]): string {
|
|
19
|
+
return cues.map((c, i) => `${i + 1}\n${ts(c.from, ",")} --> ${ts(c.to, ",")}\n${c.text}\n`).join("\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function vtt(cues: Cue[]): string {
|
|
23
|
+
return `WEBVTT\n\n${cues.map((c) => `${ts(c.from, ".")} --> ${ts(c.to, ".")}\n${c.text}\n`).join("\n")}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 뮤비 가사. 시각이 붙은 lyrics.json(natsu_mv align_lyrics.py 꼴: {lines:[{text,start,end}]})이 있으면 그대로,
|
|
27
|
+
// 시각 없는 lyrics.txt 뿐이면 인트로·아웃트로를 뺀 구간에 줄 길이 비율로 대충 편다 — 정확한 시각은 보컬 분리(demucs) + 강제 정렬로(큰 모델이라 GPU 있는 기계에서)
|
|
28
|
+
function lyricLines(dir: string, scenes: { id: string; role: string; from: number; dur: number }[]): CaptionLine[] | null {
|
|
29
|
+
const find = (f: string) => [join(dir, f), join(dir, "assets", "in", f)].find((p) => existsSync(p));
|
|
30
|
+
const timed = find("lyrics.json");
|
|
31
|
+
if (timed) {
|
|
32
|
+
const j = JSON.parse(readFileSync(timed, "utf8")) as { lines?: { text: string; start: number; end: number; words?: { w: string; start: number; end: number }[] }[] };
|
|
33
|
+
return (j.lines ?? []).map((l) => ({ from: l.start, to: l.end, text: l.text, words: l.words?.map((w) => ({ text: w.w, from: w.start, to: w.end })) }));
|
|
34
|
+
}
|
|
35
|
+
const plain = find("lyrics.txt");
|
|
36
|
+
if (!plain) return null;
|
|
37
|
+
const lines = readFileSync(plain, "utf8").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
38
|
+
const sung = scenes.filter((s) => !/intro|outro/.test(s.role));
|
|
39
|
+
const total = sung.reduce((a, s) => a + s.dur, 0);
|
|
40
|
+
const chars = lines.reduce((a, l) => a + l.length, 0);
|
|
41
|
+
const out: CaptionLine[] = [];
|
|
42
|
+
let k = 0;
|
|
43
|
+
let acc = 0;
|
|
44
|
+
for (const l of lines) {
|
|
45
|
+
let at = (acc / chars) * total;
|
|
46
|
+
while (k < sung.length - 1 && at > sung[k].dur) {
|
|
47
|
+
at -= sung[k].dur;
|
|
48
|
+
acc -= (sung[k].dur / total) * chars;
|
|
49
|
+
k++;
|
|
50
|
+
}
|
|
51
|
+
const from = sung[k].from + at;
|
|
52
|
+
out.push({ from, to: from + (l.length / chars) * total, text: l });
|
|
53
|
+
acc += l.length;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 프로젝트의 captions.json(또는 가사) → out/captions.ko.srt·vtt. 자막 none 이거나 들릴 말이 없으면 빈 배열. mode 는 kf.json 을 그 판만 덮는다
|
|
59
|
+
export function writeCaptions(dir: string, mode?: CaptionMode): string[] {
|
|
60
|
+
const kf = readProject(dir);
|
|
61
|
+
// captions 칸이 없는 옛 프로젝트는 들릴 말이 있으면 트랙만 낸다
|
|
62
|
+
if ((mode ?? (kf.captions === undefined ? "off" : captionMode(kf.captions))) === "none") return [];
|
|
63
|
+
const scenes = kf.scenes;
|
|
64
|
+
const file = join(dir, "captions.json");
|
|
65
|
+
const lines = existsSync(file) ? (JSON.parse(readFileSync(file, "utf8")) as CaptionLine[]) : lyricLines(dir, scenes);
|
|
66
|
+
if (!lines) return [];
|
|
67
|
+
// 화면 판과 같은 마디로 — style.json captions(글자 크기·줄 수)가 있으면 그 값으로 나눈다
|
|
68
|
+
const stylePath = join(dir, (kf as { style?: string }).style ?? "style.json");
|
|
69
|
+
const look = existsSync(stylePath) ? JSON.parse(readFileSync(stylePath, "utf8")).captions : undefined;
|
|
70
|
+
const m = kf.width && kf.height ? captionMetrics({ width: kf.width, height: kf.height, safe: kf.safeArea, minPx: kf.text?.minPx, look }) : undefined;
|
|
71
|
+
const cues = toCues(lines, scenes, { maxEm: m?.maxEm, duration: kf.duration, maxLines: m?.maxLines });
|
|
72
|
+
if (!cues.length) return [];
|
|
73
|
+
mkdirSync(join(dir, "out"), { recursive: true });
|
|
74
|
+
const out = [join(dir, "out", "captions.ko.srt"), join(dir, "out", "captions.ko.vtt")];
|
|
75
|
+
writeFileSync(out[0], srt(cues));
|
|
76
|
+
writeFileSync(out[1], vtt(cues));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mentions, parseAspect, parseDuration } from "./request.ts";
|
|
2
|
+
import type { Purpose, Template } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export interface DetectInput {
|
|
5
|
+
request: string;
|
|
6
|
+
assets?: string[]; // 받은 파일 경로들
|
|
7
|
+
music?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Ranked {
|
|
11
|
+
purpose: Purpose;
|
|
12
|
+
score: number;
|
|
13
|
+
p: number;
|
|
14
|
+
reasons: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Detection {
|
|
18
|
+
purpose: Purpose;
|
|
19
|
+
confidence: number;
|
|
20
|
+
ranked: Ranked[];
|
|
21
|
+
// 낱말이 하나도 안 걸렸거나 1·2위가 붙어 있으면 사람에게 묻는다
|
|
22
|
+
ambiguous: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const VIDEO = /\.(mp4|mov|webm|mkv|m4v)$/i;
|
|
26
|
+
const TEMPERATURE = 1.5;
|
|
27
|
+
export const ASK_BELOW = 0.5;
|
|
28
|
+
|
|
29
|
+
export function detect(input: DetectInput, templates: Template[]): Detection {
|
|
30
|
+
const { request } = input;
|
|
31
|
+
const dur = parseDuration(request);
|
|
32
|
+
const aspect = parseAspect(request);
|
|
33
|
+
const hasVideo = (input.assets ?? []).some((a) => VIDEO.test(a));
|
|
34
|
+
let anyWord = false;
|
|
35
|
+
|
|
36
|
+
const scored = templates.map((tpl) => {
|
|
37
|
+
let score = 0;
|
|
38
|
+
const reasons: string[] = [];
|
|
39
|
+
for (const w of tpl.detect.words) {
|
|
40
|
+
if (mentions(request, w)) {
|
|
41
|
+
score += 3;
|
|
42
|
+
anyWord = true;
|
|
43
|
+
reasons.push(`"${w}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const w of tpl.detect.hints) {
|
|
47
|
+
if (mentions(request, w)) {
|
|
48
|
+
score += 1;
|
|
49
|
+
reasons.push(`"${w}"(힌트)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (aspect) {
|
|
53
|
+
if (aspect === tpl.aspect) {
|
|
54
|
+
score += 2;
|
|
55
|
+
reasons.push(`화면 ${aspect}`);
|
|
56
|
+
} else if (tpl.variants?.[aspect]) score += 0.5;
|
|
57
|
+
else score -= 1.5;
|
|
58
|
+
}
|
|
59
|
+
if (dur !== undefined) {
|
|
60
|
+
const { min, max, allowed } = tpl.duration;
|
|
61
|
+
if (allowed?.includes(dur)) {
|
|
62
|
+
score += 2.5;
|
|
63
|
+
reasons.push(`${dur}초 = 고정 길이`);
|
|
64
|
+
} else if (dur >= min && dur <= max) {
|
|
65
|
+
score += 1.5;
|
|
66
|
+
reasons.push(`${dur}초가 ${min}–${max}초 안`);
|
|
67
|
+
} else score -= 2;
|
|
68
|
+
}
|
|
69
|
+
if (hasVideo && tpl.detect.assetKinds?.includes("video")) {
|
|
70
|
+
score += 2;
|
|
71
|
+
reasons.push("받은 자산에 영상");
|
|
72
|
+
}
|
|
73
|
+
if (input.music && tpl.music === "required") score += 0.5;
|
|
74
|
+
return { purpose: tpl.id, score, reasons };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const z = scored.map((s) => Math.exp(s.score / TEMPERATURE));
|
|
78
|
+
const sum = z.reduce((a, b) => a + b, 0);
|
|
79
|
+
const ranked: Ranked[] = scored
|
|
80
|
+
.map((s, i) => ({ ...s, p: round(z[i] / sum) }))
|
|
81
|
+
.sort((a, b) => b.score - a.score);
|
|
82
|
+
const top = ranked[0];
|
|
83
|
+
return {
|
|
84
|
+
purpose: top.purpose,
|
|
85
|
+
confidence: top.p,
|
|
86
|
+
ranked,
|
|
87
|
+
ambiguous: !anyWord || top.p < ASK_BELOW,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function round(x: number): number {
|
|
92
|
+
return Math.round(x * 100) / 100;
|
|
93
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type * from "./types.ts";
|
|
2
|
+
export { PURPOSES } from "./types.ts";
|
|
3
|
+
export { templatesDir, listTemplates, loadTemplate, frameFor, sceneSkeleton } from "./templates.ts";
|
|
4
|
+
export { parseAspect, parseDuration, parseStyle, parseTitle, mentions } from "./request.ts";
|
|
5
|
+
export { detect, ASK_BELOW } from "./detect.ts";
|
|
6
|
+
export type { Detection, Ranked, DetectInput } from "./detect.ts";
|
|
7
|
+
export { pickDuration, layoutScenes } from "./layout.ts";
|
|
8
|
+
export type { Layout, LaidScene } from "./layout.ts";
|
|
9
|
+
export { makePlan, refuses3d, wantsMotionCut, wantsVideo } from "./plan.ts";
|
|
10
|
+
export { applyAnswers, planQuestions, WAIT } from "./asks.ts";
|
|
11
|
+
export type { PlanQuestions } from "./asks.ts";
|
|
12
|
+
export type { PlanInput } from "./plan.ts";
|
|
13
|
+
export { chat, codeBlock, jsonBlock, ledger, premiumBlock, spend, spentLine, writeStats } from "./llm.ts";
|
|
14
|
+
export type { Message, Part, ChatResult, Spent } from "./llm.ts";
|
|
15
|
+
export { projectBrief, readProject } from "./brief.ts";
|
|
16
|
+
export type { ProjectFile, ProjectScene } from "./brief.ts";
|
|
17
|
+
export { toCues, srt, vtt, writeCaptions } from "./captions.ts";
|
|
18
|
+
export type { CaptionLine, Cue } from "./captions.ts";
|
|
19
|
+
export { suggestThemes, pickTheme, themeFromRequest } from "./themes.ts";
|
|
20
|
+
export type { ThemePick } from "./themes.ts";
|
|
21
|
+
export { look, lookSheet, formatLook, DEFAULT_EYES } from "./look.ts";
|
|
22
|
+
export type { LookSheet } from "./look.ts";
|
|
23
|
+
export type { LookReport, LookScene } from "./look.ts";
|
|
24
|
+
export { attachStudio } from "./studio.ts";
|
|
25
|
+
export type { StudioLink } from "./studio.ts";
|
|
26
|
+
export { checkSeries, listSeries, loadSeries, seriesDir, seriesFromRequest, writeSeries } from "./series.ts";
|
|
27
|
+
export type { Series } from "./series.ts";
|