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,280 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
import { clockScript } from "@kasaframe/clock";
|
|
4
|
+
import { openProject, repoRoot } from "@kasaframe/cli";
|
|
5
|
+
import { scanPage } from "./scan.ts";
|
|
6
|
+
import { serveStatic } from "./serve.ts";
|
|
7
|
+
import { probeVideo, videoText, videoThumbs } from "./video.ts";
|
|
8
|
+
import { badText, glyphProblems, offscreenText, textPolicy, type TextPolicy } from "./rules/text.ts";
|
|
9
|
+
import { captionOverlap } from "./rules/captions.ts";
|
|
10
|
+
import { genAssets } from "./rules/assets.ts";
|
|
11
|
+
import { blanks, cuts, freezes, spikes } from "./rules/frames.ts";
|
|
12
|
+
import { flashes } from "./rules/flash.ts";
|
|
13
|
+
import { fill, staticLayout, type SceneSpan } from "./rules/layout.ts";
|
|
14
|
+
import { cameraWander, heroEdge } from "./rules/camera.ts";
|
|
15
|
+
import { boundaries, motionRules } from "./rules/beats.ts";
|
|
16
|
+
import { contrast, pageErrors, repeatable } from "./rules/page.ts";
|
|
17
|
+
import type { Beats, Finding, MotionEvent, MotionRules, Report, SafeArea } from "./types.ts";
|
|
18
|
+
|
|
19
|
+
export interface CheckOptions {
|
|
20
|
+
target: string;
|
|
21
|
+
from?: number;
|
|
22
|
+
to?: number;
|
|
23
|
+
tabs?: number;
|
|
24
|
+
// 옛 페이지(window.seek 만 있는 곳)에서 meta 대신
|
|
25
|
+
fps?: number;
|
|
26
|
+
duration?: number;
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
query?: string;
|
|
30
|
+
thumbFps?: number;
|
|
31
|
+
shotFps?: number;
|
|
32
|
+
ocr?: boolean;
|
|
33
|
+
ocrFps?: number;
|
|
34
|
+
beats?: string;
|
|
35
|
+
style?: string;
|
|
36
|
+
log?: (s: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Project {
|
|
40
|
+
dir: string | null;
|
|
41
|
+
kf: any | null;
|
|
42
|
+
beats: Beats | null;
|
|
43
|
+
rules: MotionRules;
|
|
44
|
+
safe?: SafeArea;
|
|
45
|
+
text: TextPolicy;
|
|
46
|
+
allowText: string[];
|
|
47
|
+
scenes: number[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readJson(p: string): any | null {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// kf.json 이 있는 폴더가 프로젝트. mp4 는 보통 <proj>/out/ 에 있으니 위로 두 칸까지 찾는다.
|
|
59
|
+
function findProject(start: string): string | null {
|
|
60
|
+
let d = resolve(start);
|
|
61
|
+
for (let i = 0; i < 3; i++) {
|
|
62
|
+
if (existsSync(join(d, "kf.json"))) return d;
|
|
63
|
+
d = dirname(d);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadProject(dir: string | null, o: CheckOptions): Project {
|
|
69
|
+
const kf = dir ? readJson(join(dir, "kf.json")) : null;
|
|
70
|
+
const at = (p: string | undefined, fallback: string) => (dir ? join(dir, p ?? fallback) : null);
|
|
71
|
+
const beatsPath = o.beats ?? at(kf?.beats, "beats.json");
|
|
72
|
+
const stylePath = o.style ?? at(kf?.style, "style.json");
|
|
73
|
+
const beats = beatsPath ? readJson(beatsPath) : null;
|
|
74
|
+
const style = stylePath ? readJson(stylePath) : null;
|
|
75
|
+
// 틀의 motion 이 style.json 위에 덮인다(SPEC 계약 7).
|
|
76
|
+
const template = kf?.template ? readJson(join(repoRoot, "templates", kf.template, "template.json")) : null;
|
|
77
|
+
const rules: MotionRules = { ...(style?.motion ?? {}), ...(template?.motion ?? {}) };
|
|
78
|
+
const scenes: number[] = Array.isArray(kf?.scenes) ? kf.scenes.map((s: any) => Number(s.from)).filter((t: number) => Number.isFinite(t)) : [];
|
|
79
|
+
return {
|
|
80
|
+
dir,
|
|
81
|
+
kf,
|
|
82
|
+
beats: beats && Array.isArray(beats.beats) ? beats : null,
|
|
83
|
+
rules,
|
|
84
|
+
safe: kf?.safeArea,
|
|
85
|
+
text: { ...(template?.text ?? {}), ...(kf?.text ?? {}) },
|
|
86
|
+
allowText: kf?.check?.allowText ?? [],
|
|
87
|
+
scenes,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 화면 채움·장면 안 구도 변화는 장면 단위로 본다 — kf.json 장면이 없으면 영상 하나를 한 장면으로
|
|
92
|
+
function sceneSpans(kf: any): SceneSpan[] {
|
|
93
|
+
return Array.isArray(kf?.scenes) ? kf.scenes.filter((s: any) => Number.isFinite(Number(s.from))).map((s: any) => ({ id: String(s.id), from: Number(s.from), dur: Number(s.dur) || 0 })) : [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 이음매는 화면 전체를 옮기는 게 일이다 — 카메라·주인공 자리 판정에서 뺀다
|
|
97
|
+
function seamScenes(kf: any): string[] {
|
|
98
|
+
return Array.isArray(kf?.scenes) ? kf.scenes.filter((s: any) => s.role === "seam").map((s: any) => String(s.id)) : [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sortFindings(f: Finding[]): Finding[] {
|
|
102
|
+
return f.sort((a, b) => (a.severity === b.severity ? a.from - b.from : a.severity === "error" ? -1 : 1));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function finish(target: string, kind: Report["kind"], meta: Report["meta"], findings: Finding[], skipped: string[], sampled: Report["sampled"], t0: number): Report {
|
|
106
|
+
const counts = { error: findings.filter((f) => f.severity === "error").length, warn: findings.filter((f) => f.severity === "warn").length };
|
|
107
|
+
const round = (x: number) => Math.round(x * 1000) / 1000;
|
|
108
|
+
for (const f of findings) {
|
|
109
|
+
f.from = round(f.from);
|
|
110
|
+
f.to = round(f.to);
|
|
111
|
+
}
|
|
112
|
+
return { target, kind, ok: counts.error === 0, meta, sampled, counts, findings: sortFindings(findings), skipped, elapsedSec: round((Date.now() - t0) / 1000) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// kf.json 이 없어도 TS·TSX 모듈을 부르는 페이지는 묶어야 돈다 — 엔진의 Vite 길로 연다.
|
|
116
|
+
function needsVite(dir: string): boolean {
|
|
117
|
+
if (["vite.config.ts", "vite.config.js", "vite.config.mjs"].some((f) => existsSync(join(dir, f)))) return true;
|
|
118
|
+
const html = join(dir, "index.html");
|
|
119
|
+
return existsSync(html) && /<script[^>]+src="[^"]+\.tsx?"/.test(readFileSync(html, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const VIDEO = new Set([".mp4", ".mov", ".webm", ".mkv", ".m4v"]);
|
|
123
|
+
|
|
124
|
+
export async function check(o: CheckOptions): Promise<Report> {
|
|
125
|
+
const isUrl = /^https?:\/\//.test(o.target);
|
|
126
|
+
if (!isUrl && !existsSync(o.target)) throw new Error(`없는 경로: ${o.target}`);
|
|
127
|
+
if (!isUrl && statSync(o.target).isFile() && VIDEO.has(extname(o.target).toLowerCase())) return checkVideo(o);
|
|
128
|
+
return checkPage(o);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function checkPage(o: CheckOptions): Promise<Report> {
|
|
132
|
+
const t0 = Date.now();
|
|
133
|
+
const log = o.log ?? (() => {});
|
|
134
|
+
const isUrl = /^https?:\/\//.test(o.target);
|
|
135
|
+
const dir = isUrl ? null : resolve(o.target);
|
|
136
|
+
const projDir = dir ? findProject(dir) : null;
|
|
137
|
+
const proj = loadProject(projDir, o);
|
|
138
|
+
const skipped: string[] = [];
|
|
139
|
+
|
|
140
|
+
let url: string;
|
|
141
|
+
let close: () => Promise<void>;
|
|
142
|
+
let clock: string | undefined;
|
|
143
|
+
if (isUrl) {
|
|
144
|
+
url = o.target;
|
|
145
|
+
close = async () => {};
|
|
146
|
+
} else if (projDir || needsVite(dir!)) {
|
|
147
|
+
const p = await openProject(projDir ?? dir!, { hmr: false });
|
|
148
|
+
url = p.url;
|
|
149
|
+
close = p.close;
|
|
150
|
+
clock = await clockScript();
|
|
151
|
+
} else {
|
|
152
|
+
if (!existsSync(join(dir!, "index.html"))) throw new Error(`${dir} 에 index.html 도 kf.json 도 없다`);
|
|
153
|
+
const s = await serveStatic(dir!);
|
|
154
|
+
url = s.url + "index.html";
|
|
155
|
+
close = s.close;
|
|
156
|
+
}
|
|
157
|
+
if (o.query) url += (url.includes("?") ? "&" : "?") + o.query.replace(/^\?/, "");
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const r = await scanPage({
|
|
161
|
+
url,
|
|
162
|
+
width: o.width ?? proj.kf?.width ?? 1920,
|
|
163
|
+
height: o.height ?? proj.kf?.height ?? 1080,
|
|
164
|
+
fps: o.fps,
|
|
165
|
+
duration: o.duration,
|
|
166
|
+
from: o.from,
|
|
167
|
+
to: o.to,
|
|
168
|
+
// 팬 소리 줄이기 — 탭은 둘까지
|
|
169
|
+
tabs: Math.min(2, o.tabs ?? 2),
|
|
170
|
+
// 0.1초 멈춤을 잡으려면 0.1초보다 촘촘히 봐야 한다
|
|
171
|
+
thumbFps: o.thumbFps ?? 30,
|
|
172
|
+
shotFps: o.shotFps ?? 2,
|
|
173
|
+
clock,
|
|
174
|
+
log,
|
|
175
|
+
});
|
|
176
|
+
const { meta } = r;
|
|
177
|
+
if (r.legacy) skipped.push("페이지 계약(window.__kf) 대신 옛 window.seek(t) 로 훑었다");
|
|
178
|
+
const f: Finding[] = [];
|
|
179
|
+
f.push(...pageErrors(r.errors, meta.fps));
|
|
180
|
+
f.push(...badText(r.texts, { fps: meta.fps, allow: proj.allowText }));
|
|
181
|
+
f.push(...glyphProblems(r.texts, r.glyphs, meta.fps));
|
|
182
|
+
f.push(...offscreenText(r.texts, { width: meta.width, height: meta.height, fps: meta.fps, safe: proj.safe }));
|
|
183
|
+
f.push(...captionOverlap(r.texts, { fps: meta.fps }));
|
|
184
|
+
f.push(...contrast(r.contrast, { shotFps: o.shotFps ?? 2 }));
|
|
185
|
+
if (proj.text.minPx || proj.text.maxOnScreen) f.push(...textPolicy(r.texts, proj.text, { fps: meta.fps }));
|
|
186
|
+
if (projDir) f.push(...genAssets(projDir));
|
|
187
|
+
f.push(...freezes(r.thumbs, { errorSec: proj.rules.maxFreezeSec }));
|
|
188
|
+
f.push(...blanks(r.thumbs, meta.duration));
|
|
189
|
+
f.push(...spikes(r.thumbs, { exclude: [...sceneSpans(proj.kf).map((s) => s.from), ...(proj.beats?.drops ?? []), ...r.markers.filter((m) => m.kind === "scene" || m.kind === "drop").map((m) => m.t)] }));
|
|
190
|
+
f.push(...flashes(r.thumbs, { maxPerSec: proj.rules.maxFlashPerSec, cuts: [...sceneSpans(proj.kf).map((s) => s.from), ...cuts(r.thumbs)] }));
|
|
191
|
+
f.push(...fill(r.thumbs, sceneSpans(proj.kf), { width: meta.width, height: meta.height, safe: proj.safe, captionTop: captionTop(r.texts) }));
|
|
192
|
+
f.push(...staticLayout(r.thumbs, sceneSpans(proj.kf), { width: meta.width, height: meta.height, warnSec: proj.rules.staticSec?.warn, errorSec: proj.rules.staticSec?.error }));
|
|
193
|
+
const seams = seamScenes(proj.kf);
|
|
194
|
+
f.push(...cameraWander(r.cams, r.events, { fps: meta.fps, skipScenes: seams }));
|
|
195
|
+
f.push(...heroEdge(r.cams, r.texts, sceneSpans(proj.kf), { width: meta.width, height: meta.height, fps: meta.fps, safe: proj.safe, skipScenes: seams }));
|
|
196
|
+
if (r.repeat.length) f.push(...repeatable(r.repeat));
|
|
197
|
+
else skipped.push(r.mode === "step" ? "같은 프레임 두 번 찍기 — step 모드는 되감을 수 없다" : "같은 프레임 두 번 찍기 — 구간이 1초보다 짧다");
|
|
198
|
+
|
|
199
|
+
const sceneStarts = r.markers.filter((m) => m.kind === "scene").map((m) => m.t);
|
|
200
|
+
const starts = sceneStarts.length ? sceneStarts : proj.scenes;
|
|
201
|
+
if (proj.beats && starts.length) f.push(...boundaries(starts, proj.beats, { fps: meta.fps, duration: meta.duration, source: "scene" }));
|
|
202
|
+
else skipped.push(proj.beats ? "장면 경계 — markers·kf.json scenes 가 없다" : "장면 경계·박 — beats.json 이 없다");
|
|
203
|
+
motionPart(f, skipped, r.events, proj, meta);
|
|
204
|
+
|
|
205
|
+
return { ...finish(o.target, "page", meta, f, skipped, { frames: r.frames, textFrames: r.texts.length, thumbFps: r.thumbFps }, t0), mode: r.mode };
|
|
206
|
+
} finally {
|
|
207
|
+
await close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function motionPart(f: Finding[], skipped: string[], events: MotionEvent[] | null, proj: Project, meta: Report["meta"]): void {
|
|
212
|
+
if (!Object.keys(proj.rules).length) {
|
|
213
|
+
skipped.push("움직임 규칙 — style.json motion 이 없다");
|
|
214
|
+
} else if (!events) {
|
|
215
|
+
skipped.push("움직임 규칙 — 렌더 옆 .json(events)이 없다. 프로젝트 폴더로 kf check 하면 센다");
|
|
216
|
+
} else {
|
|
217
|
+
f.push(...motionRules(events, proj.beats, proj.rules, { fps: meta.fps, duration: meta.duration }));
|
|
218
|
+
if (!events.length) skipped.push("움직임 규칙 — window.__kf.events 가 비었다(움직임 부품을 안 썼거나 core emit 을 안 부른다)");
|
|
219
|
+
if (!proj.beats) skipped.push("움직임 규칙 중 드롭·프레이즈 기준 — beats.json 이 없다");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function checkVideo(o: CheckOptions): Promise<Report> {
|
|
224
|
+
const t0 = Date.now();
|
|
225
|
+
const log = o.log ?? (() => {});
|
|
226
|
+
const file = resolve(o.target);
|
|
227
|
+
const info = probeVideo(file);
|
|
228
|
+
const from = Math.max(0, o.from ?? 0);
|
|
229
|
+
const to = Math.min(info.duration, o.to ?? info.duration);
|
|
230
|
+
const proj = loadProject(findProject(dirname(file)), o);
|
|
231
|
+
// kf render 가 남긴 x.json — markers·events 가 들어 있어 페이지를 다시 안 열어도 된다.
|
|
232
|
+
const side = readJson(join(dirname(file), basename(file, extname(file)) + ".json"));
|
|
233
|
+
const skipped: string[] = [];
|
|
234
|
+
const f: Finding[] = [];
|
|
235
|
+
const meta = { fps: info.fps, width: info.width, height: info.height, duration: info.duration };
|
|
236
|
+
|
|
237
|
+
log(`프레임 읽기: ${from.toFixed(2)}–${to.toFixed(2)}초 · ${info.width}×${info.height} · ${info.fps.toFixed(2)}fps`);
|
|
238
|
+
const thumbs = await videoThumbs(file, info, from, to);
|
|
239
|
+
f.push(...freezes(thumbs, { errorSec: proj.rules.maxFreezeSec }));
|
|
240
|
+
f.push(...blanks(thumbs, info.duration));
|
|
241
|
+
f.push(...spikes(thumbs, { exclude: [...sceneSpans(proj.kf).map((s) => s.from), ...(proj.beats?.drops ?? [])] }));
|
|
242
|
+
f.push(...flashes(thumbs, { maxPerSec: proj.rules.maxFlashPerSec, cuts: [...sceneSpans(proj.kf).map((s) => s.from), ...cuts(thumbs)] }));
|
|
243
|
+
f.push(...fill(thumbs, sceneSpans(proj.kf), { width: info.width, height: info.height, safe: proj.safe }));
|
|
244
|
+
f.push(...staticLayout(thumbs, sceneSpans(proj.kf), { width: info.width, height: info.height, warnSec: proj.rules.staticSec?.warn, errorSec: proj.rules.staticSec?.error }));
|
|
245
|
+
|
|
246
|
+
let textFrames = 0;
|
|
247
|
+
if (o.ocr === false) skipped.push("화면 글 훑기 — --no-ocr");
|
|
248
|
+
else {
|
|
249
|
+
const ocrFps = o.ocrFps ?? 2;
|
|
250
|
+
log(`글 읽기(OCR ${ocrFps}fps)`);
|
|
251
|
+
const texts = await videoText(file, info, { fps: ocrFps, from, to });
|
|
252
|
+
if (!texts) skipped.push("화면 글 훑기 — OCR(macOS Vision·swiftc)이 없다. 프로젝트 폴더로 kf check 하면 글을 직접 본다");
|
|
253
|
+
else {
|
|
254
|
+
textFrames = texts.length;
|
|
255
|
+
f.push(...badText(texts, { fps: ocrFps, allow: proj.allowText, fuzzy: true }));
|
|
256
|
+
skipped.push(`mp4 글 훑기는 OCR ${ocrFps}fps — 두부 글자·화면 밖 글·대비·짧게 스친 글은 프로젝트 폴더 검사에서 본다`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const markers: { t: number; kind: string }[] = side?.markers ?? [];
|
|
261
|
+
const sceneStarts = markers.filter((m) => m.kind === "scene").map((m) => m.t);
|
|
262
|
+
const starts = sceneStarts.length ? sceneStarts : proj.scenes;
|
|
263
|
+
if (proj.beats) {
|
|
264
|
+
if (starts.length) f.push(...boundaries(starts, proj.beats, { fps: info.fps, duration: info.duration, source: "scene" }));
|
|
265
|
+
else f.push(...boundaries(cuts(thumbs), proj.beats, { fps: info.fps, duration: info.duration, source: "cut" }));
|
|
266
|
+
} else skipped.push("장면 경계·박 — beats.json 이 없다(--beats 로 줄 수 있다)");
|
|
267
|
+
motionPart(f, skipped, side?.events ?? null, proj, meta);
|
|
268
|
+
if (proj.dir) f.push(...genAssets(proj.dir));
|
|
269
|
+
skipped.push("같은 프레임 두 번 찍기 — mp4 로는 못 한다");
|
|
270
|
+
skipped.push("카메라 떠돌기·주인공 자리(camera.wander·layout.edge) — mp4 로는 못 본다. 프로젝트 폴더로 kf check 하면 카메라 층을 직접 잰다");
|
|
271
|
+
|
|
272
|
+
return finish(o.target, "video", meta, f, skipped, { frames: thumbs.length, textFrames, thumbFps: info.fps }, t0);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 화면 자막 판의 윗변 — 판 글 상자 위에 판 안쪽 여백(글자 크기의 ⅓)을 더한 자리 중 가장 위. 자막이 없으면 undefined
|
|
276
|
+
function captionTop(frames: { hits: { via: string; box?: { y0: number }; sizePx: number }[] }[]): number | undefined {
|
|
277
|
+
let top = Infinity;
|
|
278
|
+
for (const f of frames) for (const h of f.hits) if (h.via === "caption" && h.box) top = Math.min(top, h.box.y0 - h.sizePx * 0.3);
|
|
279
|
+
return Number.isFinite(top) ? top : undefined;
|
|
280
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
import { num, parseFlags } from "@kasaframe/cli";
|
|
4
|
+
import { check } from "./check.ts";
|
|
5
|
+
import { formatReport } from "./report.ts";
|
|
6
|
+
|
|
7
|
+
export const usage = `kf check <프로젝트 폴더 | 영상.mp4 | http 주소> [옵션]
|
|
8
|
+
|
|
9
|
+
폴더(kf.json 또는 index.html)는 페이지를 열어 프레임마다 훑는다 — 가장 정확하다.
|
|
10
|
+
mp4 는 프레임 차·OCR 로 본다. 옆에 kf render 가 남긴 같은 이름 .json 이 있으면 움직임 규칙도 센다.
|
|
11
|
+
|
|
12
|
+
--from <초> --to <초> 이 구간만
|
|
13
|
+
--json <경로> JSON 보고서 자리 (기본: 폴더/out/check.json · 영상 옆 <이름>.check.json)
|
|
14
|
+
--strict 경고도 실패로 친다
|
|
15
|
+
--tabs <n> 크롬 탭 수(기본 2, 최대 2)
|
|
16
|
+
--query <a=1> 페이지 주소 뒤에 붙일 질의(옛 페이지의 ?rec=1 따위)
|
|
17
|
+
--fps <n> --duration <초> --width <px> --height <px> window.__kf 가 없는 옛 페이지용
|
|
18
|
+
--beats <beats.json> --style <style.json> 프로젝트 밖 영상용
|
|
19
|
+
--ocr-fps <n> mp4 글 읽기 빈도(기본 2) · --no-ocr 로 끈다
|
|
20
|
+
|
|
21
|
+
종료 코드: 0 통과 · 1 오류 있음(--strict 면 경고도) · 2 검사를 못 돌림`;
|
|
22
|
+
|
|
23
|
+
function defaultJson(target: string): string | null {
|
|
24
|
+
if (/^https?:\/\//.test(target)) return null;
|
|
25
|
+
const p = resolve(target);
|
|
26
|
+
if (statSync(p).isDirectory()) return join(p, "out", "check.json");
|
|
27
|
+
return join(dirname(p), basename(p, extname(p)) + ".check.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function run(argv: string[]): Promise<number> {
|
|
31
|
+
const { args, opts } = parseFlags(argv);
|
|
32
|
+
const target = args[0];
|
|
33
|
+
if (!target) {
|
|
34
|
+
console.error(usage);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
const opt = (k: string) => (typeof opts[k] === "string" ? (opts[k] as string) : undefined);
|
|
38
|
+
const n = (k: string) => (opts[k] === undefined ? undefined : num(opts[k], NaN));
|
|
39
|
+
try {
|
|
40
|
+
if (!/^https?:\/\//.test(target) && !existsSync(target)) throw new Error(`없는 경로: ${target}`);
|
|
41
|
+
const report = await check({
|
|
42
|
+
target,
|
|
43
|
+
from: n("from"),
|
|
44
|
+
to: n("to"),
|
|
45
|
+
tabs: n("tabs"),
|
|
46
|
+
fps: n("fps"),
|
|
47
|
+
duration: n("duration"),
|
|
48
|
+
width: n("width"),
|
|
49
|
+
height: n("height"),
|
|
50
|
+
query: opt("query"),
|
|
51
|
+
ocr: opts["no-ocr"] ? false : undefined,
|
|
52
|
+
ocrFps: n("ocr-fps"),
|
|
53
|
+
beats: opt("beats"),
|
|
54
|
+
style: opt("style"),
|
|
55
|
+
log: (s) => console.error(s),
|
|
56
|
+
});
|
|
57
|
+
if (opts.strict && report.counts.warn) report.ok = false;
|
|
58
|
+
const jsonPath = opt("json") ?? defaultJson(target);
|
|
59
|
+
console.log(formatReport(report));
|
|
60
|
+
if (jsonPath) {
|
|
61
|
+
mkdirSync(dirname(jsonPath), { recursive: true });
|
|
62
|
+
writeFileSync(jsonPath, JSON.stringify(report, null, 2) + "\n");
|
|
63
|
+
console.log(`JSON: ${jsonPath}`);
|
|
64
|
+
}
|
|
65
|
+
return report.ok ? 0 : 1;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(`kf check: 검사를 못 돌렸다 — ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { PNG } from "pngjs";
|
|
2
|
+
import type { Box } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export interface Rgba {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
data: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Thumb {
|
|
11
|
+
t: number;
|
|
12
|
+
luma: Uint8Array;
|
|
13
|
+
mean: number;
|
|
14
|
+
std: number;
|
|
15
|
+
// FLASH_GRID×FLASH_GRID 칸마다 채도 높은 빨강 픽셀 몫 — 색을 받아 찍은 썸네일만(섬광 검사의 빨강 깜빡임)
|
|
16
|
+
red?: Float32Array;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function decodePng(buf: Buffer): Rgba {
|
|
20
|
+
const png = PNG.sync.read(buf);
|
|
21
|
+
return { width: png.width, height: png.height, data: png.data };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function luma(img: Rgba): Uint8Array {
|
|
25
|
+
const out = new Uint8Array(img.width * img.height);
|
|
26
|
+
for (let i = 0, j = 0; j < out.length; i += 4, j++) {
|
|
27
|
+
out[j] = (img.data[i] * 54 + img.data[i + 1] * 183 + img.data[i + 2] * 19) >> 8;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function stats(l: Uint8Array): { mean: number; std: number } {
|
|
33
|
+
let s = 0;
|
|
34
|
+
let s2 = 0;
|
|
35
|
+
for (const v of l) {
|
|
36
|
+
s += v;
|
|
37
|
+
s2 += v * v;
|
|
38
|
+
}
|
|
39
|
+
const mean = s / l.length;
|
|
40
|
+
return { mean, std: Math.sqrt(Math.max(0, s2 / l.length - mean * mean)) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function thumb(t: number, l: Uint8Array): Thumb {
|
|
44
|
+
return { t, luma: l, ...stats(l) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const FLASH_GRID = 8;
|
|
48
|
+
|
|
49
|
+
// ITU-R BT.1702·WCAG 의 「채도 높은 빨강」 — R/(R+G+B) ≥ 0.8 이고 (R−G−B)×320 > 20(0..1 눈금)
|
|
50
|
+
export function redGrid(img: Rgba): Float32Array {
|
|
51
|
+
const g = FLASH_GRID;
|
|
52
|
+
const hit = new Float32Array(g * g);
|
|
53
|
+
const cnt = new Float32Array(g * g);
|
|
54
|
+
for (let y = 0; y < img.height; y++) {
|
|
55
|
+
const r = Math.min(g - 1, Math.floor((y * g) / img.height));
|
|
56
|
+
for (let x = 0; x < img.width; x++) {
|
|
57
|
+
const k = r * g + Math.min(g - 1, Math.floor((x * g) / img.width));
|
|
58
|
+
const i = (y * img.width + x) * 4;
|
|
59
|
+
const R = img.data[i] / 255;
|
|
60
|
+
const G = img.data[i + 1] / 255;
|
|
61
|
+
const B = img.data[i + 2] / 255;
|
|
62
|
+
cnt[k]++;
|
|
63
|
+
if (R + G + B > 0 && R / (R + G + B) >= 0.8 && (R - G - B) * 320 > 20) hit[k]++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (let k = 0; k < hit.length; k++) hit[k] /= cnt[k] || 1;
|
|
67
|
+
return hit;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function colorThumb(t: number, img: Rgba): Thumb {
|
|
71
|
+
return { ...thumb(t, luma(img)), red: redGrid(img) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function meanAbsDiff(a: Uint8Array, b: Uint8Array): number {
|
|
75
|
+
if (a.length !== b.length) return 255;
|
|
76
|
+
let s = 0;
|
|
77
|
+
for (let i = 0; i < a.length; i++) s += Math.abs(a[i] - b[i]);
|
|
78
|
+
return s / a.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function channel(v: number): number {
|
|
82
|
+
const c = v / 255;
|
|
83
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function relLum(r: number, g: number, b: number): number {
|
|
87
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function contrastRatio(a: number, b: number): number {
|
|
91
|
+
const [hi, lo] = a > b ? [a, b] : [b, a];
|
|
92
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 글 상자 바로 바깥 테두리 띠의 가운데값을 바탕색으로 본다. 상자 안은 글자가 섞여 못 쓴다.
|
|
96
|
+
export function backgroundAround(img: Rgba, box: Box, ring = 4): [number, number, number] | null {
|
|
97
|
+
const x0 = Math.max(0, Math.floor(box.x0) - ring);
|
|
98
|
+
const y0 = Math.max(0, Math.floor(box.y0) - ring);
|
|
99
|
+
const x1 = Math.min(img.width - 1, Math.ceil(box.x1) + ring);
|
|
100
|
+
const y1 = Math.min(img.height - 1, Math.ceil(box.y1) + ring);
|
|
101
|
+
if (x1 <= x0 || y1 <= y0) return null;
|
|
102
|
+
const rs: number[] = [];
|
|
103
|
+
const gs: number[] = [];
|
|
104
|
+
const bs: number[] = [];
|
|
105
|
+
const push = (x: number, y: number) => {
|
|
106
|
+
const i = (y * img.width + x) * 4;
|
|
107
|
+
rs.push(img.data[i]);
|
|
108
|
+
gs.push(img.data[i + 1]);
|
|
109
|
+
bs.push(img.data[i + 2]);
|
|
110
|
+
};
|
|
111
|
+
const step = Math.max(1, Math.floor((x1 - x0 + y1 - y0) / 200));
|
|
112
|
+
for (let x = x0; x <= x1; x += step) {
|
|
113
|
+
push(x, y0);
|
|
114
|
+
push(x, y1);
|
|
115
|
+
}
|
|
116
|
+
for (let y = y0; y <= y1; y += step) {
|
|
117
|
+
push(x0, y);
|
|
118
|
+
push(x1, y);
|
|
119
|
+
}
|
|
120
|
+
const med = (a: number[]) => a.sort((p, q) => p - q)[a.length >> 1];
|
|
121
|
+
return [med(rs), med(gs), med(bs)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 칠한 색(fillStyle)이 아니라 실제로 찍힌 픽셀로 잰다 — 위에 다른 것이 덮였거나 반투명이면 그게 보이는 대비다.
|
|
125
|
+
// 상자 안에서 바탕과 가장 멀리 떨어진 픽셀들(상위 5%)을 글자 색으로 본다. 그것도 바탕과 같으면 글이 안 보이는 것(덮임)이라 null.
|
|
126
|
+
export function inkContrast(img: Rgba, box: Box): number | null {
|
|
127
|
+
const bg = backgroundAround(img, box);
|
|
128
|
+
if (!bg) return null;
|
|
129
|
+
const bgLum = relLum(bg[0], bg[1], bg[2]);
|
|
130
|
+
const x0 = Math.max(0, Math.floor(box.x0));
|
|
131
|
+
const y0 = Math.max(0, Math.floor(box.y0));
|
|
132
|
+
const x1 = Math.min(img.width, Math.ceil(box.x1));
|
|
133
|
+
const y1 = Math.min(img.height, Math.ceil(box.y1));
|
|
134
|
+
const lums: number[] = [];
|
|
135
|
+
for (let y = y0; y < y1; y++) {
|
|
136
|
+
for (let x = x0; x < x1; x++) {
|
|
137
|
+
const i = (y * img.width + x) * 4;
|
|
138
|
+
lums.push(relLum(img.data[i], img.data[i + 1], img.data[i + 2]));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (lums.length < 4) return null;
|
|
142
|
+
lums.sort((a, b) => Math.abs(b - bgLum) - Math.abs(a - bgLum));
|
|
143
|
+
const ink = lums[Math.floor(lums.length * 0.05)];
|
|
144
|
+
const ratio = contrastRatio(ink, bgLum);
|
|
145
|
+
return ratio < 1.15 ? null : ratio;
|
|
146
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { check } from "./check.ts";
|
|
2
|
+
export type { CheckOptions } from "./check.ts";
|
|
3
|
+
export { run, usage } from "./cli.ts";
|
|
4
|
+
export { formatReport } from "./report.ts";
|
|
5
|
+
export { scanPage } from "./scan.ts";
|
|
6
|
+
export { assembleLines, badText, glyphProblems, offscreenText } from "./rules/text.ts";
|
|
7
|
+
export { freezes, blanks, cuts } from "./rules/frames.ts";
|
|
8
|
+
export { boundaries, motionRules } from "./rules/beats.ts";
|
|
9
|
+
export type { Finding, Report, Severity, TextHit, FrameText, MotionEvent, MotionRules, Beats } from "./types.ts";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// mp4 는 글자 정보가 없으니 macOS Vision 으로 읽는다. 폴더의 png 를 이름 순으로 읽어 한 줄에 한 장씩 JSON 을 낸다.
|
|
2
|
+
// usesLanguageCorrection 을 끄는 이유: 켜면 「undefined」「NaN」 같은 낱말을 사전 낱말로 고쳐 버린다.
|
|
3
|
+
import AppKit
|
|
4
|
+
import Foundation
|
|
5
|
+
import Vision
|
|
6
|
+
|
|
7
|
+
let dir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "."
|
|
8
|
+
let files = ((try? FileManager.default.contentsOfDirectory(atPath: dir)) ?? [])
|
|
9
|
+
.filter { $0.hasSuffix(".png") }
|
|
10
|
+
.sorted()
|
|
11
|
+
|
|
12
|
+
for name in files {
|
|
13
|
+
let path = (dir as NSString).appendingPathComponent(name)
|
|
14
|
+
var items: [[String: Any]] = []
|
|
15
|
+
if let img = NSImage(contentsOfFile: path),
|
|
16
|
+
let cg = img.cgImage(forProposedRect: nil, context: nil, hints: nil)
|
|
17
|
+
{
|
|
18
|
+
let req = VNRecognizeTextRequest()
|
|
19
|
+
req.recognitionLevel = .accurate
|
|
20
|
+
req.recognitionLanguages = ["ko-KR", "en-US"]
|
|
21
|
+
req.usesLanguageCorrection = false
|
|
22
|
+
try? VNImageRequestHandler(cgImage: cg, options: [:]).perform([req])
|
|
23
|
+
for obs in req.results ?? [] {
|
|
24
|
+
guard let top = obs.topCandidates(1).first else { continue }
|
|
25
|
+
let b = obs.boundingBox
|
|
26
|
+
items.append([
|
|
27
|
+
"s": top.string, "c": top.confidence,
|
|
28
|
+
"x0": b.minX, "y0": 1 - b.maxY, "x1": b.maxX, "y1": 1 - b.minY,
|
|
29
|
+
])
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let obj: [String: Any] = ["file": name, "items": items]
|
|
33
|
+
if let data = try? JSONSerialization.data(withJSONObject: obj), let s = String(data: data, encoding: .utf8) {
|
|
34
|
+
print(s)
|
|
35
|
+
fflush(stdout)
|
|
36
|
+
}
|
|
37
|
+
}
|