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,179 @@
|
|
|
1
|
+
import { detect, type Detection } from "./detect.ts";
|
|
2
|
+
import { layoutScenes, pickDuration } from "./layout.ts";
|
|
3
|
+
import { parseAspect, parseDuration, parseStyle, parseTitle } from "./request.ts";
|
|
4
|
+
import { frameFor, listTemplates, loadTemplate } from "./templates.ts";
|
|
5
|
+
import { loadSeries, type Series, seriesFromRequest } from "./series.ts";
|
|
6
|
+
import { pickTheme, suggestThemes, themeFromRequest } from "./themes.ts";
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import type { Aspect, BeatsLike, GenSpec, Plan, PlannedScene, Purpose, ShotSpec, Template } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
const IMAGE = /\.(png|jpe?g|webp)$/i;
|
|
11
|
+
|
|
12
|
+
// 이음매 기본은 모션그래픽 전환이다. 영상 모델은 요청에 분명히 적었을 때만 부른다(돈·시간이 크고 결이 들쭉날쭉하다)
|
|
13
|
+
export function wantsVideo(request: string): boolean {
|
|
14
|
+
return /영상\s*생성|생성\s*영상|AI\s*영상|영상\s*모델|video\s*(gen|model)|generated\s*video/i.test(request);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 캐릭터가 실제로 움직여야 하는 컷 — 모션그래픽·3D 툰으로는 몸짓이 안 서서 영상 모델이 맡는다
|
|
18
|
+
const CHARACTER = /캐릭터|마스코트|인물|사람|소녀|소년|아이돌|댄서|동물|고양이|강아지|새|로봇|주인공|character|mascot|person|girl|boy|dancer|animal|cat|dog|robot/i;
|
|
19
|
+
const ACTION = /움직|춤|걷|달리|뛰|날아|날갯|헤엄|싸우|연기|몸짓|손짓|표정|돌아보|dance|walk|run|jump|fly|swim|wave|move|gesture/i;
|
|
20
|
+
export function wantsMotionCut(request: string): boolean {
|
|
21
|
+
return CHARACTER.test(request) && ACTION.test(request);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 「2D 로」「3D 없이」라고 하면 주인공 3D 샷을 넣지 않는다
|
|
25
|
+
export function refuses3d(request: string): boolean {
|
|
26
|
+
return /3D\s*(없이|빼|말고)|2D\s*(로|만)|평면(으로|만)|no\s*3d|flat\s*only/i.test(request);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 틀의 자리 이름은 장면 id 먼저, 없으면 그 역할의 첫 장면(뮤비처럼 곡 구간으로 id 가 바뀌는 틀)
|
|
30
|
+
function slot<T extends { id: string; role: string }>(scenes: T[], name?: string): T | undefined {
|
|
31
|
+
if (!name) return undefined;
|
|
32
|
+
return scenes.find((s) => s.id === name) ?? scenes.find((s) => s.role === name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PlanInput {
|
|
36
|
+
request: string;
|
|
37
|
+
purpose?: Purpose; // 사람이 못 박으면 판별을 건너뛴다
|
|
38
|
+
assets?: string[];
|
|
39
|
+
music?: string | null;
|
|
40
|
+
beats?: BeatsLike | null;
|
|
41
|
+
duration?: number;
|
|
42
|
+
aspect?: Aspect;
|
|
43
|
+
style?: string;
|
|
44
|
+
title?: string;
|
|
45
|
+
theme?: string; // 테마 id · "auto" · "none"(프리셋만)
|
|
46
|
+
series?: string; // 시리즈 id · "none". 안 주면 요청 글이 짚은 시리즈(series/<id>.json 의 match)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function makePlan(input: PlanInput): Plan {
|
|
50
|
+
const templates = listTemplates();
|
|
51
|
+
// 시리즈가 먼저 — 짚었으면 용도·테마·길이·자막·끝 카드를 그 묶음이 정한다(사람이 따로 준 값은 그대로 이긴다)
|
|
52
|
+
const series: Series | null = input.series === "none" ? null : input.series ? loadSeries(input.series) : seriesFromRequest(input.request);
|
|
53
|
+
const det: Detection = input.purpose
|
|
54
|
+
? { purpose: input.purpose, confidence: 1, ranked: [], ambiguous: false }
|
|
55
|
+
: series
|
|
56
|
+
? { purpose: series.purpose, confidence: 1, ranked: [], ambiguous: false }
|
|
57
|
+
: detect({ request: input.request, assets: input.assets, music: input.music }, templates);
|
|
58
|
+
const base = loadTemplate(det.purpose);
|
|
59
|
+
const tpl: Template = series?.duration ? { ...base, duration: { ...base.duration, ...series.duration } } : base;
|
|
60
|
+
const aspect = input.aspect ?? parseAspect(input.request);
|
|
61
|
+
const frame = frameFor(tpl, aspect && (aspect === tpl.aspect || tpl.variants?.[aspect]) ? aspect : undefined);
|
|
62
|
+
const { duration, notes } = pickDuration(tpl, { requested: input.duration ?? parseDuration(input.request), beats: input.beats });
|
|
63
|
+
const laid = layoutScenes(tpl, duration, input.beats);
|
|
64
|
+
// auto: 요청이 테마를 말로 짚었으면 그 테마, 아니면 내용에 맞는 approved 후보 셋 중 첫째
|
|
65
|
+
// none 은 프리셋만 — 시리즈 테마도 쓰지 않는다
|
|
66
|
+
const themeIn = input.theme === "none" ? undefined : (input.theme === undefined || input.theme === "auto") && series?.theme ? series.theme : input.theme;
|
|
67
|
+
const named = themeIn === "auto" ? themeFromRequest(input.request, tpl.id) : null;
|
|
68
|
+
const themeCandidates = themeIn === "auto" ? [...(named ? [named] : []), ...suggestThemes(input.request, tpl.id, frame.aspect).filter((c) => c.id !== named?.id)].slice(0, 3) : undefined;
|
|
69
|
+
const theme = themeIn === "auto" ? (themeCandidates?.[0]?.id ?? null) : themeIn ? pickTheme(themeIn).id : null;
|
|
70
|
+
// 테마를 고르면 그 테마의 style 이 프리셋을 대신한다(kf new 가 themeStyle 로 쓴다)
|
|
71
|
+
const style = theme ?? input.style ?? series?.style ?? parseStyle(input.request) ?? tpl.style;
|
|
72
|
+
const title = input.title ?? series?.end?.title ?? parseTitle(input.request) ?? "";
|
|
73
|
+
const gen = genSpecs(tpl, laid.scenes, {
|
|
74
|
+
subject: title || input.request,
|
|
75
|
+
style,
|
|
76
|
+
width: frame.width,
|
|
77
|
+
height: frame.height,
|
|
78
|
+
video: wantsVideo(input.request),
|
|
79
|
+
act: tpl.id === "mv" || wantsMotionCut(input.request),
|
|
80
|
+
refs: (input.assets ?? []).filter((a) => IMAGE.test(a)).map((a) => `assets/in/${basename(a)}`),
|
|
81
|
+
});
|
|
82
|
+
const hero = refuses3d(input.request) ? undefined : slot(laid.scenes, tpl.assets.shot3d);
|
|
83
|
+
const shots: ShotSpec[] = hero
|
|
84
|
+
? [{ id: `${hero.id}-3d`, scene: hero.id, tool: "blender", idea: `${title || input.request} — 「${hero.idea}」을 주인공 3D 장면으로(툰 셰이딩·라인아트, 카메라 열쇠는 첫박·드롭에)`, file: `shots/${hero.id}-3d.py` }]
|
|
85
|
+
: [];
|
|
86
|
+
|
|
87
|
+
const scenes: PlannedScene[] = laid.scenes.map((s) => ({
|
|
88
|
+
id: s.id,
|
|
89
|
+
role: s.role,
|
|
90
|
+
from: s.from,
|
|
91
|
+
dur: s.dur,
|
|
92
|
+
idea: s.idea,
|
|
93
|
+
assets: gen.filter((g) => g.scene === s.id).map((g) => `gen:${g.kind}:${g.id}`),
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
const top = det.ranked[0];
|
|
97
|
+
const plan: Plan = {
|
|
98
|
+
request: input.request,
|
|
99
|
+
purpose: det.purpose,
|
|
100
|
+
confidence: det.confidence,
|
|
101
|
+
alternatives: det.ranked.slice(1, 3).map((r) => ({ purpose: r.purpose, p: r.p })),
|
|
102
|
+
reasons: [...(series ? [`시리즈 ${series.id}(${series.label})`] : []), ...(top?.reasons ?? (input.purpose ? ["사람이 정함"] : [])), ...notes, ...laid.notes],
|
|
103
|
+
title,
|
|
104
|
+
aspect: frame.aspect,
|
|
105
|
+
width: frame.width,
|
|
106
|
+
height: frame.height,
|
|
107
|
+
fps: tpl.fps,
|
|
108
|
+
duration: laid.duration,
|
|
109
|
+
style,
|
|
110
|
+
music: input.music ?? null,
|
|
111
|
+
captions: series?.captions ?? tpl.captions,
|
|
112
|
+
deliverables: tpl.deliverables,
|
|
113
|
+
scenes,
|
|
114
|
+
gen,
|
|
115
|
+
shots,
|
|
116
|
+
theme,
|
|
117
|
+
...(themeCandidates ? { themeCandidates } : {}),
|
|
118
|
+
...(series ? { series: series.id, ...(series.music?.mood ? { mood: series.music.mood } : {}) } : {}),
|
|
119
|
+
};
|
|
120
|
+
if (det.ambiguous) {
|
|
121
|
+
plan.questions = [
|
|
122
|
+
{
|
|
123
|
+
id: "purpose",
|
|
124
|
+
ask: "어떤 용도의 영상인가?",
|
|
125
|
+
choices: det.ranked.slice(0, 4).map((r) => `${r.purpose} — ${templates.find((t) => t.id === r.purpose)?.label}`),
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
if (tpl.music === "required" && !input.music) {
|
|
130
|
+
plan.questions = [...(plan.questions ?? []), { id: "music", ask: `${tpl.label} 영상은 곡이 끌고 간다. 쓸 곡 파일은?`, choices: [] }];
|
|
131
|
+
}
|
|
132
|
+
return plan;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// "img:hook-bg" → hook 장면 바탕 그림, "vid:seam" → seam 장면 전체를 채우는 이음매 클립(앞 장면 끝 프레임 → 뒤 장면 첫 프레임, 계약 8)
|
|
136
|
+
function genSpecs(
|
|
137
|
+
tpl: Template,
|
|
138
|
+
scenes: { id: string; role: string; idea: string }[],
|
|
139
|
+
o: { subject: string; style: string; width: number; height: number; video: boolean; act: boolean; refs: string[] },
|
|
140
|
+
): GenSpec[] {
|
|
141
|
+
const gen: GenSpec[] = [];
|
|
142
|
+
for (const spec of [...tpl.assets.gen, ...(o.video ? (tpl.assets.genVideo ?? []) : [])]) {
|
|
143
|
+
const [kind, id] = spec.split(":") as ["img" | "vid", string];
|
|
144
|
+
if (kind === "vid") {
|
|
145
|
+
const i = scenes.findIndex((s) => s.id === id);
|
|
146
|
+
if (i <= 0 || i === scenes.length - 1) continue;
|
|
147
|
+
gen.push({
|
|
148
|
+
id,
|
|
149
|
+
kind,
|
|
150
|
+
scene: id,
|
|
151
|
+
seam: true,
|
|
152
|
+
prompt: `${o.subject} — 「${scenes[i - 1].idea}」 화면에서 「${scenes[i + 1].idea}」 화면으로 이음매 없이 빨려 들어가는 카메라 이동. ${o.style} 결, 글자 없음.`,
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// 뮤비 주인공은 받은 그림을 참조해 한 장 만들고, 나머지 그림은 그 주인공을 참조해 같은 얼굴로 간다
|
|
157
|
+
if (id === "character") {
|
|
158
|
+
gen.push({ id, kind, scene: scenes[0].id, prompt: `${o.subject} 뮤직비디오 주인공 캐릭터 — 정면 전신 한 명, 단색 바탕, 글자 없음. ${o.style} 결.`, size: `${o.width}x${o.height}`, ...(o.refs.length ? { refs: o.refs } : {}) });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const scene = scenes.find((s) => id === s.id || id.startsWith(`${s.id}-`)) ?? scenes[0];
|
|
162
|
+
const refs = gen.some((g) => g.id === "character") ? { refs: ["assets/gen/character.png"] } : {};
|
|
163
|
+
gen.push({ id, kind, scene: scene.id, prompt: `${o.subject} ${tpl.label} 영상 「${scene.idea}」 장면의 바탕 그림. ${o.style} 결, 글자·로고 없음.`, size: `${o.width}x${o.height}`, ...refs });
|
|
164
|
+
}
|
|
165
|
+
const act = o.act ? slot(scenes, tpl.assets.act) : undefined;
|
|
166
|
+
if (act) {
|
|
167
|
+
// 주인공 그림이 있으면 그 얼굴로 시작한다(첫 프레임) — 그림은 영상보다 먼저 만든다(resolvePlan 순서)
|
|
168
|
+
const face = gen.find((g) => g.id === "character") ?? gen.find((g) => g.kind === "img" && g.scene === act.id);
|
|
169
|
+
gen.push({
|
|
170
|
+
id: `${act.id}-act`,
|
|
171
|
+
kind: "vid",
|
|
172
|
+
scene: act.id,
|
|
173
|
+
dur: 5,
|
|
174
|
+
prompt: `${o.subject} — 「${act.idea}」 장면에서 캐릭터가 실제로 움직이는 컷(몸짓·걸음·표정). 카메라는 거의 고정, ${o.style} 결, 글자 없음.`,
|
|
175
|
+
...(face ? { first: `assets/gen/${face.id}.png` } : {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return gen;
|
|
179
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Aspect } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
// 요청 글에서 사람이 못 박은 값만 뽑는다. 못 찾으면 undefined — 틀 기본값이 채운다.
|
|
4
|
+
|
|
5
|
+
export function parseDuration(text: string): number | undefined {
|
|
6
|
+
const t = text.toLowerCase();
|
|
7
|
+
const min = t.match(/(\d+(?:\.\d+)?)\s*(?:분|min(?:ute)?s?\b)/);
|
|
8
|
+
const sec = t.match(/(\d+(?:\.\d+)?)\s*(?:초|s\b|sec(?:ond)?s?\b)/);
|
|
9
|
+
if (!min && !sec) {
|
|
10
|
+
if (/한\s*분|일\s*분/.test(t)) return 60;
|
|
11
|
+
if (/반\s*분/.test(t)) return 30;
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return (min ? Number(min[1]) * 60 : 0) + (sec ? Number(sec[1]) : 0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseAspect(text: string): Aspect | undefined {
|
|
18
|
+
const t = text.toLowerCase();
|
|
19
|
+
if (/9\s*[:x×]\s*16|세로|vertical|portrait/.test(t)) return "9:16";
|
|
20
|
+
if (/1\s*[:x×]\s*1\b|정사각|square/.test(t)) return "1:1";
|
|
21
|
+
if (/16\s*[:x×]\s*9|가로|landscape|widescreen/.test(t)) return "16:9";
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const STYLE_HINTS: [RegExp, string][] = [
|
|
26
|
+
[/카와이|kawaii|파스텔|pastel|귀여운|큐트|cute/i, "kawaii-pastel"],
|
|
27
|
+
[/카사텀|kasaterm/i, "kasaterm"],
|
|
28
|
+
[/\bog\b|오픈게이트웨이|opengateway|open gateway/i, "og"],
|
|
29
|
+
[/다크|어두운|dark|시네마틱|cinematic/i, "neutral-dark"],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function parseStyle(text: string): string | undefined {
|
|
33
|
+
return STYLE_HINTS.find(([re]) => re.test(text))?.[1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SUBJECTS: [RegExp, string][] = [
|
|
37
|
+
[/카사텀|kasaterm/i, "kasaterm"],
|
|
38
|
+
[/\bog\b|오픈게이트웨이|opengateway|open gateway/i, "OpenGateway"],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// 아는 제품만 잡는다. 모르면 비워 두고 kf new(폴더 이름)·kf make(LLM)가 채운다 — 첫 낱말을 이름으로 찍으면 "15초" 같은 게 끝 카드에 뜬다
|
|
42
|
+
export function parseTitle(text: string): string | undefined {
|
|
43
|
+
return SUBJECTS.find(([re]) => re.test(text))?.[1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ASCII_WORD = /^[a-z0-9 .:-]+$/i;
|
|
47
|
+
|
|
48
|
+
// 영어 짧은 낱말("ad")이 "made" 같은 데 걸리지 않게 영문은 낱말 경계로, 한글은 그대로 찾는다
|
|
49
|
+
export function mentions(text: string, word: string): boolean {
|
|
50
|
+
const t = text.toLowerCase();
|
|
51
|
+
const w = word.toLowerCase();
|
|
52
|
+
if (!ASCII_WORD.test(w)) return t.includes(w);
|
|
53
|
+
const esc = w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
return new RegExp(`(^|[^a-z0-9])${esc}([^a-z0-9]|$)`).test(t);
|
|
55
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { PURPOSES, type Purpose } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
// 시리즈 묶음 — 같은 시리즈면 생김새·목소리·곡·자막·길이·끝 카드·생성 답이 매번 같다. 요청이 시리즈를 짚으면 묻지 않고 이 묶음을 쓴다
|
|
7
|
+
export interface Series {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
summary?: string;
|
|
11
|
+
match: string[]; // 요청 글에 이 말이 있으면 이 시리즈(대소문자·띄어쓰기 무시)
|
|
12
|
+
purpose: Purpose;
|
|
13
|
+
theme?: string;
|
|
14
|
+
style?: string; // 테마 없이 프리셋으로 갈 때
|
|
15
|
+
voice?: string; // "<엔진>:<목소리>" · "none"
|
|
16
|
+
music?: { mood?: string; file?: string }; // file 은 레포 기준 곡 — 있으면 늘 그 곡, 없으면 분위기로 곡 모음에서(같은 분위기면 같은 곡)
|
|
17
|
+
captions?: "on" | "off" | "none";
|
|
18
|
+
duration?: { min?: number; max?: number; default?: number };
|
|
19
|
+
end?: { title?: string; line?: string }; // 끝 카드
|
|
20
|
+
answers?: Record<string, string>; // 생성 질문에 미리 박은 답(image·video·blender·voice·music) — 묻지 않는다
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const seriesDir = (): string => process.env.KF_SERIES_DIR || fileURLToPath(new URL("../../../series/", import.meta.url));
|
|
24
|
+
|
|
25
|
+
export function checkSeries(s: Partial<Series>): string[] {
|
|
26
|
+
const bad: string[] = [];
|
|
27
|
+
if (!s.id || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) bad.push("id 는 소문자·숫자·- 로");
|
|
28
|
+
if (!s.label) bad.push("label 이 없다");
|
|
29
|
+
if (!s.match?.length) bad.push("match 가 비었다 — 요청에서 짚을 말을 하나 이상");
|
|
30
|
+
if (!s.purpose || !PURPOSES.includes(s.purpose)) bad.push(`purpose 는 ${PURPOSES.join("·")} 중 하나`);
|
|
31
|
+
if (s.captions && !["on", "off", "none"].includes(s.captions)) bad.push("captions 는 on·off·none");
|
|
32
|
+
return bad;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function listSeries(): Series[] {
|
|
36
|
+
const dir = seriesDir();
|
|
37
|
+
if (!existsSync(dir)) return [];
|
|
38
|
+
return readdirSync(dir)
|
|
39
|
+
.filter((f) => f.endsWith(".json"))
|
|
40
|
+
.map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")) as Series)
|
|
41
|
+
.filter((s) => !checkSeries(s).length)
|
|
42
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function loadSeries(id: string): Series {
|
|
46
|
+
const file = join(seriesDir(), `${id}.json`);
|
|
47
|
+
if (!existsSync(file)) throw new Error(`시리즈 없음: ${id} (있는 것: ${listSeries().map((s) => s.id).join(" ") || "없음"})`);
|
|
48
|
+
const s = JSON.parse(readFileSync(file, "utf8")) as Series;
|
|
49
|
+
const bad = checkSeries(s);
|
|
50
|
+
if (bad.length) throw new Error(`시리즈 ${id} 가 깨졌다 — ${bad.join(" · ")}`);
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const squash = (x: string) => x.toLowerCase().replace(/\s+/g, "");
|
|
55
|
+
|
|
56
|
+
// 가장 길게 맞은 말의 시리즈 — 「OG 쇼츠」와 「OG 쇼츠 영어판」이 같이 있으면 긴 쪽
|
|
57
|
+
export function seriesFromRequest(request: string): Series | null {
|
|
58
|
+
const r = squash(request);
|
|
59
|
+
let best: { s: Series; n: number } | null = null;
|
|
60
|
+
for (const s of listSeries()) {
|
|
61
|
+
for (const m of s.match) {
|
|
62
|
+
const k = squash(m);
|
|
63
|
+
if (k && r.includes(k) && (!best || k.length > best.n)) best = { s, n: k.length };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return best?.s ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function writeSeries(s: Series): string {
|
|
70
|
+
const bad = checkSeries(s);
|
|
71
|
+
if (bad.length) throw new Error(bad.join(" · "));
|
|
72
|
+
mkdirSync(seriesDir(), { recursive: true });
|
|
73
|
+
const file = join(seriesDir(), `${s.id}.json`);
|
|
74
|
+
writeFileSync(file, `${JSON.stringify(s, null, 2)}\n`);
|
|
75
|
+
return file;
|
|
76
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// 말로 시작한 kf make 도 작업실이 떠 있으면 거기에 등록한다 — 페이지 「지난 작업」·실시간 미리보기로 보이게. 서버가 없으면 조용히 넘어간다
|
|
2
|
+
const base = () => (process.env.KF_APP_URL ?? "http://127.0.0.1:5150").replace(/\/$/, "");
|
|
3
|
+
|
|
4
|
+
async function post<T>(path: string, body: unknown): Promise<T | null> {
|
|
5
|
+
try {
|
|
6
|
+
const r = await fetch(`${base()}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(1500) });
|
|
7
|
+
return r.ok ? ((await r.json()) as T) : null;
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StudioLink {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
send(p: { lines?: string[]; phase?: string; done?: boolean; ok?: boolean }): Promise<void>;
|
|
17
|
+
// console.error 로 찍는 진행 줄을 작업실로도 흘린다(모아서 0.5초마다)
|
|
18
|
+
mirror(): () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function attachStudio(o: { dir: string; request: string; model?: string; purpose?: string }): Promise<StudioLink | null> {
|
|
22
|
+
const r = await post<{ id: string; url: string }>("/api/jobs/attach", o);
|
|
23
|
+
if (!r) return null;
|
|
24
|
+
const send = async (p: Parameters<StudioLink["send"]>[0]) => {
|
|
25
|
+
await post(`/api/jobs/${r.id}/progress`, p);
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
...r,
|
|
29
|
+
send,
|
|
30
|
+
mirror() {
|
|
31
|
+
const orig = console.error;
|
|
32
|
+
let queue: string[] = [];
|
|
33
|
+
let chain = Promise.resolve();
|
|
34
|
+
const flush = () => {
|
|
35
|
+
if (!queue.length) return chain;
|
|
36
|
+
const lines = queue;
|
|
37
|
+
queue = [];
|
|
38
|
+
chain = chain.then(() => send({ lines }));
|
|
39
|
+
return chain;
|
|
40
|
+
};
|
|
41
|
+
const timer = setInterval(flush, 500);
|
|
42
|
+
console.error = (...args: unknown[]) => {
|
|
43
|
+
orig(...args);
|
|
44
|
+
queue.push(...args.map(String).join(" ").split("\n"));
|
|
45
|
+
};
|
|
46
|
+
return async () => {
|
|
47
|
+
clearInterval(timer);
|
|
48
|
+
console.error = orig;
|
|
49
|
+
await flush();
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { type Aspect, PURPOSES, type Purpose, type SafeArea, type Template } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export const templatesDir = fileURLToPath(new URL("../../../templates/", import.meta.url));
|
|
7
|
+
|
|
8
|
+
export function listTemplates(): Template[] {
|
|
9
|
+
return readdirSync(templatesDir)
|
|
10
|
+
.filter((d) => existsSync(join(templatesDir, d, "template.json")))
|
|
11
|
+
.map((d) => loadTemplate(d))
|
|
12
|
+
.sort((a, b) => PURPOSES.indexOf(a.id) - PURPOSES.indexOf(b.id));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadTemplate(id: string): Template {
|
|
16
|
+
const file = join(templatesDir, id, "template.json");
|
|
17
|
+
if (!existsSync(file)) {
|
|
18
|
+
const have = readdirSync(templatesDir).filter((d) => existsSync(join(templatesDir, d, "template.json")));
|
|
19
|
+
throw new Error(`틀 "${id}" 가 없다. 있는 틀: ${have.join(" ")}`);
|
|
20
|
+
}
|
|
21
|
+
return JSON.parse(readFileSync(file, "utf8")) as Template;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function frameFor(tpl: Template, aspect?: Aspect): { aspect: Aspect; width: number; height: number; safeArea: SafeArea } {
|
|
25
|
+
if (!aspect || aspect === tpl.aspect) return { aspect: tpl.aspect, width: tpl.width, height: tpl.height, safeArea: tpl.safeArea };
|
|
26
|
+
const v = tpl.variants?.[aspect];
|
|
27
|
+
if (!v) throw new Error(`틀 ${tpl.id} 는 ${aspect} 를 받지 않는다(${[tpl.aspect, ...Object.keys(tpl.variants ?? {})].join("·")})`);
|
|
28
|
+
return { aspect, width: v.width, height: v.height, safeArea: v.safeArea ?? tpl.safeArea };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 틀 고유(장면 id → 틀 기본) → 역할 공용 → 공용 기본. 뮤비처럼 장면 id 가 곡마다 바뀌는 틀은 틀 기본(scenes/default.ts)이 받는다
|
|
32
|
+
export function sceneSkeleton(tpl: Purpose, sceneId: string, role: string): string {
|
|
33
|
+
const tries = [
|
|
34
|
+
join(templatesDir, tpl, "scenes", `${sceneId}.ts`),
|
|
35
|
+
join(templatesDir, tpl, "scenes", "default.ts"),
|
|
36
|
+
join(templatesDir, "_shared", "scenes", `${role}.ts`),
|
|
37
|
+
join(templatesDir, "_shared", "scenes", "default.ts"),
|
|
38
|
+
];
|
|
39
|
+
const hit = tries.find((p) => existsSync(p));
|
|
40
|
+
if (!hit) throw new Error(`장면 뼈대가 없다: ${tries.join(" · ")}`);
|
|
41
|
+
return readFileSync(hit, "utf8");
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { listThemes, loadTheme, type Theme } from "@kasaframe/themes";
|
|
2
|
+
import { parseTitle } from "./request.ts";
|
|
3
|
+
import type { Aspect, Purpose } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export interface ThemePick {
|
|
6
|
+
id: string;
|
|
7
|
+
why: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const words = (s: string) => s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length >= 2);
|
|
11
|
+
|
|
12
|
+
// 자동 추천은 approved 테마만(SPEC 9). 요청 낱말이 테마 이름·비유·THEME.md 에 얼마나 나오나로 줄 세우고, 같으면 최근에 승인된 쪽
|
|
13
|
+
export function suggestThemes(request: string, purpose: Purpose, aspect?: Aspect, n = 3): ThemePick[] {
|
|
14
|
+
const pool = listThemes({ status: "approved", purpose, ...(aspect ? { aspect } : {}) });
|
|
15
|
+
const ask = new Set(words(request));
|
|
16
|
+
const scored = pool.map((t) => {
|
|
17
|
+
const hay = `${t.name} ${t.metaphor} ${t.guide}`.toLowerCase();
|
|
18
|
+
const hit = [...ask].filter((w) => hay.includes(w));
|
|
19
|
+
return { t, score: hit.length, hit };
|
|
20
|
+
});
|
|
21
|
+
scored.sort((a, b) => b.score - a.score || (b.t.approvedAt ?? "").localeCompare(a.t.approvedAt ?? ""));
|
|
22
|
+
return scored.slice(0, n).map(({ t, hit }) => ({ id: t.id, why: hit.length ? `요청의 ${hit.slice(0, 3).map((w) => `「${w}」`).join("·")} 이(가) 테마 비유와 맞는다 — ${t.metaphor}` : `${purpose} 에 쓰는 승인 테마 — ${t.metaphor}` }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 「리소 느낌으로」처럼 사람이 말로 테마를 짚었는지 — 테마 이름 낱말(두 글자 넘게)·id 조각(세 글자 넘게)이 요청에 나오면 그 테마.
|
|
26
|
+
// 사람이 짚은 것이라 review 까지 받는다(draft 는 빼고)
|
|
27
|
+
export function themeFromRequest(request: string, purpose?: Purpose): ThemePick | null {
|
|
28
|
+
const t = request.toLowerCase();
|
|
29
|
+
let best: { th: Theme; score: number; hit: string[] } | null = null;
|
|
30
|
+
for (const th of listThemes({ status: ["approved", "review"] })) {
|
|
31
|
+
const keys = [...th.name.split(/\s+/).filter((w) => w.length >= 2), ...th.id.split("-").filter((w) => w.length >= 3)].map((w) => w.toLowerCase());
|
|
32
|
+
const hit = keys.filter((k) => t.includes(k));
|
|
33
|
+
if (!hit.length) continue;
|
|
34
|
+
// 제품 이름(「카사텀」·「OG」)은 무엇을 찍나일 뿐 결이 아니다 — 「카와이」·「리소」 같은 결 낱말보다 약하게, 「~ 느낌·풍·스타일」로 짚으면 가장 세게.
|
|
35
|
+
// 용도에 안 맞는 테마도 사람이 짚었으면 쓰되, 같은 점수면 용도에 맞는 쪽
|
|
36
|
+
const weight = (k: string) => (parseTitle(k) ? 4 : 10) + (new RegExp(`${k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*(느낌|풍|스타일|결|톤|테마|처럼)`).test(t) ? 20 : 0);
|
|
37
|
+
const score = hit.reduce((a, k) => a + weight(k), 0) + (purpose && th.purposes.includes(purpose) ? 1 : 0);
|
|
38
|
+
if (!best || score > best.score) best = { th, score, hit };
|
|
39
|
+
}
|
|
40
|
+
return best ? { id: best.th.id, why: `요청이 「${best.hit.join("·")}」 로 ${best.th.name} 을(를) 짚었다 — ${best.th.metaphor}` } : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 사람이 고른 id 는 review 까지 쓴다. draft 는 테마 폴더 sample/ 안에서만이라 여기선 막는다
|
|
44
|
+
export function pickTheme(id: string): Theme {
|
|
45
|
+
const t = loadTheme(id);
|
|
46
|
+
if (t.status === "draft") throw new Error(`테마 ${id} 는 아직 draft — 견본이 review 로 올라온 뒤에 쓴다`);
|
|
47
|
+
return t;
|
|
48
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Question } from "@kasaframe/gen";
|
|
2
|
+
import type { Bundle } from "@kasaframe/motion/menu";
|
|
3
|
+
export type Aspect = "16:9" | "9:16" | "1:1";
|
|
4
|
+
export type Purpose = "promo" | "shorts" | "explainer" | "launch" | "demo" | "ad" | "square" | "mv";
|
|
5
|
+
export const PURPOSES: Purpose[] = ["promo", "shorts", "explainer", "launch", "demo", "ad", "square", "mv"];
|
|
6
|
+
|
|
7
|
+
export interface SafeArea { top: number; bottom: number; left: number; right: number }
|
|
8
|
+
|
|
9
|
+
export interface TemplateScene {
|
|
10
|
+
id: string;
|
|
11
|
+
role: string;
|
|
12
|
+
sec?: number;
|
|
13
|
+
weight?: number;
|
|
14
|
+
anchor?: string; // start · end · drop:<k> · drop:last · section:<kind>
|
|
15
|
+
snap?: "bar" | "beat" | "none";
|
|
16
|
+
idea: string;
|
|
17
|
+
transition?: string; // 이음매 장면의 3D 전환 종류(dive·portal·cards·shatter·zoomOut·fold)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Template {
|
|
21
|
+
id: Purpose;
|
|
22
|
+
label: string;
|
|
23
|
+
summary: string; // 틀 고르는 화면의 한 줄
|
|
24
|
+
aspect: Aspect;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
fps: number;
|
|
28
|
+
variants?: Partial<Record<Aspect, { width: number; height: number; safeArea?: SafeArea }>>;
|
|
29
|
+
duration: { min: number; max: number; default: number; allowed?: number[] };
|
|
30
|
+
music: "required" | "optional" | "none";
|
|
31
|
+
// 곡 모음 태그·생성 곡 프롬프트를 고르는 분위기(@kasaframe/gen MOOD_TAGS·MOOD_PROMPT) — 같은 틀이면 같은 결의 곡
|
|
32
|
+
mood?: string;
|
|
33
|
+
// 내레이션 먼저 — 장면별 문장을 읽혀 길이를 재고 장면을 목소리에 맞춘다. voices 는 엔진마다 박은 목소리(같은 틀이면 같은 목소리)
|
|
34
|
+
narration?: Narration;
|
|
35
|
+
// 섞기 — 내레이션 아래 곡 musicDb, 말할 때 덕킹, 끝 페이드, 정규화 LUFS
|
|
36
|
+
audio?: AudioMix;
|
|
37
|
+
// off: srt·vtt 만 · on: 화면에도 얹는다 · none: 없음 (옛 이름 track·burned 도 읽는다 — @kasaframe/motion/captions 의 captionMode)
|
|
38
|
+
captions: "off" | "on" | "none";
|
|
39
|
+
loop: boolean;
|
|
40
|
+
style: string;
|
|
41
|
+
safeArea: SafeArea;
|
|
42
|
+
text: { policy: "none" | "terms-only" | "captions" | "lyrics"; maxOnScreen: number; minPx: number };
|
|
43
|
+
// 뮤비: kinetic 이면 부른 낱말이 그 시각에 화면에 뜬다(lyrics.json 낱말 시각), track 이면 자막 트랙만
|
|
44
|
+
lyrics?: { mode: "kinetic" | "track" | "none"; align?: string; lipsync?: string };
|
|
45
|
+
motion: Record<string, unknown>;
|
|
46
|
+
// 장면마다 거는 연출 수 묶음(@kasaframe/motion/menu) — kf new 가 요청 글을 씨앗으로 장면마다 고른다. roles 는 역할별(hook·drop·end …)
|
|
47
|
+
moves?: Bundle;
|
|
48
|
+
// kf render 기본값 — kf new 가 kf.json render 로 옮긴다. motionBlur 하위 프레임 수(0 이면 끔)·shutter 셔터 각도
|
|
49
|
+
render?: { motionBlur?: number; shutter?: number };
|
|
50
|
+
scenes: TemplateScene[];
|
|
51
|
+
// sections: 곡 구간(beats.json sections)마다 장면 하나 — scenes 는 곡이 없을 때 쓰는 기본, sectionIdeas 는 구간 종류별 할 일
|
|
52
|
+
layout?: "fixed" | "sections";
|
|
53
|
+
sectionIdeas?: Record<string, string>;
|
|
54
|
+
// gen 은 늘 만드는 그림, genVideo 는 요청에 「영상 생성」을 분명히 적었을 때만(이음매 기본은 모션그래픽 전환).
|
|
55
|
+
// shot3d 는 주인공 3D 장면(Blender) 자리, act 는 캐릭터가 실제로 움직여야 할 때 영상 모델 컷 자리 — 장면 id, 없으면 역할로 찾는다
|
|
56
|
+
assets: { need: string[]; want: string[]; gen: string[]; genVideo?: string[]; shot3d?: string; act?: string };
|
|
57
|
+
deliverables: string[];
|
|
58
|
+
detect: { words: string[]; hints: string[]; aspect: Aspect; assetKinds?: string[] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// SPEC 4 중 장면 배치에 쓰는 것만
|
|
62
|
+
export interface BeatsLike {
|
|
63
|
+
duration: number;
|
|
64
|
+
beats: number[];
|
|
65
|
+
downbeats?: number[];
|
|
66
|
+
bars?: number[];
|
|
67
|
+
drops?: number[];
|
|
68
|
+
sections?: { t: number; kind: string; strength?: number }[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PlannedScene {
|
|
72
|
+
id: string;
|
|
73
|
+
role: string;
|
|
74
|
+
from: number;
|
|
75
|
+
dur: number;
|
|
76
|
+
idea: string;
|
|
77
|
+
assets: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// SPEC 계약 8 — @kasaframe/gen 의 GenSpec 과 같은 꼴. 이음매(seam) 영상의 id 는 kf.json 장면 id 와 같다
|
|
81
|
+
export interface GenSpec {
|
|
82
|
+
id: string;
|
|
83
|
+
kind: "img" | "vid";
|
|
84
|
+
scene?: string;
|
|
85
|
+
prompt: string;
|
|
86
|
+
size?: string;
|
|
87
|
+
transparent?: boolean;
|
|
88
|
+
quality?: string;
|
|
89
|
+
refs?: string[];
|
|
90
|
+
seam?: boolean;
|
|
91
|
+
dur?: number;
|
|
92
|
+
first?: string;
|
|
93
|
+
last?: string;
|
|
94
|
+
provider?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 주인공 3D 장면 — blender 면 shots/<id>.py 를 kf blender 로 굽고, three 면 그 장면이 stage3d 로 3D 를 짓는다(Blender 가 없을 때의 대신)
|
|
98
|
+
export interface ShotSpec {
|
|
99
|
+
id: string;
|
|
100
|
+
scene: string;
|
|
101
|
+
tool: "blender" | "three";
|
|
102
|
+
idea: string;
|
|
103
|
+
file: string; // 프로젝트 기준 샷 스크립트 — blender 일 때만 쓴다
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Narration {
|
|
107
|
+
on: boolean;
|
|
108
|
+
lang: string;
|
|
109
|
+
style: string;
|
|
110
|
+
// 이 틀의 목소리 하나 — 같은 틀이면 어느 기계에서든 이 목소리. 그 엔진이 없으면 다른 엔진으로 넘어가지 않고 묻는다
|
|
111
|
+
voice: string;
|
|
112
|
+
// 사람이 다른 엔진을 고를 때 그 엔진의 목소리
|
|
113
|
+
voices: Record<string, string>;
|
|
114
|
+
lead: number; // 장면 시작에서 말이 들어오기까지(초)
|
|
115
|
+
tail: number; // 말이 끝나고 장면이 끝나기까지(초)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AudioMix {
|
|
119
|
+
musicDb: number;
|
|
120
|
+
duck: boolean;
|
|
121
|
+
lufs: number;
|
|
122
|
+
truePeak: number;
|
|
123
|
+
fadeOut: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface Plan {
|
|
127
|
+
request: string;
|
|
128
|
+
purpose: Purpose;
|
|
129
|
+
confidence: number;
|
|
130
|
+
alternatives: { purpose: Purpose; p: number }[];
|
|
131
|
+
reasons: string[];
|
|
132
|
+
title: string;
|
|
133
|
+
aspect: Aspect;
|
|
134
|
+
width: number;
|
|
135
|
+
height: number;
|
|
136
|
+
fps: number;
|
|
137
|
+
duration: number;
|
|
138
|
+
style: string;
|
|
139
|
+
music: string | null;
|
|
140
|
+
// 목소리 "<엔진>:<목소리>" · "none"(자막만). 정하기 전에는 없다
|
|
141
|
+
voice?: string;
|
|
142
|
+
captions: Template["captions"];
|
|
143
|
+
deliverables: string[];
|
|
144
|
+
scenes: PlannedScene[];
|
|
145
|
+
gen: GenSpec[];
|
|
146
|
+
// 고른 테마(themes/<id>). auto 면 approved 후보 셋을 themeCandidates 에 남기고 첫째를 쓴다
|
|
147
|
+
theme?: string | null;
|
|
148
|
+
themeCandidates?: { id: string; why: string }[];
|
|
149
|
+
questions?: { id: string; ask: string; choices: string[] }[];
|
|
150
|
+
shots?: ShotSpec[];
|
|
151
|
+
// 시리즈 묶음(series/<id>.json)으로 세운 계획이면 그 id, 곡 분위기는 시리즈가 틀 mood 를 덮는다
|
|
152
|
+
series?: string;
|
|
153
|
+
mood?: string;
|
|
154
|
+
// 만들기 전에 사람이 고를 것(@kasaframe/gen Question) — 아직 안 고른 것만. 고른 답은 answers 에 { 질문 id: 값 }
|
|
155
|
+
asks?: Question[];
|
|
156
|
+
answers?: Record<string, string>;
|
|
157
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { studioPlugin, studioConfig, type StudioConfig } from "./plugin.ts";
|