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,176 @@
|
|
|
1
|
+
import type { BeatsLike, Template, TemplateScene } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface LaidScene extends TemplateScene {
|
|
4
|
+
from: number;
|
|
5
|
+
dur: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Layout {
|
|
9
|
+
duration: number;
|
|
10
|
+
scenes: LaidScene[];
|
|
11
|
+
notes: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MIN_SCENE = 0.75;
|
|
15
|
+
|
|
16
|
+
// 요청 길이 → 곡 길이 → 틀 기본 순. 곡이 틀 상한보다 길면 틀 기본 길이에서 끊는다.
|
|
17
|
+
export function pickDuration(tpl: Template, o: { requested?: number; beats?: BeatsLike | null }): { duration: number; notes: string[] } {
|
|
18
|
+
const { min, max, allowed } = tpl.duration;
|
|
19
|
+
const notes: string[] = [];
|
|
20
|
+
const music = o.beats?.duration;
|
|
21
|
+
if (o.requested !== undefined) {
|
|
22
|
+
let d = o.requested;
|
|
23
|
+
if (allowed && !allowed.includes(d)) {
|
|
24
|
+
const near = allowed.reduce((a, b) => (Math.abs(b - d) < Math.abs(a - d) ? b : a));
|
|
25
|
+
notes.push(`${d}초 대신 ${tpl.label} 고정 길이 ${near}초`);
|
|
26
|
+
d = near;
|
|
27
|
+
} else if (d < min || d > max) {
|
|
28
|
+
const c = Math.min(max, Math.max(min, d));
|
|
29
|
+
notes.push(`${d}초는 ${tpl.label} 범위(${min}–${max}초) 밖이라 ${c}초`);
|
|
30
|
+
d = c;
|
|
31
|
+
}
|
|
32
|
+
if (music !== undefined && music < d) {
|
|
33
|
+
notes.push(`곡이 ${music.toFixed(1)}초라 영상도 거기까지`);
|
|
34
|
+
d = music;
|
|
35
|
+
}
|
|
36
|
+
return { duration: d, notes };
|
|
37
|
+
}
|
|
38
|
+
if (music === undefined) return { duration: tpl.duration.default, notes };
|
|
39
|
+
if (music <= max) {
|
|
40
|
+
if (music < min) notes.push(`곡이 ${music.toFixed(1)}초로 ${tpl.label} 최소 ${min}초보다 짧다`);
|
|
41
|
+
return { duration: music, notes };
|
|
42
|
+
}
|
|
43
|
+
// 구간 경계에서 끊으면 「1분 홍보」가 56초가 된다 — 사람은 길이를 숫자로 기대하니 틀 기본 길이에서 끊는다
|
|
44
|
+
const d = Math.min(max, Math.max(min, tpl.duration.default));
|
|
45
|
+
notes.push(`곡(${music.toFixed(1)}초)이 상한 ${max}초보다 길어 기본 길이 ${d}초에서 끊는다`);
|
|
46
|
+
return { duration: d, notes };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 곡이 영상보다 길 수 있다 — 영상 안에 든 드롭·구간만 센다
|
|
50
|
+
function anchorTime(anchor: string | undefined, beats: BeatsLike | null | undefined, duration: number): number | undefined {
|
|
51
|
+
if (!anchor || !beats) return undefined;
|
|
52
|
+
const drops = (beats.drops ?? []).filter((t) => t > 0 && t < duration);
|
|
53
|
+
if (anchor === "drop:last") return drops.length ? drops[drops.length - 1] : undefined;
|
|
54
|
+
const d = anchor.match(/^drop:(\d+)$/);
|
|
55
|
+
if (d) return drops[Number(d[1])];
|
|
56
|
+
const s = anchor.match(/^section:(.+)$/);
|
|
57
|
+
if (s) return beats.sections?.find((x) => x.kind === s[1] && x.t < duration)?.t;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 고정 초(sec)를 먼저 떼고 남은 시간을 weight 로 나눈다. 칸이 모자라면 고정 초도 같은 비율로 줄인다.
|
|
62
|
+
function fill(scenes: TemplateScene[], span: number): number[] {
|
|
63
|
+
const fixed = scenes.reduce((a, s) => a + (s.sec ?? 0), 0);
|
|
64
|
+
const weight = scenes.reduce((a, s) => a + (s.sec === undefined ? (s.weight ?? 1) : 0), 0);
|
|
65
|
+
if (weight === 0 || fixed >= span) {
|
|
66
|
+
const k = fixed > 0 ? span / fixed : 0;
|
|
67
|
+
return scenes.map((s) => (fixed > 0 ? (s.sec ?? 0) * k : span / scenes.length));
|
|
68
|
+
}
|
|
69
|
+
const rest = span - fixed;
|
|
70
|
+
return scenes.map((s) => s.sec ?? (rest * (s.weight ?? 1)) / weight);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const MIN_SECTION = 4;
|
|
74
|
+
const MAX_SECTIONS = 14;
|
|
75
|
+
|
|
76
|
+
// 뮤비처럼 곡 구간이 곧 장면인 틀 — 잰 구간 경계를 그대로 쓰고, 너무 짧은 구간은 앞 구간에 붙이고, 너무 많으면 가장 짧은 것부터 합친다
|
|
77
|
+
function sectionLayout(tpl: Template, duration: number, beats: BeatsLike): Layout {
|
|
78
|
+
const marks = (beats.sections ?? []).filter((s) => s.t < duration - MIN_SECTION).sort((a, b) => a.t - b.t);
|
|
79
|
+
if (!marks.length || marks[0].t > 0.5) marks.unshift({ t: 0, kind: "intro" });
|
|
80
|
+
else marks[0] = { ...marks[0], t: 0 };
|
|
81
|
+
let segs = marks.map((m, i) => ({ kind: m.kind, from: m.t, to: i + 1 < marks.length ? marks[i + 1].t : duration }));
|
|
82
|
+
for (let i = segs.length - 1; i > 0; i--) {
|
|
83
|
+
if (segs[i].to - segs[i].from < MIN_SECTION) {
|
|
84
|
+
segs[i - 1].to = segs[i].to;
|
|
85
|
+
segs.splice(i, 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
while (segs.length > MAX_SECTIONS) {
|
|
89
|
+
let k = 0;
|
|
90
|
+
for (let i = 1; i < segs.length; i++) if (segs[i].to - segs[i].from < segs[k].to - segs[k].from) k = i;
|
|
91
|
+
const j = k === 0 ? 1 : k === segs.length - 1 ? k - 1 : segs[k - 1].to - segs[k - 1].from < segs[k + 1].to - segs[k + 1].from ? k - 1 : k + 1;
|
|
92
|
+
const [a, b] = [Math.min(k, j), Math.max(k, j)];
|
|
93
|
+
segs[a] = { ...segs[a], to: segs[b].to };
|
|
94
|
+
segs.splice(b, 1);
|
|
95
|
+
}
|
|
96
|
+
const f = (x: number) => Math.round(x * tpl.fps) / tpl.fps;
|
|
97
|
+
const count: Record<string, number> = {};
|
|
98
|
+
const scenes = segs.map((g) => {
|
|
99
|
+
count[g.kind] = (count[g.kind] ?? 0) + 1;
|
|
100
|
+
const from = f(g.from);
|
|
101
|
+
return { id: `${g.kind}${count[g.kind]}`, role: g.kind, idea: tpl.sectionIdeas?.[g.kind] ?? tpl.sectionIdeas?.main ?? g.kind, snap: "none" as const, from: r4(from), dur: r4(f(g.to) - from) };
|
|
102
|
+
});
|
|
103
|
+
return { duration: f(duration), scenes, notes: [`곡 구간 ${scenes.length}개를 장면으로(${MIN_SECTION}초보다 짧은 구간은 앞에 붙임)`] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function layoutScenes(tpl: Template, duration: number, beats?: BeatsLike | null): Layout {
|
|
107
|
+
if (tpl.layout === "sections" && beats?.sections && beats.sections.length >= 2) return sectionLayout(tpl, duration, beats);
|
|
108
|
+
const scenes = tpl.scenes;
|
|
109
|
+
const n = scenes.length;
|
|
110
|
+
const notes: string[] = [];
|
|
111
|
+
|
|
112
|
+
// 1. 드롭·구간에 묶인 장면 시작을 못으로 박는다. 앞 장면들이 설 자리가 없으면 그 못은 버린다.
|
|
113
|
+
const pins = new Map<number, number>([[0, 0], [n, duration]]);
|
|
114
|
+
for (let i = 1; i < n; i++) {
|
|
115
|
+
const a = anchorTime(scenes[i].anchor, beats, duration);
|
|
116
|
+
if (beats && scenes[i].anchor && !scenes[i].anchor!.match(/^(start|end)$/) && a === undefined) {
|
|
117
|
+
notes.push(`${scenes[i].id}: ${scenes[i].anchor} 이 곡에 없어 비율로 둔다`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (a === undefined) continue;
|
|
121
|
+
const prevPin = Math.max(...[...pins.entries()].filter(([k]) => k < i).map(([, v]) => v));
|
|
122
|
+
const before = i - [...pins.keys()].filter((k) => k < i).reduce((x, y) => Math.max(x, y), 0);
|
|
123
|
+
const tailNeed = scenes.slice(i).reduce((s, x) => s + (x.sec ?? MIN_SCENE), 0);
|
|
124
|
+
if (a - prevPin < before * MIN_SCENE || duration - a < tailNeed) {
|
|
125
|
+
notes.push(`${scenes[i].id}: ${scenes[i].anchor}(${a.toFixed(2)}초)에 붙이면 앞뒤 장면이 설 자리가 없어 비율로 둔다`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
pins.set(i, a);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. 못 사이를 sec·weight 로 채운다
|
|
132
|
+
const bounds: number[] = new Array(n + 1);
|
|
133
|
+
const keys = [...pins.keys()].sort((a, b) => a - b);
|
|
134
|
+
for (let k = 0; k < keys.length - 1; k++) {
|
|
135
|
+
const i0 = keys[k];
|
|
136
|
+
const i1 = keys[k + 1];
|
|
137
|
+
const t0 = pins.get(i0)!;
|
|
138
|
+
const lens = fill(scenes.slice(i0, i1), pins.get(i1)! - t0);
|
|
139
|
+
bounds[i0] = t0;
|
|
140
|
+
let t = t0;
|
|
141
|
+
for (let j = i0; j < i1; j++) {
|
|
142
|
+
t += lens[j - i0];
|
|
143
|
+
bounds[j + 1] = t;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
bounds[n] = duration;
|
|
147
|
+
|
|
148
|
+
// 3. 못이 아닌 경계를 마디·박에 붙인다. 고정 초 장면 곁은 0.25초 안에서만 움직인다.
|
|
149
|
+
if (beats) {
|
|
150
|
+
for (let i = 1; i < n; i++) {
|
|
151
|
+
if (pins.has(i)) continue;
|
|
152
|
+
// snap 을 안 적은 장면(고정 초 장면 곁)도 박에는 붙인다 — 검사가 박에서 어긋난 경계를 잡는다
|
|
153
|
+
const snap = scenes[i].snap ?? "beat";
|
|
154
|
+
if (snap === "none") continue;
|
|
155
|
+
const grid = snap === "bar" ? (beats.bars ?? beats.downbeats ?? beats.beats) : beats.beats;
|
|
156
|
+
if (!grid?.length) continue;
|
|
157
|
+
const fixedSide = scenes[i - 1].sec !== undefined || scenes[i].sec !== undefined;
|
|
158
|
+
const reach = fixedSide ? 0.25 : Math.min(bounds[i] - bounds[i - 1], bounds[i + 1] - bounds[i]) / 2;
|
|
159
|
+
const near = grid.reduce((a, b) => (Math.abs(b - bounds[i]) < Math.abs(a - bounds[i]) ? b : a));
|
|
160
|
+
if (Math.abs(near - bounds[i]) <= reach && near - bounds[i - 1] >= MIN_SCENE * 0.5 && bounds[i + 1] - near >= MIN_SCENE * 0.5) bounds[i] = near;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 4. 프레임에 맞춘다 — 경계가 프레임 사이에 걸리면 한 프레임에 두 장면이 겹치거나 빈다
|
|
165
|
+
const f = (x: number) => Math.round(x * tpl.fps) / tpl.fps;
|
|
166
|
+
const out = bounds.map(f);
|
|
167
|
+
return {
|
|
168
|
+
duration: out[n],
|
|
169
|
+
notes,
|
|
170
|
+
scenes: scenes.map((s, i) => ({ ...s, from: r4(out[i]), dur: r4(out[i + 1] - out[i]) })),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function r4(x: number): number {
|
|
175
|
+
return Math.round(x * 10000) / 10000;
|
|
176
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readKey } from "@kasaframe/gen";
|
|
4
|
+
|
|
5
|
+
// OG chat/completions 하나로 어떤 모델이든 부른다. 그림 보는 검사도 여기로 — OG /v1/messages 는 딥시크에 그림을 넘기지 않는다
|
|
6
|
+
const BASE = process.env.KF_OG_BASE ?? "https://apis.opengateway.ai/v1";
|
|
7
|
+
|
|
8
|
+
// Opus·GPT 계열은 한 번에 값이 크게 뛴다 — 장면 쓰기·고치기·그림 검사가 모두 이 문을 지나니 여기서 한 번만 막는다.
|
|
9
|
+
// 짚은 사람이 풀어야만(--allow-premium 이 KF_ALLOW_PREMIUM=1 로 넘긴다 — 자식 kf 까지 같은 값)
|
|
10
|
+
export function premiumBlock(model: string): string | null {
|
|
11
|
+
if (process.env.KF_ALLOW_PREMIUM === "1") return null;
|
|
12
|
+
const name = model.toLowerCase().split("/").pop() ?? "";
|
|
13
|
+
if (!/^claude.*opus|^opus/.test(name) && !/^(gpt|chatgpt|o\d+)([-.]|$)/.test(name)) return null;
|
|
14
|
+
return `${model} 은 비싼 모델(Opus·GPT 계열)이라 부르기 전에 막았다 — 꼭 써야 하면 --allow-premium(또는 KF_ALLOW_PREMIUM=1)`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Part = { type: "text"; text: string } | { type: "image_url"; image_url: { url: string } };
|
|
18
|
+
export interface Message {
|
|
19
|
+
role: "system" | "user" | "assistant";
|
|
20
|
+
content: string | Part[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 모델별로 쓴 만큼 — kf make 가 끝날 때(성공·실패·중단) <작업 폴더>/make-stats.json 에 남겨 모델끼리 시간·토큰·비용을 견준다.
|
|
24
|
+
// og 는 OG 를 거친 호출(chat·OG 그림) — 그 밖의 생성(fal 영상 따위)도 같은 장부에 두되 「OG 호출」 줄에는 안 센다
|
|
25
|
+
export interface Spent {
|
|
26
|
+
model: string;
|
|
27
|
+
stage: string;
|
|
28
|
+
ms: number;
|
|
29
|
+
prompt: number;
|
|
30
|
+
completion: number;
|
|
31
|
+
cached: number;
|
|
32
|
+
usd: number | null;
|
|
33
|
+
og: boolean;
|
|
34
|
+
}
|
|
35
|
+
const spent: Spent[] = [];
|
|
36
|
+
export const ledger = (): Spent[] => [...spent];
|
|
37
|
+
export const spend = (s: Spent): void => void spent.push(s);
|
|
38
|
+
|
|
39
|
+
interface Total {
|
|
40
|
+
calls: number;
|
|
41
|
+
prompt: number;
|
|
42
|
+
completion: number;
|
|
43
|
+
cached: number;
|
|
44
|
+
usd: number;
|
|
45
|
+
ms: number;
|
|
46
|
+
// 가격표를 못 받아 값을 모르는 호출 수 — usd 합에 안 들어갔다
|
|
47
|
+
unpriced: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sum(xs: Spent[]): Total {
|
|
51
|
+
const t: Total = { calls: xs.length, prompt: 0, completion: 0, cached: 0, usd: 0, ms: 0, unpriced: 0 };
|
|
52
|
+
for (const x of xs) {
|
|
53
|
+
t.prompt += x.prompt;
|
|
54
|
+
t.completion += x.completion;
|
|
55
|
+
t.cached += x.cached;
|
|
56
|
+
t.ms += x.ms;
|
|
57
|
+
if (x.usd === null) t.unpriced++;
|
|
58
|
+
else t.usd += x.usd;
|
|
59
|
+
}
|
|
60
|
+
t.usd = Number(t.usd.toFixed(6));
|
|
61
|
+
return t;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function spentLine(xs: Spent[] = spent): string {
|
|
65
|
+
const og = sum(xs.filter((x) => x.og));
|
|
66
|
+
const other = sum(xs.filter((x) => !x.og));
|
|
67
|
+
const head = og.calls ? `OG 호출 ${og.calls}번 · 입력 ${og.prompt.toLocaleString("en-US")}/출력 ${og.completion.toLocaleString("en-US")} 토큰 · $${og.usd.toFixed(4)}${og.unpriced ? `(값 모름 ${og.unpriced}번)` : ""}` : "OG 호출 0";
|
|
68
|
+
return other.calls ? `${head} · 그 밖 생성 ${other.calls}번 $${other.usd.toFixed(4)}` : head;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 한 kf make 실행이 한 판 — --finish·--rewrite 가 같은 폴더에 다시 돌아도 앞 판을 지우지 않고 뒤에 붙인다
|
|
72
|
+
export function writeStats(dir: string, run: { command: string; model: string; code: number | string }): string {
|
|
73
|
+
const file = join(dir, "make-stats.json");
|
|
74
|
+
let runs: unknown[] = [];
|
|
75
|
+
try {
|
|
76
|
+
runs = (JSON.parse(readFileSync(file, "utf8")) as { runs?: unknown[] }).runs ?? [];
|
|
77
|
+
} catch {}
|
|
78
|
+
const stages = ledger();
|
|
79
|
+
runs.push({ at: new Date().toISOString(), ...run, stages, total: sum(stages), og: sum(stages.filter((x) => x.og)), line: spentLine(stages) });
|
|
80
|
+
const all = (runs as { stages?: Spent[] }[]).flatMap((r) => r.stages ?? []);
|
|
81
|
+
writeFileSync(file, `${JSON.stringify({ runs, total: sum(all), og: sum(all.filter((x) => x.og)), line: spentLine(all) }, null, 2)}\n`);
|
|
82
|
+
return file;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let prices: Record<string, { inputCostPerToken?: number; outputCostPerToken?: number; cacheReadInputTokenCost?: number }> | null = null;
|
|
86
|
+
// 공개 가격표(키 없이). 못 받으면 비용은 null 로 두고 토큰만 남긴다
|
|
87
|
+
async function priceOf(model: string) {
|
|
88
|
+
if (!prices) {
|
|
89
|
+
try {
|
|
90
|
+
prices = (await (await fetch("https://opengateway.ai/api/model-prices", { signal: AbortSignal.timeout(10000) })).json()) as typeof prices;
|
|
91
|
+
} catch {
|
|
92
|
+
prices = {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return prices?.[model] ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ChatResult {
|
|
99
|
+
text: string;
|
|
100
|
+
// "length" 면 답이 상한에서 잘렸다
|
|
101
|
+
finish?: string;
|
|
102
|
+
usage?: { prompt_tokens?: number; completion_tokens?: number };
|
|
103
|
+
ms: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function chat(
|
|
107
|
+
model: string,
|
|
108
|
+
messages: Message[],
|
|
109
|
+
o: { temperature?: number; maxTokens?: number; json?: boolean; timeoutSec?: number; effort?: "low" | "medium" | "high"; stage?: string } = {},
|
|
110
|
+
): Promise<ChatResult> {
|
|
111
|
+
const blocked = premiumBlock(model);
|
|
112
|
+
if (blocked) throw new Error(blocked);
|
|
113
|
+
const t0 = Date.now();
|
|
114
|
+
const body: Record<string, unknown> = { model, messages, max_tokens: o.maxTokens ?? 8000 };
|
|
115
|
+
// 추론을 켠 Claude 는 temperature 를 받지 않는다
|
|
116
|
+
if (o.effort) body.reasoning_effort = o.effort;
|
|
117
|
+
else body.temperature = o.temperature ?? 0.4;
|
|
118
|
+
if (o.json) body.response_format = { type: "json_object" };
|
|
119
|
+
let last: unknown;
|
|
120
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
const r = await fetch(`${BASE}/chat/completions`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${readKey("opengateway")}` },
|
|
125
|
+
body: JSON.stringify(body),
|
|
126
|
+
signal: AbortSignal.timeout((o.timeoutSec ?? 300) * 1000),
|
|
127
|
+
});
|
|
128
|
+
const j = (await r.json().catch(() => ({}))) as any;
|
|
129
|
+
if (!r.ok) {
|
|
130
|
+
const msg = j?.error?.message ?? JSON.stringify(j).slice(0, 300);
|
|
131
|
+
if (r.status >= 500 || r.status === 429) throw new Error(`OG ${r.status}: ${msg}`);
|
|
132
|
+
throw Object.assign(new Error(`OG ${r.status}: ${msg}`), { fatal: true });
|
|
133
|
+
}
|
|
134
|
+
const ms = Date.now() - t0;
|
|
135
|
+
const u = j.usage ?? {};
|
|
136
|
+
const p = await priceOf(model);
|
|
137
|
+
const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
|
|
138
|
+
const prompt = u.prompt_tokens ?? 0;
|
|
139
|
+
const completion = u.completion_tokens ?? 0;
|
|
140
|
+
const usd = p?.inputCostPerToken !== undefined ? (prompt - cached) * p.inputCostPerToken + cached * (p.cacheReadInputTokenCost ?? p.inputCostPerToken) + completion * (p.outputCostPerToken ?? 0) : null;
|
|
141
|
+
spent.push({ model, stage: o.stage ?? "", ms, prompt, completion, cached, usd, og: true });
|
|
142
|
+
return { text: j.choices?.[0]?.message?.content ?? "", finish: j.choices?.[0]?.finish_reason, usage: j.usage, ms };
|
|
143
|
+
} catch (e) {
|
|
144
|
+
if ((e as any)?.fatal) throw e;
|
|
145
|
+
last = e;
|
|
146
|
+
// 429 는 잠깐 쉬어서는 안 풀린다 — 더 길게 기다린다
|
|
147
|
+
const wait = /OG 429/.test(String(e)) ? 5000 * (attempt + 1) : 1500 * (attempt + 1);
|
|
148
|
+
await new Promise((res) => setTimeout(res, wait));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 모델이 설명을 붙여도 코드 블록 하나만 건진다. 닫는 울타리가 없으면(답이 잘렸거나 빠뜨림) 여는 울타리 뒤를 다 쓴다
|
|
155
|
+
export function codeBlock(text: string, lang = "ts"): string | null {
|
|
156
|
+
const open = "```(?:" + lang + "|typescript|tsx|js|javascript)?[ \\t]*\\n";
|
|
157
|
+
const blocks = [...text.matchAll(new RegExp(open + "([\\s\\S]*?)\\n```", "g"))].map((m) => m[1]);
|
|
158
|
+
if (blocks.length) return blocks.sort((a, b) => b.length - a.length)[0].trim() + "\n";
|
|
159
|
+
const lone = text.match(new RegExp(open + "([\\s\\S]*)$"));
|
|
160
|
+
const body = (lone ? lone[1] : text).replace(/\n?```\s*$/, "").trim();
|
|
161
|
+
return /export default/.test(body) ? body + "\n" : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function jsonBlock<T>(text: string): T | null {
|
|
165
|
+
const m = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
|
|
166
|
+
const raw = m ? m[1] : text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1);
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(raw) as T;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { loadTheme } from "@kasaframe/themes";
|
|
7
|
+
import { chat, jsonBlock, type Part } from "./llm.ts";
|
|
8
|
+
|
|
9
|
+
// 기본은 세션이 직접 본다 — 장면 모음 한 장과 장면별 확인표를 만들어 경로만 찍는다(외부 모델 0, 돈 0).
|
|
10
|
+
// 그림 보는 모델에 남이 대신 보게 하면 장면의 할 일·테마를 모르는 채 겉만 보고, 그림을 볼 수 있는 세션까지 돈을 썼다.
|
|
11
|
+
// 그림을 못 보는 세션(OG 로 붙인 DeepSeek 같은 — OG /v1/messages 는 그림을 버린다)만 --vision-model 로 다른 모델의 눈을 빌린다
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_EYES = "google/gemini-3.5-flash";
|
|
14
|
+
// 기본 눈이 막히면(429 등) 넘어갈 곳 — 둘 다 chat/completions 로 그림을 받는다
|
|
15
|
+
const SPARE_EYES = "anthropic/claude-haiku-5-5";
|
|
16
|
+
const kfBin = fileURLToPath(new URL("../../cli/src/kf.ts", import.meta.url));
|
|
17
|
+
const VIDEO = /\.(mp4|mov|webm|m4v)$/i;
|
|
18
|
+
|
|
19
|
+
export interface LookScene {
|
|
20
|
+
id: string;
|
|
21
|
+
from: number;
|
|
22
|
+
to: number;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
problems: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LookReport {
|
|
28
|
+
model: string;
|
|
29
|
+
scenes: LookScene[];
|
|
30
|
+
summary: string;
|
|
31
|
+
answer?: string; // --ask 에 대한 답
|
|
32
|
+
ms: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface KfLike {
|
|
36
|
+
scenes: { id: string; from: number; dur: number; idea?: string; role?: string }[];
|
|
37
|
+
theme?: string;
|
|
38
|
+
text?: { policy?: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findProject(start: string): string | null {
|
|
42
|
+
let d = resolve(start);
|
|
43
|
+
for (let i = 0; i < 3; i++) {
|
|
44
|
+
if (existsSync(join(d, "kf.json"))) return d;
|
|
45
|
+
d = dirname(d);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 폴더면 kf sheet(장면마다 가운데 한 장을 모은 한 장, 렌더 잠금), mp4 면 장면 가운데마다 ffmpeg 로 한 장씩
|
|
51
|
+
function pictures(target: string, kf: KfLike | null, dir: string | null): { parts: Part[]; note: string } {
|
|
52
|
+
if (!VIDEO.test(target)) {
|
|
53
|
+
const out = join(dir!, "out", "look-sheet.png");
|
|
54
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
55
|
+
// 다른 렌더가 잠금을 오래 쥐고 있으면 기다리지 않고, 구워 둔 영상이 있으면 그걸 본다
|
|
56
|
+
const baked = ["video.mp4", "render.mp4"].map((f) => join(dir!, "out", f)).find((f) => existsSync(f));
|
|
57
|
+
const r = spawnSync(process.execPath, [kfBin, "sheet", dir!, "--out", out, "--cols", "4", "--width", "480", "--lock-wait", baked ? "20" : "900"], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
58
|
+
if ((r.status !== 0 || !existsSync(out)) && baked) return pictures(baked, kf, dir);
|
|
59
|
+
if (r.status !== 0 || !existsSync(out)) throw new Error(`kf sheet 이 실패했다: ${(r.stderr ?? "").trim().split("\n").slice(-2).join(" ")}`);
|
|
60
|
+
return { parts: [{ type: "image_url", image_url: { url: `data:image/png;base64,${readFileSync(out).toString("base64")}` } }], note: "장면 모음 한 장 — 왼쪽 위부터 오른쪽으로, 줄을 바꿔 가며 장면 순서대로 한 칸씩(4열)." };
|
|
61
|
+
}
|
|
62
|
+
const tmp = join(tmpdir(), `kf-look-${process.pid}`);
|
|
63
|
+
mkdirSync(tmp, { recursive: true });
|
|
64
|
+
const times = kf?.scenes.length ? kf.scenes.map((s) => s.from + s.dur / 2) : Array.from({ length: 12 }, (_, i) => i * 5 + 2.5);
|
|
65
|
+
const parts: Part[] = [];
|
|
66
|
+
try {
|
|
67
|
+
times.forEach((t, i) => {
|
|
68
|
+
const f = join(tmp, `${i}.jpg`);
|
|
69
|
+
spawnSync("ffmpeg", ["-v", "error", "-y", "-ss", t.toFixed(3), "-i", target, "-frames:v", "1", "-vf", "scale=640:-1", "-q:v", "4", f]);
|
|
70
|
+
if (!existsSync(f)) return;
|
|
71
|
+
parts.push({ type: "text", text: `${kf?.scenes[i]?.id ?? `${t.toFixed(1)}초`}` }, { type: "image_url", image_url: { url: `data:image/jpeg;base64,${readFileSync(f).toString("base64")}` } });
|
|
72
|
+
});
|
|
73
|
+
} finally {
|
|
74
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
if (!parts.length) throw new Error(`영상에서 프레임을 못 뽑았다: ${target}`);
|
|
77
|
+
return { parts, note: "장면마다 가운데 한 장 — 그림 앞에 장면 이름." };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function open(target: string): { dir: string | null; kf: KfLike | null } {
|
|
81
|
+
const dir = findProject(target);
|
|
82
|
+
const kf: KfLike | null = dir ? JSON.parse(readFileSync(join(dir, "kf.json"), "utf8")) : null;
|
|
83
|
+
if (!dir && !VIDEO.test(target)) throw new Error(`${target} 에 kf.json 이 없다 — 프로젝트 폴더나 mp4 를 준다`);
|
|
84
|
+
return { dir, kf };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function themeOf(kf: KfLike | null): { name: string; metaphor: string; guide: string } | null {
|
|
88
|
+
if (!kf?.theme) return null;
|
|
89
|
+
try {
|
|
90
|
+
return loadTheme(kf.theme);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface LookSheet {
|
|
97
|
+
sheet: string;
|
|
98
|
+
checklist: string;
|
|
99
|
+
scenes: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 세션이 직접 볼 것 — 장면 모음 한 장(장면마다 가운데 한 장, 4열)과 장면별 확인표(look.md). 폴더면 out/, mp4 면 옆에 */
|
|
103
|
+
export function lookSheet(target: string): LookSheet {
|
|
104
|
+
const { dir, kf } = open(target);
|
|
105
|
+
const isVideo = VIDEO.test(target);
|
|
106
|
+
const outDir = isVideo ? dirname(resolve(target)) : join(dir!, "out");
|
|
107
|
+
mkdirSync(outDir, { recursive: true });
|
|
108
|
+
const sheet = join(outDir, "look-sheet.png");
|
|
109
|
+
const scenes = kf?.scenes ?? [];
|
|
110
|
+
if (isVideo) videoSheet(target, scenes, sheet);
|
|
111
|
+
else {
|
|
112
|
+
const baked = ["video.mp4", "render.mp4"].map((f) => join(dir!, "out", f)).find((f) => existsSync(f));
|
|
113
|
+
const r = spawnSync(process.execPath, [kfBin, "sheet", dir!, "--out", sheet, "--cols", "4", "--width", "480", "--lock-wait", baked ? "20" : "900"], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
114
|
+
if ((r.status !== 0 || !existsSync(sheet)) && baked) videoSheet(baked, scenes, sheet);
|
|
115
|
+
else if (r.status !== 0 || !existsSync(sheet)) throw new Error(`kf sheet 이 실패했다: ${(r.stderr ?? "").trim().split("\n").slice(-2).join(" ")}`);
|
|
116
|
+
}
|
|
117
|
+
const checklist = join(outDir, "look.md");
|
|
118
|
+
writeFileSync(checklist, checklistText(kf, sheet));
|
|
119
|
+
return { sheet, checklist, scenes: scenes.length };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// mp4 — 장면 가운데마다 한 장을 뽑아 4열로 붙인다(kf sheet 와 같은 차례)
|
|
123
|
+
function videoSheet(file: string, scenes: KfLike["scenes"], out: string): void {
|
|
124
|
+
const times = scenes.length ? scenes.map((s) => s.from + s.dur / 2) : Array.from({ length: 12 }, (_, i) => i * 5 + 2.5);
|
|
125
|
+
const tmp = join(tmpdir(), `kf-look-${process.pid}`);
|
|
126
|
+
mkdirSync(tmp, { recursive: true });
|
|
127
|
+
try {
|
|
128
|
+
let n = 0;
|
|
129
|
+
for (const t of times) {
|
|
130
|
+
const f = join(tmp, `${String(n).padStart(3, "0")}.png`);
|
|
131
|
+
spawnSync("ffmpeg", ["-v", "error", "-y", "-ss", t.toFixed(3), "-i", file, "-frames:v", "1", "-vf", "scale=480:-2", f]);
|
|
132
|
+
if (existsSync(f)) n++;
|
|
133
|
+
}
|
|
134
|
+
if (!n) throw new Error(`영상에서 프레임을 못 뽑았다: ${file}`);
|
|
135
|
+
const cols = Math.min(4, n);
|
|
136
|
+
spawnSync("ffmpeg", ["-v", "error", "-y", "-start_number", "0", "-i", join(tmp, "%03d.png"), "-vf", `tile=${cols}x${Math.ceil(n / cols)}:padding=6:color=black`, "-frames:v", "1", out]);
|
|
137
|
+
if (!existsSync(out)) throw new Error(`장면 모음을 못 붙였다: ${out}`);
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 정지 그림으로 볼 수 있는 것만 — 움직임·박·섬광·카메라 이동은 kf check 가 숫자로 본다
|
|
144
|
+
function checklistText(kf: KfLike | null, sheet: string): string {
|
|
145
|
+
const theme = themeOf(kf);
|
|
146
|
+
const scenes = kf?.scenes ?? [];
|
|
147
|
+
const common = [
|
|
148
|
+
"화면이 비지 않았다 — 주 요소가 안전 영역을 크게 채운다(세로 화면은 폭 70% 넘게)",
|
|
149
|
+
"글이 읽힌다 — 깨진 글자(빈 네모·undefined·NaN·null) 없음, 바탕과 대비가 선다, 잘림·겹침 없음",
|
|
150
|
+
"주인공이 안전 영역 가운데에 선다 — 가장자리로 밀리거나 잘리지 않았다",
|
|
151
|
+
`화면 글 정책(${kf?.text?.policy ?? "모름"})을 지킨다`,
|
|
152
|
+
"뜻 없는 장식이 없다 — 움직이거나 놓인 것은 다 내용 하나를 뜻한다(떠다니는 판·빛 방울·큐브·흩뿌린 입자 없음, 바탕은 정지거나 아주 옅은 질감)",
|
|
153
|
+
"3D↔2D 가 끊기지 않는다 — 3D 샷과 2D 장면 사이는 공간 잇기 이음매(rise·land)나 투영을 맞춘 컷으로, 같은 물체·자리가 이어진다",
|
|
154
|
+
...(theme ? [`테마 ${theme.name}(${theme.metaphor})의 생김새·금지를 지킨다 — themes/${kf?.theme}/THEME.md`] : []),
|
|
155
|
+
];
|
|
156
|
+
return [
|
|
157
|
+
"# kf look — 장면별 확인표",
|
|
158
|
+
"",
|
|
159
|
+
`장면 모음: \`${sheet}\` — 왼쪽 위부터 오른쪽으로, 줄을 바꿔 가며 장면 차례대로 한 칸씩(4열). 세션이 Read 로 직접 본다.`,
|
|
160
|
+
"그림을 못 보는 세션이면 `kf look <폴더> --vision-model google/gemini-3.5-flash` 로 다른 모델이 대신 본다.",
|
|
161
|
+
"정지 그림으로 못 보는 것(0.1초 멈춤·같은 구도·박·섬광·카메라 이동·주인공 가장자리)은 `kf check` 가 본다.",
|
|
162
|
+
"",
|
|
163
|
+
"장면마다:",
|
|
164
|
+
...common.map((c) => `- [ ] ${c}`),
|
|
165
|
+
"",
|
|
166
|
+
...scenes.flatMap((s, i) => [
|
|
167
|
+
`## ${i + 1}. ${s.id} — ${s.from.toFixed(2)}–${(s.from + s.dur).toFixed(2)}초${s.role ? ` · ${s.role}` : ""} (칸 ${i + 1})`,
|
|
168
|
+
"",
|
|
169
|
+
...(s.idea ? [`할 일: ${s.idea}`, "", "- [ ] 할 일대로의 화면이다"] : []),
|
|
170
|
+
"- [ ] 위 「장면마다」를 다 넘는다",
|
|
171
|
+
"",
|
|
172
|
+
]),
|
|
173
|
+
].join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 그림을 못 보는 세션용 — 장면 모음을 그림 보는 모델(model)에 보내 장면별 글 보고. 한 번에 1센트 안팎 */
|
|
177
|
+
export async function look(target: string, o: { ask?: string; model?: string } = {}): Promise<LookReport> {
|
|
178
|
+
const model = o.model ?? DEFAULT_EYES;
|
|
179
|
+
const { dir, kf } = open(target);
|
|
180
|
+
const { parts, note } = pictures(target, kf, dir);
|
|
181
|
+
const scenes = kf?.scenes ?? [];
|
|
182
|
+
const t = themeOf(kf);
|
|
183
|
+
const theme = t ? `\n테마 ${t.name} — ${t.metaphor}\n테마 지침(금지 포함):\n${t.guide.slice(0, 2500)}` : "";
|
|
184
|
+
const prompt = `영상 장면을 검수한다. ${note}
|
|
185
|
+
장면: ${JSON.stringify(scenes.map((s) => ({ id: s.id, from: +s.from.toFixed(2), to: +(s.from + s.dur).toFixed(2), role: s.role, idea: s.idea })))}
|
|
186
|
+
화면 글 정책: ${kf?.text?.policy ?? "모름"}${theme}
|
|
187
|
+
|
|
188
|
+
장면마다 그림만 보고 문제를 짧게 적는다 — 빈 화면·화면을 못 채운 작은 요소·글자 깨짐(빈 네모·undefined·NaN)·겹침·잘림·대비 낮은 글·할 일(idea)과 다른 화면·테마 위반·뜻 없는 장식(내용과 상관없이 떠다니는 판·빛 방울·큐브·입자)·3D 장면과 2D 장면 사이가 끊김(앞뒤가 다른 세계로 튐). 정지 그림이라 박·움직임은 판단하지 않는다. 문제가 없으면 problems 를 비운다.${o.ask ? `\n그리고 이것에 답한다: ${o.ask}` : ""}
|
|
189
|
+
JSON 하나로만: {"scenes":[{"id":"…","problems":["…"]}],"summary":"전체 한두 문장"${o.ask ? ',"answer":"…"' : ""}}`;
|
|
190
|
+
const ask = (m: string) => chat(m, [{ role: "user", content: [...parts, { type: "text", text: prompt }] }], { json: true, temperature: 0.2, maxTokens: 6000, stage: "look" });
|
|
191
|
+
let used = model;
|
|
192
|
+
let r;
|
|
193
|
+
try {
|
|
194
|
+
r = await ask(model);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (o.model) throw e;
|
|
197
|
+
used = SPARE_EYES;
|
|
198
|
+
r = await ask(SPARE_EYES);
|
|
199
|
+
}
|
|
200
|
+
const j = jsonBlock<{ scenes?: { id: string; problems?: string[] }[]; summary?: string; answer?: string }>(r.text);
|
|
201
|
+
if (!j) throw new Error(`그림 보는 모델 답을 못 읽었다: ${r.text.slice(0, 200)}`);
|
|
202
|
+
const byId = new Map((j.scenes ?? []).map((s) => [s.id, s.problems ?? []]));
|
|
203
|
+
const rows: LookScene[] = scenes.length
|
|
204
|
+
? scenes.map((s) => {
|
|
205
|
+
const problems = byId.get(s.id) ?? [];
|
|
206
|
+
return { id: s.id, from: s.from, to: s.from + s.dur, ok: !problems.length, problems };
|
|
207
|
+
})
|
|
208
|
+
: (j.scenes ?? []).map((s) => ({ id: s.id, from: 0, to: 0, ok: !s.problems?.length, problems: s.problems ?? [] }));
|
|
209
|
+
return { model: used, scenes: rows, summary: j.summary ?? "", answer: j.answer, ms: r.ms };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function formatLook(r: LookReport): string {
|
|
213
|
+
const bad = r.scenes.filter((s) => !s.ok);
|
|
214
|
+
return [
|
|
215
|
+
`kf look — ${r.model} · ${(r.ms / 1000).toFixed(1)}초 · 장면 ${r.scenes.length}개 중 문제 ${bad.length}개`,
|
|
216
|
+
...r.scenes.map((s) => `${s.ok ? " 통과" : " [문제]"} ${s.id}${s.to ? ` ${s.from.toFixed(2)}–${s.to.toFixed(2)}초` : ""}${s.problems.length ? ` — ${s.problems.join(" / ")}` : ""}`),
|
|
217
|
+
r.summary ? `요약: ${r.summary}` : "",
|
|
218
|
+
r.answer ? `답: ${r.answer}` : "",
|
|
219
|
+
]
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
.join("\n");
|
|
222
|
+
}
|