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,172 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { listThemes, sheetDir, type Theme, themeDims, themesDir } from "@kasaframe/themes";
|
|
6
|
+
import { briefs, review, scaffold, sessionPrompt, setStatus, staticCheck } from "@kasaframe/themes/factory";
|
|
7
|
+
import { launch } from "../browser.ts";
|
|
8
|
+
import { type Command, num, parseFlags } from "../command.ts";
|
|
9
|
+
|
|
10
|
+
const kfBin = new URL("../kf.ts", import.meta.url).pathname;
|
|
11
|
+
|
|
12
|
+
const cmd: Command = {
|
|
13
|
+
summary: "테마 공장 — 참고 자료로 테마 팩 뼈대·지시서, 계약 검사, 견본 review, 사람 OK 뒤 approve",
|
|
14
|
+
usage: `kf theme list
|
|
15
|
+
kf theme new <id> --ref <그림·영상·폴더·URL> [--ref …] [--name 이름] [--like <기존 테마>] [--3d] [--brief ${briefs().join("|")}] [--force]
|
|
16
|
+
kf theme check <id> [--no-page] 계약·빈칸 검사 + kf check sample
|
|
17
|
+
kf theme review <id> [--tabs 2] 굽기 → mp4 검사 → 장면 모음·대표 장면 → 공유 폴더 → status review
|
|
18
|
+
kf theme approve <id> 사람이 견본을 보고 OK 한 뒤에만
|
|
19
|
+
kf theme sheet [--out 파일.png] approved 테마마다 같은 문장 한 장면(쇼츠 9:16)을 감독 모드로 짓고 검사를 거쳐, 장면 끝 프레임을
|
|
20
|
+
격자 한 장으로 — 이름·2D/3D 딱지, 안전 영역 점선. 기본 themes/_sheet/sheet.png, 칸은 그 옆 <id>.png
|
|
21
|
+
(kf make 의 테마 질문 보기가 이 칸을 견본으로 짚는다). 검사 오류가 남은 칸은 표시하고 종료 1
|
|
22
|
+
|
|
23
|
+
new 는 themes/<id>/ 에 계약 파일 뼈대·refs/·브리프대로 자른 곡과 박·장면 넷 뼈대·THEME-MAKE.md(지시서)를 짓고,
|
|
24
|
+
그 지시서를 받을 세션을 여는 명령을 알려 준다. 테마는 그 세션이 만든다.`,
|
|
25
|
+
async run(argv) {
|
|
26
|
+
const [sub, ...rest] = argv;
|
|
27
|
+
const { args, opts } = parseFlags(rest);
|
|
28
|
+
const id = args[0];
|
|
29
|
+
if (sub === "sheet") return sheet(resolve(typeof opts.out === "string" ? opts.out : join(sheetDir, "sheet.png")));
|
|
30
|
+
if (sub === "list" || !sub) {
|
|
31
|
+
const all = listThemes();
|
|
32
|
+
if (!all.length) console.log("(테마 없음)");
|
|
33
|
+
for (const t of all) console.log(`${t.id.padEnd(18)} ${t.status.padEnd(9)} ${String(t.approvedAt ?? "").padEnd(11)} 효과 ${String(t.effects.length).padStart(2)} · ${t.metaphor}`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (!id) {
|
|
37
|
+
console.error(`kf theme ${sub}: 테마 id 가 필요하다. kf theme --help`);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
if (sub === "new") {
|
|
41
|
+
// --ref 는 여러 번 — parseFlags 는 마지막 하나만 남기니 따로 모은다
|
|
42
|
+
const refs = rest.flatMap((a, i) => (a === "--ref" && rest[i + 1] ? [rest[i + 1]] : a.startsWith("--ref=") ? [a.slice(6)] : []));
|
|
43
|
+
try {
|
|
44
|
+
const r = scaffold(id, {
|
|
45
|
+
refs,
|
|
46
|
+
name: typeof opts.name === "string" ? opts.name : undefined,
|
|
47
|
+
like: typeof opts.like === "string" ? opts.like : undefined,
|
|
48
|
+
brief: typeof opts.brief === "string" ? opts.brief : undefined,
|
|
49
|
+
kind: opts["3d"] ? "3d" : "flat",
|
|
50
|
+
force: !!opts.force,
|
|
51
|
+
});
|
|
52
|
+
console.log(`${r.dir}/ — 참고 자료 ${r.refs.length}개${r.urls.length ? ` · 주소 ${r.urls.length}개` : ""}`);
|
|
53
|
+
console.log(`지시서: ${r.make}`);
|
|
54
|
+
console.log(`\n세션 열기(레포 뿌리에서):\n claude --model opus "${sessionPrompt(id)}"`);
|
|
55
|
+
return 0;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(`kf theme new: ${(e as Error).message}`);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (sub === "check") {
|
|
62
|
+
const bad = await staticCheck(id);
|
|
63
|
+
for (const b of bad) console.log(`[오류] ${b}`);
|
|
64
|
+
if (!bad.length) console.log("계약·빈칸: 통과");
|
|
65
|
+
let page = 0;
|
|
66
|
+
if (!opts["no-page"]) page = spawnSync(process.execPath, [kfBin, "check", join(themesDir, id, "sample"), "--tabs", String(num(opts.tabs, 2))], { stdio: "inherit" }).status ?? 1;
|
|
67
|
+
return bad.length || page ? 1 : 0;
|
|
68
|
+
}
|
|
69
|
+
if (sub === "review") {
|
|
70
|
+
const bad = await staticCheck(id);
|
|
71
|
+
if (bad.length) {
|
|
72
|
+
for (const b of bad) console.log(`[오류] ${b}`);
|
|
73
|
+
console.error("kf theme review: 계약·빈칸 오류부터 — kf theme check");
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const r = review(id, { tabs: num(opts.tabs, 2), brief: typeof opts.brief === "string" ? opts.brief : undefined });
|
|
77
|
+
for (const n of r.notes) console.log(n);
|
|
78
|
+
if (!r.ok) return 1;
|
|
79
|
+
console.log(`status review · 공유 폴더 ${r.share}`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
if (sub === "approve") {
|
|
83
|
+
const m = setStatus(id, "approved");
|
|
84
|
+
console.log(`${id} approved ${m.approvedAt} — 사람이 견본을 보고 OK 한 테마만`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
console.error(`kf theme: 모르는 하위 명령 "${sub}". kf theme --help`);
|
|
88
|
+
return 2;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default cmd;
|
|
93
|
+
|
|
94
|
+
// 견본 시트 — 테마끼리 견주려면 내용이 같아야 한다. 감독 모드가 실제로 짓는 장면(쇼츠 첫 장면 hook-number)을 테마만 바꿔,
|
|
95
|
+
// 검사를 거쳐(떨어진 패턴은 감독이 다음 후보로 다시 짓는다) 사람이 실제로 받을 화면을 장면 끝 프레임에서 찍는다 — 움직임이 다 앉은 자리
|
|
96
|
+
const SAMPLE = {
|
|
97
|
+
title: "kasaframe",
|
|
98
|
+
request: "테마 견본 — 같은 문장을 테마마다",
|
|
99
|
+
scenes: [{ id: "hook", type: "hook-number", value: 57, unit: "×", label: "KV 캐시가 이만큼 작아진다", source: "DeepSeek-V2 기술 보고서", dur: 4 }],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
async function sheet(out: string): Promise<number> {
|
|
103
|
+
const themes = listThemes({ status: "approved" });
|
|
104
|
+
if (!themes.length) {
|
|
105
|
+
console.error("kf theme sheet: approved 테마가 없다");
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
109
|
+
const tmp = mkdtempSync(join(tmpdir(), "kf-theme-sheet-"));
|
|
110
|
+
const cells: { t: Theme; png: string; failed: boolean }[] = [];
|
|
111
|
+
let frame = { width: 1080, height: 1920, safe: { top: 0, bottom: 0, left: 0, right: 0 }, at: 0 };
|
|
112
|
+
try {
|
|
113
|
+
for (const t of themes) {
|
|
114
|
+
const dir = join(tmp, t.id);
|
|
115
|
+
const png = join(dirname(out), `${t.id}.png`);
|
|
116
|
+
const kf = (...a: string[]) => spawnSync(process.execPath, [kfBin, ...a], { encoding: "utf8" });
|
|
117
|
+
let r = kf("new", dir, "--template", "shorts", "--theme", t.id, "--title", SAMPLE.title, "--series", "none");
|
|
118
|
+
if (r.status === 0) {
|
|
119
|
+
writeFileSync(join(dir, "content.json"), `${JSON.stringify(SAMPLE, null, 2)}\n`);
|
|
120
|
+
r = kf("direct", dir, "--no-narrate", "--no-lock");
|
|
121
|
+
}
|
|
122
|
+
// 1 은 다시 지어도 검사 오류가 남았다는 뜻 — 찍되 칸에 표시한다
|
|
123
|
+
const failed = r.status === 1;
|
|
124
|
+
if (r.status === 0 || failed) {
|
|
125
|
+
const p = JSON.parse(readFileSync(join(dir, "kf.json"), "utf8"));
|
|
126
|
+
const scene = p.scenes.find((x: { id: string }) => x.id === SAMPLE.scenes[0].id);
|
|
127
|
+
frame = { width: p.width, height: p.height, safe: p.safeArea, at: +(scene.from + scene.dur - 1 / p.fps).toFixed(4) };
|
|
128
|
+
r = kf("still", dir, "--t", String(frame.at), "--out", png);
|
|
129
|
+
}
|
|
130
|
+
if (r.status !== 0) {
|
|
131
|
+
console.error(`테마 ${t.id}: 못 찍음 — ${`${r.stderr ?? ""}${r.stdout ?? ""}`.trim().split("\n").slice(-3).join(" / ")}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
console.error(`테마 ${t.id}: ${png}${failed ? " — 검사 오류가 남았다(out/check.json)" : ""}`);
|
|
135
|
+
cells.push({ t, png, failed });
|
|
136
|
+
}
|
|
137
|
+
if (!cells.length) return 1;
|
|
138
|
+
const w = 360;
|
|
139
|
+
const h = Math.round((w * frame.height) / frame.width);
|
|
140
|
+
const k = w / frame.width;
|
|
141
|
+
const html = `<!doctype html><meta charset="utf-8"><style>
|
|
142
|
+
body { margin: 0; padding: 24px; background: #111; font: 16px/1.35 -apple-system, "Apple SD Gothic Neo", sans-serif; color: #ddd; }
|
|
143
|
+
h1 { margin: 0 0 16px; font-size: 18px; font-weight: 600; color: #fff; }
|
|
144
|
+
main { display: grid; grid-template-columns: repeat(${cells.length}, ${w}px); gap: 16px; }
|
|
145
|
+
figure { margin: 0; position: relative; }
|
|
146
|
+
.safe { position: absolute; left: 0; width: ${w}px; border-top: 2px dashed #ff4d4d; }
|
|
147
|
+
img { display: block; width: ${w}px; height: ${h}px; outline: 1px solid #333; }
|
|
148
|
+
figcaption { padding: 8px 2px 0; }
|
|
149
|
+
b { color: #fff; }
|
|
150
|
+
.d { display: inline-block; margin-left: 6px; padding: 0 6px; border: 1px solid #666; border-radius: 4px; font-size: 13px; }
|
|
151
|
+
small { display: block; color: #999; font-size: 13px; }
|
|
152
|
+
.bad { color: #ff6b6b; }
|
|
153
|
+
</style><h1>같은 문장 한 장면 — 「${SAMPLE.scenes[0].value}${SAMPLE.scenes[0].unit} ${SAMPLE.scenes[0].label}」 · 쇼츠 9:16 · 장면 끝 프레임(${frame.at}초) · 빨간 점선 밖은 플랫폼 UI 자리라 비운다</h1><main>${cells
|
|
154
|
+
.map(
|
|
155
|
+
({ t, png, failed }) =>
|
|
156
|
+
`<figure><img src="data:image/png;base64,${readFileSync(png).toString("base64")}"><div class="safe" style="top:${Math.round(frame.safe.top * k)}px"></div><div class="safe" style="top:${Math.round((frame.height - frame.safe.bottom) * k)}px"></div><figcaption><b>${t.name}</b><span class="d">${themeDims(t)}</span><small>${t.id}${failed ? ' · <span class="bad">검사 오류 남음</span>' : ""}</small></figcaption></figure>`,
|
|
157
|
+
)
|
|
158
|
+
.join("")}</main>`;
|
|
159
|
+
const browser = await launch();
|
|
160
|
+
try {
|
|
161
|
+
const page = await browser.newPage({ viewport: { width: 48 + cells.length * w + (cells.length - 1) * 16, height: 400 } });
|
|
162
|
+
await page.setContent(html, { waitUntil: "load" });
|
|
163
|
+
writeFileSync(out, await page.screenshot({ fullPage: true, type: "png" }));
|
|
164
|
+
} finally {
|
|
165
|
+
await browser.close();
|
|
166
|
+
}
|
|
167
|
+
console.log(out);
|
|
168
|
+
return cells.length === themes.length && !cells.some((c) => c.failed) ? 0 : 1;
|
|
169
|
+
} finally {
|
|
170
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from "../command.ts";
|
|
2
|
+
import { parseFlags } from "../command.ts";
|
|
3
|
+
import { CATALOG, type TransitionEntry } from "@kasaframe/motion/transitions2d";
|
|
4
|
+
|
|
5
|
+
const USE: Record<TransitionEntry["use"], string> = { transition3d: "이름으로", react: "React 부품", vendor: "원본만" };
|
|
6
|
+
|
|
7
|
+
const cmd: Command = {
|
|
8
|
+
summary: "전환 목록 — 이름·출처·라이선스·seek/step",
|
|
9
|
+
usage: `kf transitions [찾을 말] [--source 출처] [--clock seek|step] [--use transition3d|react|vendor] [--json]
|
|
10
|
+
이름으로(transition3d) 인 것은 장면에서 transition3d(root, ctx, { kind: "<이름>" }) 로 부른다 — 예 gl:crosswarp · smooth:sdf-circle · dive
|
|
11
|
+
React 부품은 @kasaframe/motion/transitions2d/react, 원본만은 geono-hub vendor/transitions/ 에 있다
|
|
12
|
+
step 은 자기 시계(rAF·타이머)라 kf.json 의 mode 를 "step" 으로`,
|
|
13
|
+
async run(argv) {
|
|
14
|
+
const { args, opts } = parseFlags(argv);
|
|
15
|
+
const has = (v: string, q: unknown) => typeof q !== "string" || v.toLowerCase().includes(q.toLowerCase());
|
|
16
|
+
const list = CATALOG.filter(
|
|
17
|
+
(e) =>
|
|
18
|
+
has(e.source, opts.source) &&
|
|
19
|
+
(typeof opts.clock !== "string" || e.clock === opts.clock) &&
|
|
20
|
+
(typeof opts.use !== "string" || e.use === opts.use) &&
|
|
21
|
+
args.every((q) => has(`${e.name} ${e.note ?? ""} ${e.author ?? ""}`, q)),
|
|
22
|
+
);
|
|
23
|
+
if (opts.json) {
|
|
24
|
+
console.log(JSON.stringify(list, null, 2));
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const count = (k: (e: TransitionEntry) => string) => [...list.reduce((m, e) => m.set(k(e), (m.get(k(e)) ?? 0) + 1), new Map<string, number>())].map(([k, n]) => `${k} ${n}`).join(" · ");
|
|
28
|
+
console.log(`${list.length}개 — ${count((e) => e.source)}`);
|
|
29
|
+
console.log(`시계 ${count((e) => e.clock)} · ${count((e) => USE[e.use])}\n`);
|
|
30
|
+
const w = Math.max(...list.map((e) => e.name.length), 4) + 2;
|
|
31
|
+
const sw = Math.max(...list.map((e) => e.source.length), 4) + 2;
|
|
32
|
+
const lw = Math.max(...list.map((e) => e.license.length), 4) + 2;
|
|
33
|
+
for (const e of list) {
|
|
34
|
+
const extra = [e.component && `<${e.component}>`, e.author, e.note].filter(Boolean).join(" — ");
|
|
35
|
+
console.log(`${e.name.padEnd(w)}${e.source.padEnd(sw)}${e.license.padEnd(lw)}${e.clock.padEnd(6)}${USE[e.use].padEnd(9)}${extra}`);
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export default cmd;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir, platform } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { hasKey, whereToPut } from "@kasaframe/gen";
|
|
8
|
+
import { jizuraCheck } from "@kasaframe/jizura/install";
|
|
9
|
+
import { launch } from "./browser.ts";
|
|
10
|
+
import { lyricsCheck } from "./commands/lyrics.ts";
|
|
11
|
+
|
|
12
|
+
// kf doctor·kf setup 이 같이 보는 바깥 도구들 — npm 이 못 까는 것(ffmpeg·파이썬 꾸러미·크롬 본체)만.
|
|
13
|
+
|
|
14
|
+
export const requirements = fileURLToPath(new URL("../../../tools/beats/requirements.txt", import.meta.url));
|
|
15
|
+
// 요즘 파이썬(Homebrew·데비안)은 시스템에 pip 설치를 막는다(PEP 668) — kf setup 은 쓰임마다 venv 를 따로 둔다
|
|
16
|
+
export const pyVenv = (name: string): string => join(homedir(), ".cache", "kasaframe", "py", name);
|
|
17
|
+
export const venvPython = (dir: string): string => join(dir, platform() === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
18
|
+
export const beatsVenv = pyVenv("beats");
|
|
19
|
+
|
|
20
|
+
export function python(): string {
|
|
21
|
+
return process.env.KF_PYTHON ?? (existsSync(venvPython(beatsVenv)) ? venvPython(beatsVenv) : "python3");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// npx playwright 는 다른 판의 playwright 를 집을 수 있다 — 크롬 판이 이 패키지가 쓰는 playwright 와 맞아야 뜬다
|
|
25
|
+
export const playwrightCli = (): string => join(dirname(createRequire(import.meta.url).resolve("playwright/package.json")), "cli.js");
|
|
26
|
+
|
|
27
|
+
export interface Check {
|
|
28
|
+
name: string;
|
|
29
|
+
ok: boolean;
|
|
30
|
+
/** 없으면 렌더가 안 된다 */
|
|
31
|
+
required: boolean;
|
|
32
|
+
detail: string;
|
|
33
|
+
fix?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const run = (cmd: string, args: string[]) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
37
|
+
|
|
38
|
+
function installHint(pkg: { brew: string; apt: string; url: string }): string {
|
|
39
|
+
if (platform() === "darwin") return `brew install ${pkg.brew}`;
|
|
40
|
+
if (platform() === "linux") return `sudo apt-get install -y ${pkg.apt} (데비안·우분투 — 다른 배포판은 그 패키지 관리자로)`;
|
|
41
|
+
return pkg.url;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function node(): Check {
|
|
45
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
46
|
+
const ok = major > 22 || (major === 22 && minor >= 15);
|
|
47
|
+
return { name: "Node 22.15 이상", ok, required: true, detail: process.version, fix: ok ? undefined : "https://nodejs.org 에서 22 LTS 이상" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ffmpeg(): Check {
|
|
51
|
+
const r = run("ffmpeg", ["-hide_banner", "-version"]);
|
|
52
|
+
const p = run("ffprobe", ["-hide_banner", "-version"]);
|
|
53
|
+
const ok = r.status === 0 && p.status === 0;
|
|
54
|
+
return {
|
|
55
|
+
name: "ffmpeg·ffprobe",
|
|
56
|
+
ok,
|
|
57
|
+
required: true,
|
|
58
|
+
detail: ok ? (/version (\S+)/.exec(r.stdout)?.[1] ?? "있음") : "없음 — MP4 로 굽기·검사에 쓴다",
|
|
59
|
+
fix: ok ? undefined : `${installHint({ brew: "ffmpeg", apt: "ffmpeg", url: "https://ffmpeg.org/download.html" })} — PATH 밖 정적 빌드면 KF_FFMPEG=<ffmpeg 경로> KF_FFPROBE=<ffprobe 경로>`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function chrome(): Promise<Check> {
|
|
64
|
+
try {
|
|
65
|
+
const b = await launch();
|
|
66
|
+
const v = b.version();
|
|
67
|
+
await b.close();
|
|
68
|
+
return { name: "크롬(Playwright)", ok: true, required: true, detail: v };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const first = String(err instanceof Error ? err.message : err).split("\n")[0];
|
|
71
|
+
return { name: "크롬(Playwright)", ok: false, required: true, detail: `못 띄웠다 — ${first.slice(0, 120)}`, fix: `kf setup chrome (같은 일: node ${playwrightCli()} install chromium${platform() === "linux" ? " · 리눅스는 sudo 로 install-deps chromium 도" : ""})` };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function librosa(): Check {
|
|
76
|
+
const py = python();
|
|
77
|
+
const r = run(py, ["-c", "import librosa; print(librosa.__version__)"]);
|
|
78
|
+
const ok = r.status === 0;
|
|
79
|
+
return {
|
|
80
|
+
name: "python3 + librosa",
|
|
81
|
+
ok,
|
|
82
|
+
required: false,
|
|
83
|
+
detail: ok ? `${py} · librosa ${r.stdout.trim()}` : `${py} 에 없다 — 곡에 박자를 맞출 때(kf beats·--music)만 쓴다`,
|
|
84
|
+
fix: ok ? undefined : `kf setup beats (같은 일: python3 -m venv ${beatsVenv} && ${venvPython(beatsVenv)} -m pip install -r ${requirements})`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ogKey(): Check {
|
|
89
|
+
const ok = hasKey("opengateway");
|
|
90
|
+
return {
|
|
91
|
+
name: "OG 키",
|
|
92
|
+
ok,
|
|
93
|
+
required: false,
|
|
94
|
+
detail: ok ? "있음 — 그림 생성(kf gen)·kf make OG 모드" : "없음 — 그림 생성(kf gen)과 kf make OG 모드만 못 쓴다. 장면·렌더·검사는 키 없이 돈다",
|
|
95
|
+
fix: ok ? undefined : whereToPut("opengateway"),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 글꼴은 CDN(jsdelivr)에서 받지만, 망이 막힌 리눅스 상자에서는 한글이 두부로 나온다
|
|
100
|
+
function koreanFont(): Check | null {
|
|
101
|
+
if (platform() !== "linux") return null;
|
|
102
|
+
const r = run("fc-list", [":lang=ko", "family"]);
|
|
103
|
+
const ok = r.status === 0 && r.stdout.trim().length > 0;
|
|
104
|
+
return { name: "한글 글꼴", ok, required: false, detail: ok ? r.stdout.trim().split("\n")[0] : "없음 — 글꼴 CDN 이 막히면 한글이 두부로 나온다", fix: ok ? undefined : "sudo apt-get install -y fonts-noto-cjk" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 점검을 더할 땐 여기 한 줄 — kf doctor 와 kf setup 끝 표가 같이 쓴다
|
|
108
|
+
export async function checks(): Promise<Check[]> {
|
|
109
|
+
return [node(), ffmpeg(), await chrome(), librosa(), jizuraCheck(), lyricsCheck(), ogKey(), koreanFont()].filter((c): c is Check => c !== null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 한글은 터미널에서 두 칸을 먹는다 — padEnd 로는 열이 어긋난다
|
|
113
|
+
export const cols = (s: string) => [...s].reduce((n, ch) => n + (/[\u1100-\u11ff\u3000-\u9fff\uac00-\ud7af]/.test(ch) ? 2 : 1), 0);
|
|
114
|
+
export const pad = (s: string, n: number) => s + " ".repeat(Math.max(0, n - cols(s)));
|
|
115
|
+
|
|
116
|
+
export function report(list: Check[]): void {
|
|
117
|
+
const w = Math.max(...list.map((c) => cols(c.name))) + 2;
|
|
118
|
+
for (const c of list) {
|
|
119
|
+
console.log(` ${pad(c.ok ? "ok" : c.required ? "없음" : "선택", 4)} ${pad(c.name, w)}${c.detail}`);
|
|
120
|
+
if (c.fix) console.log(` ${pad("", 4 + 2 + w)}→ ${c.fix}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
|
|
4
|
+
export type VideoFormat = "h264" | "prores";
|
|
5
|
+
|
|
6
|
+
export interface Encoder {
|
|
7
|
+
/** PNG, raw 로 만들었으면 RGBA 날것 */
|
|
8
|
+
write(frame: Buffer): Promise<void>;
|
|
9
|
+
end(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function run(args: string[], label: string): { proc: ChildProcess; done: Promise<void> } {
|
|
13
|
+
const proc = spawn("ffmpeg", ["-hide_banner", "-loglevel", "error", "-y", ...args], {
|
|
14
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
let err = "";
|
|
17
|
+
proc.stderr!.on("data", (d) => (err += d));
|
|
18
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
19
|
+
proc.on("error", (e) => reject(new Error(`${label}: ffmpeg 를 못 띄웠다 (${e.message})`)));
|
|
20
|
+
proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${label}: ffmpeg 종료 ${code}\n${err.trim()}`))));
|
|
21
|
+
});
|
|
22
|
+
return { proc, done };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// RGB→YUV 를 BT.709 로 바꾸고 그렇다고 적는다. 안 적으면 ffmpeg 는 BT.601 로 바꾸는데 브라우저·유튜브는 HD 를 709 로 풀어
|
|
26
|
+
// 색이 어긋난다(#20C040 이 (20,168,61) 로 — 초록이 눌린다)
|
|
27
|
+
const BT709 = ["-vf", "scale=out_color_matrix=bt709:out_range=tv,setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"];
|
|
28
|
+
|
|
29
|
+
const CODEC: Record<VideoFormat, string[]> = {
|
|
30
|
+
h264: ["-c:v", "libx264", "-preset", "medium", "-crf", "16", "-pix_fmt", "yuv420p", ...BT709, "-movflags", "+faststart"],
|
|
31
|
+
prores: ["-c:v", "prores_ks", "-profile:v", "3", "-pix_fmt", "yuv422p10le", ...BT709, "-vendor", "apl0"],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// PNG(또는 raw 를 주면 RGBA 날것 — 모션 블러가 평균한 프레임)를 stdin 으로 흘려 구간 파일 하나를 만든다. 쓰기가 밀리면 기다려 메모리가 차지 않게 한다.
|
|
35
|
+
export function encoder(out: string, fps: number, format: VideoFormat, crf?: number, raw?: { width: number; height: number }): Encoder {
|
|
36
|
+
const codec = [...CODEC[format]];
|
|
37
|
+
if (crf !== undefined && format === "h264") codec[codec.indexOf("-crf") + 1] = String(crf);
|
|
38
|
+
const input = raw
|
|
39
|
+
? ["-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${raw.width}x${raw.height}`, "-framerate", String(fps), "-i", "-"]
|
|
40
|
+
: ["-f", "image2pipe", "-framerate", String(fps), "-c:v", "png", "-i", "-"];
|
|
41
|
+
const { proc, done } = run([...input, ...codec, "-r", String(fps), out], out);
|
|
42
|
+
const stdin = proc.stdin!;
|
|
43
|
+
let failed: Error | null = null;
|
|
44
|
+
done.catch((e) => (failed = e));
|
|
45
|
+
return {
|
|
46
|
+
async write(frame) {
|
|
47
|
+
if (failed) throw failed;
|
|
48
|
+
if (!stdin.write(frame)) await Promise.race([once(stdin, "drain"), done]);
|
|
49
|
+
},
|
|
50
|
+
async end() {
|
|
51
|
+
stdin.end();
|
|
52
|
+
await done;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 구간들을 다시 인코딩하지 않고 잇고, 곡이 있으면 그 구간만큼 잘라 얹는다.
|
|
58
|
+
export async function concat(o: {
|
|
59
|
+
list: string;
|
|
60
|
+
out: string;
|
|
61
|
+
format: VideoFormat;
|
|
62
|
+
music?: string;
|
|
63
|
+
from: number;
|
|
64
|
+
dur: number;
|
|
65
|
+
}): Promise<void> {
|
|
66
|
+
const args = ["-f", "concat", "-safe", "0", "-i", o.list];
|
|
67
|
+
if (o.music) args.push("-ss", o.from.toFixed(6), "-i", o.music);
|
|
68
|
+
args.push("-map", "0:v:0");
|
|
69
|
+
if (o.music) {
|
|
70
|
+
args.push("-map", "1:a:0", ...(o.format === "prores" ? ["-c:a", "pcm_s16le"] : ["-c:a", "aac", "-b:a", "256k"]));
|
|
71
|
+
args.push("-af", `apad`, "-t", o.dur.toFixed(6));
|
|
72
|
+
}
|
|
73
|
+
args.push("-c:v", "copy");
|
|
74
|
+
// concat 이 구간 파일의 primaries·trc 태그를 떨군다 — 다시 인코딩 없이 비트스트림에 709 를 다시 적는다
|
|
75
|
+
args.push("-bsf:v", o.format === "h264" ? "h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1" : "prores_metadata=color_primaries=bt709:color_trc=bt709:colorspace=bt709");
|
|
76
|
+
args.push("-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709");
|
|
77
|
+
if (o.format === "h264") args.push("-movflags", "+faststart");
|
|
78
|
+
args.push(o.out);
|
|
79
|
+
const { done } = run(args, o.out);
|
|
80
|
+
await done;
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { Command, Flags } from "./command.ts";
|
|
2
|
+
export { parseFlags, num } from "./command.ts";
|
|
3
|
+
export { openProject, repoRoot } from "./serve.ts";
|
|
4
|
+
export type { OpenProject, OpenProjectOptions } from "./serve.ts";
|
|
5
|
+
export { openPage, openSession, launch, Tab } from "./browser.ts";
|
|
6
|
+
export type { OpenPage, PageInfo, PageMeta, Session, SessionOptions } from "./browser.ts";
|
|
7
|
+
export { blurOpts, captureBlurred, rgbaToPng, subTimes } from "./blur.ts";
|
|
8
|
+
export type { BlurOpts } from "./blur.ts";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fontsMissing } from "@kasaframe/jizura/install";
|
|
5
|
+
import type { Command } from "./command.ts";
|
|
6
|
+
|
|
7
|
+
const commandsDir = fileURLToPath(new URL("./commands/", import.meta.url));
|
|
8
|
+
|
|
9
|
+
function available(): string[] {
|
|
10
|
+
return readdirSync(commandsDir)
|
|
11
|
+
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"))
|
|
12
|
+
.map((f) => f.slice(0, -3))
|
|
13
|
+
.sort();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function load(name: string): Promise<Command> {
|
|
17
|
+
const mod = await import(new URL(`./commands/${name}.ts`, import.meta.url).href);
|
|
18
|
+
return mod.default as Command;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function help(): Promise<void> {
|
|
22
|
+
console.log("kf <명령> [인자]\n");
|
|
23
|
+
for (const name of available()) {
|
|
24
|
+
const cmd = await load(name).catch(() => null);
|
|
25
|
+
console.log(` ${name.padEnd(10)} ${cmd?.summary ?? ""}`);
|
|
26
|
+
}
|
|
27
|
+
console.log("\nkf <명령> --help 로 인자를 본다.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function main(argv: string[]): Promise<number> {
|
|
31
|
+
const [name, ...rest] = argv;
|
|
32
|
+
if (!name || name === "help" || name === "--help" || name === "-h") {
|
|
33
|
+
await help();
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (!available().includes(name)) {
|
|
37
|
+
console.error(`kf: 모르는 명령 "${name}". 있는 명령: ${available().join(" ")}`);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
const cmd = await load(name);
|
|
41
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
42
|
+
console.log(`kf ${name} — ${cmd.summary}\n\n${cmd.usage ?? ""}`.trimEnd());
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
return (await cmd.run(rest)) ?? 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 가사 층 글꼴을 처음 쓰다 못 받았으면(서버가 받는다 — jizura/vite.ts) 다른 글꼴로 구운 결과 대신 고를 길을 찍는다.
|
|
49
|
+
// 명령이 실패했으면 종료 3, 그 글꼴을 쓰는 프레임 없이 끝났으면 그대로
|
|
50
|
+
async function finish(code: number): Promise<number> {
|
|
51
|
+
const miss = fontsMissing();
|
|
52
|
+
if (!miss.length) return code;
|
|
53
|
+
const [{ fontsQuestion }, { questionsText }] = await Promise.all([import("./commands/lyrics.ts"), import("@kasaframe/gen")]);
|
|
54
|
+
console.error(`\n${code ? "" : "이번 구간은 그 글꼴 없이 끝났다 — 그 플랜이 나오는 구간을 구우려면 먼저 고른다\n"}${questionsText([fontsQuestion(miss)])}`);
|
|
55
|
+
return code ? 3 : code;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
main(process.argv.slice(2)).then(
|
|
59
|
+
async (code) => process.exit(await finish(code)),
|
|
60
|
+
async (err) => {
|
|
61
|
+
console.error(err instanceof Error ? (err.stack ?? err.message) : err);
|
|
62
|
+
process.exit(await finish(1));
|
|
63
|
+
},
|
|
64
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { installed, repoRoot } from "./serve.ts";
|
|
5
|
+
|
|
6
|
+
const LOCK = installed ? join(tmpdir(), "kasaframe-render-lock") : join(repoRoot, ".render-lock");
|
|
7
|
+
|
|
8
|
+
function alive(pid: number): boolean {
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function owner(): { pid: number; dir: string; started: string } | null {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(join(LOCK, "owner.json"), "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 최종 렌더는 레포 전체에서 하나씩 — mkdir 이 원자적이라 먼저 만든 쪽만 이긴다.
|
|
26
|
+
export async function acquireRenderLock(dir: string, waitSec: number): Promise<() => void> {
|
|
27
|
+
const until = Date.now() + waitSec * 1000;
|
|
28
|
+
let told = false;
|
|
29
|
+
for (;;) {
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(LOCK);
|
|
32
|
+
writeFileSync(join(LOCK, "owner.json"), JSON.stringify({ pid: process.pid, dir, started: new Date().toISOString() }));
|
|
33
|
+
let held = true;
|
|
34
|
+
const release = () => {
|
|
35
|
+
if (!held) return;
|
|
36
|
+
held = false;
|
|
37
|
+
if (owner()?.pid === process.pid) rmSync(LOCK, { recursive: true, force: true });
|
|
38
|
+
};
|
|
39
|
+
process.once("exit", release);
|
|
40
|
+
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
41
|
+
process.once(sig, () => {
|
|
42
|
+
release();
|
|
43
|
+
process.exit(130);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return release;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
49
|
+
}
|
|
50
|
+
const o = owner();
|
|
51
|
+
// 남이 mkdir 하고 owner.json 을 쓰기 전 찰나일 수 있으니, 주인 없는 잠금은 10초 지난 것만 치운다.
|
|
52
|
+
const age = (() => {
|
|
53
|
+
try {
|
|
54
|
+
return Date.now() - statSync(LOCK).mtimeMs;
|
|
55
|
+
} catch {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
if ((o && !alive(o.pid)) || (!o && age > 10_000)) {
|
|
60
|
+
rmSync(LOCK, { recursive: true, force: true });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (Date.now() > until) {
|
|
64
|
+
throw new Error(`렌더 잠금을 못 얻었다 — ${o ? `pid ${o.pid} 가 ${o.dir} 를 ${o.started} 부터 굽는 중` : LOCK}`);
|
|
65
|
+
}
|
|
66
|
+
if (!told) {
|
|
67
|
+
console.error(`렌더 잠금 기다리는 중${o ? ` (pid ${o.pid}: ${o.dir})` : ""} — 최대 ${waitSec}초`);
|
|
68
|
+
told = true;
|
|
69
|
+
}
|
|
70
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
71
|
+
}
|
|
72
|
+
}
|