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.
Files changed (442) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +148 -0
  3. package/README.md +92 -2
  4. package/app/package.json +12 -0
  5. package/app/server/engine.ts +533 -0
  6. package/app/server/main.ts +209 -0
  7. package/app/web/env.d.ts +2 -0
  8. package/app/web/icons.ts +31 -0
  9. package/app/web/index.html +21 -0
  10. package/app/web/main.ts +1195 -0
  11. package/app/web/style.css +463 -0
  12. package/bin/hooks.mjs +64 -0
  13. package/bin/kf.mjs +32 -0
  14. package/docs/AGENTS.md +286 -0
  15. package/music/README.md +37 -0
  16. package/package.json +44 -4
  17. package/packages/adapters/package.json +36 -0
  18. package/packages/adapters/src/css.ts +11 -0
  19. package/packages/adapters/src/gsap.ts +26 -0
  20. package/packages/adapters/src/index.ts +9 -0
  21. package/packages/adapters/src/lottie.ts +22 -0
  22. package/packages/adapters/src/media.ts +59 -0
  23. package/packages/adapters/src/page.ts +229 -0
  24. package/packages/adapters/src/project.ts +214 -0
  25. package/packages/adapters/src/react.ts +36 -0
  26. package/packages/adapters/src/svelte.ts +22 -0
  27. package/packages/adapters/src/three.ts +18 -0
  28. package/packages/adapters/src/vue.ts +27 -0
  29. package/packages/check/package.json +19 -0
  30. package/packages/check/src/check.ts +280 -0
  31. package/packages/check/src/cli.ts +70 -0
  32. package/packages/check/src/image.ts +146 -0
  33. package/packages/check/src/index.ts +9 -0
  34. package/packages/check/src/ocr.swift +37 -0
  35. package/packages/check/src/probe.ts +328 -0
  36. package/packages/check/src/report.ts +44 -0
  37. package/packages/check/src/rules/assets.ts +45 -0
  38. package/packages/check/src/rules/beats.ts +212 -0
  39. package/packages/check/src/rules/camera.ts +217 -0
  40. package/packages/check/src/rules/captions.ts +49 -0
  41. package/packages/check/src/rules/flash.ts +235 -0
  42. package/packages/check/src/rules/frames.ts +136 -0
  43. package/packages/check/src/rules/layout.ts +281 -0
  44. package/packages/check/src/rules/page.ts +87 -0
  45. package/packages/check/src/rules/runs.ts +20 -0
  46. package/packages/check/src/rules/text.ts +326 -0
  47. package/packages/check/src/scan.ts +240 -0
  48. package/packages/check/src/serve.ts +67 -0
  49. package/packages/check/src/types.ts +80 -0
  50. package/packages/check/src/video.ts +137 -0
  51. package/packages/cli/package.json +30 -0
  52. package/packages/cli/src/blur.ts +80 -0
  53. package/packages/cli/src/browser.ts +203 -0
  54. package/packages/cli/src/command.ts +44 -0
  55. package/packages/cli/src/commands/app.ts +25 -0
  56. package/packages/cli/src/commands/beats.ts +36 -0
  57. package/packages/cli/src/commands/bench.ts +123 -0
  58. package/packages/cli/src/commands/blender.ts +293 -0
  59. package/packages/cli/src/commands/check.ts +25 -0
  60. package/packages/cli/src/commands/direct.ts +210 -0
  61. package/packages/cli/src/commands/doctor.ts +30 -0
  62. package/packages/cli/src/commands/gen.ts +173 -0
  63. package/packages/cli/src/commands/look.ts +46 -0
  64. package/packages/cli/src/commands/lyrics.ts +157 -0
  65. package/packages/cli/src/commands/make.ts +1121 -0
  66. package/packages/cli/src/commands/music.ts +78 -0
  67. package/packages/cli/src/commands/narrate.ts +238 -0
  68. package/packages/cli/src/commands/new.ts +185 -0
  69. package/packages/cli/src/commands/preview.ts +43 -0
  70. package/packages/cli/src/commands/render.ts +250 -0
  71. package/packages/cli/src/commands/series.ts +110 -0
  72. package/packages/cli/src/commands/setup.ts +102 -0
  73. package/packages/cli/src/commands/sheet.ts +114 -0
  74. package/packages/cli/src/commands/skill.ts +43 -0
  75. package/packages/cli/src/commands/still.ts +40 -0
  76. package/packages/cli/src/commands/theme.ts +172 -0
  77. package/packages/cli/src/commands/transitions.ts +40 -0
  78. package/packages/cli/src/deps.ts +122 -0
  79. package/packages/cli/src/ffmpeg.ts +81 -0
  80. package/packages/cli/src/index.ts +8 -0
  81. package/packages/cli/src/kf.ts +64 -0
  82. package/packages/cli/src/lock.ts +72 -0
  83. package/packages/cli/src/mix.ts +131 -0
  84. package/packages/cli/src/out.ts +13 -0
  85. package/packages/cli/src/pool.ts +34 -0
  86. package/packages/cli/src/segments.ts +402 -0
  87. package/packages/cli/src/serve.ts +124 -0
  88. package/packages/clock/package.json +14 -0
  89. package/packages/clock/src/index.ts +25 -0
  90. package/packages/clock/src/install.ts +351 -0
  91. package/packages/clock/src/types.ts +45 -0
  92. package/packages/core/package.json +9 -0
  93. package/packages/core/src/beats.ts +283 -0
  94. package/packages/core/src/ease.ts +87 -0
  95. package/packages/core/src/frame.ts +99 -0
  96. package/packages/core/src/index.ts +27 -0
  97. package/packages/core/src/project.ts +157 -0
  98. package/packages/core/src/rand.ts +56 -0
  99. package/packages/core/src/spring.ts +52 -0
  100. package/packages/core/src/style.ts +100 -0
  101. package/packages/core/src/time.ts +88 -0
  102. package/packages/director/package.json +23 -0
  103. package/packages/director/src/assemble.ts +361 -0
  104. package/packages/director/src/brief.ts +122 -0
  105. package/packages/director/src/catalog.ts +49 -0
  106. package/packages/director/src/content.ts +343 -0
  107. package/packages/director/src/fallback.ts +58 -0
  108. package/packages/director/src/index.ts +13 -0
  109. package/packages/director/src/pattern.ts +90 -0
  110. package/packages/director/src/patterns/fill/cells.ts +115 -0
  111. package/packages/director/src/patterns/formula/stack.ts +93 -0
  112. package/packages/director/src/patterns/util.ts +58 -0
  113. package/packages/director/src/scene.ts +62 -0
  114. package/packages/director/src/select.ts +163 -0
  115. package/packages/director/src/transitions.ts +115 -0
  116. package/packages/gen/PROVIDERS.md +90 -0
  117. package/packages/gen/package.json +9 -0
  118. package/packages/gen/src/ask.ts +132 -0
  119. package/packages/gen/src/audio.ts +91 -0
  120. package/packages/gen/src/blender.ts +54 -0
  121. package/packages/gen/src/capabilities.ts +151 -0
  122. package/packages/gen/src/generate.ts +206 -0
  123. package/packages/gen/src/image/codex.ts +117 -0
  124. package/packages/gen/src/image/og.ts +133 -0
  125. package/packages/gen/src/index.ts +26 -0
  126. package/packages/gen/src/keys.ts +86 -0
  127. package/packages/gen/src/media.ts +150 -0
  128. package/packages/gen/src/music/elevenlabs.ts +41 -0
  129. package/packages/gen/src/music/index.ts +120 -0
  130. package/packages/gen/src/music/library.ts +73 -0
  131. package/packages/gen/src/music/lyria.ts +52 -0
  132. package/packages/gen/src/music/types.ts +28 -0
  133. package/packages/gen/src/plan.ts +106 -0
  134. package/packages/gen/src/registry.ts +54 -0
  135. package/packages/gen/src/seam.ts +103 -0
  136. package/packages/gen/src/tts/dashscope.ts +64 -0
  137. package/packages/gen/src/tts/elevenlabs.ts +84 -0
  138. package/packages/gen/src/tts/gemini.ts +73 -0
  139. package/packages/gen/src/tts/index.ts +241 -0
  140. package/packages/gen/src/tts/local.ts +42 -0
  141. package/packages/gen/src/tts/types.ts +44 -0
  142. package/packages/gen/src/types.ts +90 -0
  143. package/packages/gen/src/video/fal.ts +140 -0
  144. package/packages/gen/src/video/h3.ts +112 -0
  145. package/packages/gen/src/video/http.ts +44 -0
  146. package/packages/gen/src/video/ltx.ts +60 -0
  147. package/packages/gen/src/video/minimax.ts +55 -0
  148. package/packages/gen/src/video/veo.ts +71 -0
  149. package/packages/jizura/package.json +16 -0
  150. package/packages/jizura/src/deny.ts +73 -0
  151. package/packages/jizura/src/index.ts +7 -0
  152. package/packages/jizura/src/install.ts +269 -0
  153. package/packages/jizura/src/layer.ts +325 -0
  154. package/packages/jizura/src/lyrics.ts +81 -0
  155. package/packages/jizura/src/pin.ts +8 -0
  156. package/packages/jizura/src/vendor.ts +140 -0
  157. package/packages/jizura/src/vite.ts +63 -0
  158. package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
  159. package/packages/jizura/vendor/NOTICE +43 -0
  160. package/packages/jizura/vendor/engine.json +47 -0
  161. package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
  162. package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
  163. package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
  164. package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
  165. package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  166. package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
  167. package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
  168. package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
  169. package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
  170. package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
  171. package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
  172. package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
  173. package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
  174. package/packages/jizura/vendor/fonts/fonts.json +52 -0
  175. package/packages/jizura/vendor/jizura_engine.js +35024 -0
  176. package/packages/motion/README.md +153 -0
  177. package/packages/motion/package.json +30 -0
  178. package/packages/motion/src/camera.ts +97 -0
  179. package/packages/motion/src/captions.ts +487 -0
  180. package/packages/motion/src/enter.ts +128 -0
  181. package/packages/motion/src/hangul.ts +129 -0
  182. package/packages/motion/src/impact.ts +100 -0
  183. package/packages/motion/src/index.ts +17 -0
  184. package/packages/motion/src/moves/brushWipe.ts +103 -0
  185. package/packages/motion/src/moves/cloneArray.ts +111 -0
  186. package/packages/motion/src/moves/curtains.ts +81 -0
  187. package/packages/motion/src/moves/dress.ts +191 -0
  188. package/packages/motion/src/moves/echoStack.ts +70 -0
  189. package/packages/motion/src/moves/fit.ts +70 -0
  190. package/packages/motion/src/moves/flood.ts +61 -0
  191. package/packages/motion/src/moves/ghostStack.ts +88 -0
  192. package/packages/motion/src/moves/gridFloor.ts +100 -0
  193. package/packages/motion/src/moves/imageSwarm.ts +113 -0
  194. package/packages/motion/src/moves/index.ts +38 -0
  195. package/packages/motion/src/moves/irisBlades.ts +113 -0
  196. package/packages/motion/src/moves/karaokeLine.ts +108 -0
  197. package/packages/motion/src/moves/ledBoard.ts +94 -0
  198. package/packages/motion/src/moves/liquidTab.ts +72 -0
  199. package/packages/motion/src/moves/markerWrite.ts +63 -0
  200. package/packages/motion/src/moves/menu.ts +138 -0
  201. package/packages/motion/src/moves/morphBox.ts +96 -0
  202. package/packages/motion/src/moves/neonSign.ts +78 -0
  203. package/packages/motion/src/moves/panelPop.ts +78 -0
  204. package/packages/motion/src/moves/paperTear.ts +75 -0
  205. package/packages/motion/src/moves/path.ts +64 -0
  206. package/packages/motion/src/moves/pathText.ts +107 -0
  207. package/packages/motion/src/moves/pointSphere.ts +109 -0
  208. package/packages/motion/src/moves/ransomLetters.ts +73 -0
  209. package/packages/motion/src/moves/shapeMorph.ts +143 -0
  210. package/packages/motion/src/moves/slamWords.ts +96 -0
  211. package/packages/motion/src/moves/sparkTrail.ts +130 -0
  212. package/packages/motion/src/moves/speedLines.ts +91 -0
  213. package/packages/motion/src/moves/splitFlap.ts +149 -0
  214. package/packages/motion/src/moves/stepWords.ts +128 -0
  215. package/packages/motion/src/moves/strikeOut.ts +98 -0
  216. package/packages/motion/src/moves/sunburst.ts +58 -0
  217. package/packages/motion/src/moves/tickerRows.ts +91 -0
  218. package/packages/motion/src/moves/tokenPicker.ts +115 -0
  219. package/packages/motion/src/moves/truchet.ts +85 -0
  220. package/packages/motion/src/moves/wordWall.ts +78 -0
  221. package/packages/motion/src/moves/worldRoll.ts +32 -0
  222. package/packages/motion/src/particles.ts +102 -0
  223. package/packages/motion/src/react/index.tsx +21 -0
  224. package/packages/motion/src/react/parts.tsx +431 -0
  225. package/packages/motion/src/react/time.tsx +97 -0
  226. package/packages/motion/src/text.ts +103 -0
  227. package/packages/motion/src/transition.ts +82 -0
  228. package/packages/motion/src/transition3d/cards.ts +65 -0
  229. package/packages/motion/src/transition3d/chain.ts +84 -0
  230. package/packages/motion/src/transition3d/dive.ts +181 -0
  231. package/packages/motion/src/transition3d/fold.ts +110 -0
  232. package/packages/motion/src/transition3d/gl.ts +219 -0
  233. package/packages/motion/src/transition3d/index.ts +235 -0
  234. package/packages/motion/src/transition3d/live.ts +70 -0
  235. package/packages/motion/src/transition3d/portal.ts +115 -0
  236. package/packages/motion/src/transition3d/react.tsx +29 -0
  237. package/packages/motion/src/transition3d/rig.ts +600 -0
  238. package/packages/motion/src/transition3d/shatter.ts +104 -0
  239. package/packages/motion/src/transition3d/space.ts +173 -0
  240. package/packages/motion/src/transition3d/whip.ts +78 -0
  241. package/packages/motion/src/transition3d/world.ts +737 -0
  242. package/packages/motion/src/transition3d/zoomOut.ts +114 -0
  243. package/packages/motion/src/transitions2d/catalog.ts +127 -0
  244. package/packages/motion/src/transitions2d/css.d.ts +2 -0
  245. package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
  246. package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
  247. package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
  248. package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
  249. package/packages/motion/src/transitions2d/index.ts +2 -0
  250. package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
  251. package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
  252. package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
  253. package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
  254. package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
  255. package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
  256. package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
  257. package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
  258. package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
  259. package/packages/motion/src/transitions2d/react.ts +13 -0
  260. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
  261. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
  262. package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
  263. package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
  264. package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
  265. package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
  266. package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
  267. package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
  268. package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
  269. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
  270. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
  271. package/packages/motion/src/type.ts +131 -0
  272. package/packages/motion/src/ui.ts +172 -0
  273. package/packages/patterns/package.json +15 -0
  274. package/packages/patterns/src/bars/rank.ts +119 -0
  275. package/packages/patterns/src/before-after/shrink.ts +127 -0
  276. package/packages/patterns/src/before-after/strike.ts +125 -0
  277. package/packages/patterns/src/code/terminal.ts +133 -0
  278. package/packages/patterns/src/compare/bars.ts +142 -0
  279. package/packages/patterns/src/compare/duel.ts +103 -0
  280. package/packages/patterns/src/end/lockup.ts +85 -0
  281. package/packages/patterns/src/end/slam.ts +91 -0
  282. package/packages/patterns/src/flow/graph.ts +250 -0
  283. package/packages/patterns/src/hook-number/counter.ts +101 -0
  284. package/packages/patterns/src/hook-number/roll.ts +179 -0
  285. package/packages/patterns/src/hook-number/slam.ts +91 -0
  286. package/packages/patterns/src/index.ts +38 -0
  287. package/packages/patterns/src/kit.ts +449 -0
  288. package/packages/patterns/src/limit/gauge.ts +117 -0
  289. package/packages/patterns/src/list/cards.ts +131 -0
  290. package/packages/patterns/src/list/cycle.ts +22 -0
  291. package/packages/patterns/src/media/panels.ts +100 -0
  292. package/packages/patterns/src/objects.ts +234 -0
  293. package/packages/patterns/src/quote/karaoke.ts +126 -0
  294. package/packages/patterns/src/quote/slam.ts +22 -0
  295. package/packages/patterns/src/screen/focus.ts +85 -0
  296. package/packages/patterns/src/steps/path.ts +137 -0
  297. package/packages/patterns/src/steps/stack.ts +111 -0
  298. package/packages/patterns/src/term-reveal/chips.ts +92 -0
  299. package/packages/patterns/src/term-reveal/expand.ts +127 -0
  300. package/packages/patterns/src/timeline/axis.ts +144 -0
  301. package/packages/patterns/src/title/mask.ts +85 -0
  302. package/packages/patterns/src/title/slam.ts +23 -0
  303. package/packages/patterns/src/toggle-sim/switch.ts +92 -0
  304. package/packages/patterns/src/trend/columns.ts +108 -0
  305. package/packages/patterns/src/trend/draw.ts +140 -0
  306. package/packages/patterns/src/types.ts +67 -0
  307. package/packages/patterns/src/words/moves.ts +42 -0
  308. package/packages/patterns/src/words/slam.ts +4 -0
  309. package/packages/plan/package.json +15 -0
  310. package/packages/plan/src/asks.ts +256 -0
  311. package/packages/plan/src/brief.ts +383 -0
  312. package/packages/plan/src/captions.ts +78 -0
  313. package/packages/plan/src/detect.ts +93 -0
  314. package/packages/plan/src/index.ts +27 -0
  315. package/packages/plan/src/layout.ts +176 -0
  316. package/packages/plan/src/llm.ts +172 -0
  317. package/packages/plan/src/look.ts +222 -0
  318. package/packages/plan/src/plan.ts +179 -0
  319. package/packages/plan/src/request.ts +55 -0
  320. package/packages/plan/src/series.ts +76 -0
  321. package/packages/plan/src/studio.ts +53 -0
  322. package/packages/plan/src/templates.ts +42 -0
  323. package/packages/plan/src/themes.ts +48 -0
  324. package/packages/plan/src/types.ts +157 -0
  325. package/packages/studio/package.json +12 -0
  326. package/packages/studio/src/index.ts +1 -0
  327. package/packages/studio/src/plugin.ts +236 -0
  328. package/packages/studio/src/ui/main.ts +503 -0
  329. package/packages/studio/src/ui/studio.css +165 -0
  330. package/packages/studio/src/ui/timeline.ts +317 -0
  331. package/packages/themes/kit/THEME-MAKE.md +68 -0
  332. package/packages/themes/kit/THEME.md +30 -0
  333. package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
  334. package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
  335. package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
  336. package/packages/themes/kit/effects.ts +5 -0
  337. package/packages/themes/kit/sample/index.html +11 -0
  338. package/packages/themes/kit/sample/main.ts +9 -0
  339. package/packages/themes/kit/sample/scenes/scene.ts +15 -0
  340. package/packages/themes/package.json +19 -0
  341. package/packages/themes/src/factory.ts +296 -0
  342. package/packages/themes/src/fx.ts +14 -0
  343. package/packages/themes/src/index.ts +114 -0
  344. package/packages/themes/src/slotkit.ts +79 -0
  345. package/packages/themes/src/slots.ts +46 -0
  346. package/packages/themes/src/stage3d.ts +197 -0
  347. package/packages/themes/src/types.ts +58 -0
  348. package/presets/kawaii-pastel.json +94 -0
  349. package/presets/neutral-dark.json +82 -0
  350. package/presets/og.json +98 -0
  351. package/series/og-shorts.json +14 -0
  352. package/skills/kasaframe/SKILL.md +33 -0
  353. package/templates/_shared/project/index.html +11 -0
  354. package/templates/_shared/project/lib/react.ts +22 -0
  355. package/templates/_shared/project/lib/stage.ts +292 -0
  356. package/templates/_shared/project/main.ts +9 -0
  357. package/templates/_shared/scenes/default.ts +21 -0
  358. package/templates/_shared/scenes/drop.ts +33 -0
  359. package/templates/_shared/scenes/end.ts +26 -0
  360. package/templates/_shared/scenes/hook.ts +18 -0
  361. package/templates/_shared/scenes/seam.ts +41 -0
  362. package/templates/_shared/scenes/step.ts +49 -0
  363. package/templates/ad/preview.png +0 -0
  364. package/templates/ad/template.json +151 -0
  365. package/templates/demo/preview.png +0 -0
  366. package/templates/demo/template.json +141 -0
  367. package/templates/explainer/preview.png +0 -0
  368. package/templates/explainer/template.json +193 -0
  369. package/templates/launch/preview.png +0 -0
  370. package/templates/launch/template.json +163 -0
  371. package/templates/mv/preview.png +0 -0
  372. package/templates/mv/scenes/default.ts +98 -0
  373. package/templates/mv/template.json +178 -0
  374. package/templates/promo/preview.png +0 -0
  375. package/templates/promo/template.json +185 -0
  376. package/templates/shorts/preview.png +0 -0
  377. package/templates/shorts/scenes/hook.ts +37 -0
  378. package/templates/shorts/template.json +196 -0
  379. package/templates/square/preview.png +0 -0
  380. package/templates/square/template.json +133 -0
  381. package/themes/debug-hud/THEME.md +39 -0
  382. package/themes/debug-hud/effects.ts +389 -0
  383. package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
  384. package/themes/debug-hud/sample/beats.json +1 -0
  385. package/themes/debug-hud/sample/index.html +11 -0
  386. package/themes/debug-hud/sample/kf.json +59 -0
  387. package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
  388. package/themes/debug-hud/sample/lib/og.ts +29 -0
  389. package/themes/debug-hud/sample/main.ts +9 -0
  390. package/themes/debug-hud/sample/scenes/end.ts +62 -0
  391. package/themes/debug-hud/sample/scenes/gather.ts +67 -0
  392. package/themes/debug-hud/sample/scenes/models.ts +70 -0
  393. package/themes/debug-hud/sample/scenes/price.ts +79 -0
  394. package/themes/debug-hud/sample/style.json +91 -0
  395. package/themes/debug-hud/slots.ts +99 -0
  396. package/themes/debug-hud/still.png +0 -0
  397. package/themes/debug-hud/style.json +91 -0
  398. package/themes/debug-hud/theme.json +68 -0
  399. package/themes/og-measure/THEME.md +47 -0
  400. package/themes/og-measure/effects.ts +432 -0
  401. package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
  402. package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
  403. package/themes/og-measure/sample/assets/data/measure.json +353 -0
  404. package/themes/og-measure/sample/beats.json +1 -0
  405. package/themes/og-measure/sample/index.html +11 -0
  406. package/themes/og-measure/sample/kf.json +66 -0
  407. package/themes/og-measure/sample/lib/data.ts +62 -0
  408. package/themes/og-measure/sample/main.ts +9 -0
  409. package/themes/og-measure/sample/measure.ts +78 -0
  410. package/themes/og-measure/sample/scenes/end.ts +47 -0
  411. package/themes/og-measure/sample/scenes/gather.ts +51 -0
  412. package/themes/og-measure/sample/scenes/models.ts +34 -0
  413. package/themes/og-measure/sample/scenes/one.ts +39 -0
  414. package/themes/og-measure/sample/scenes/price.ts +56 -0
  415. package/themes/og-measure/sample/style.json +97 -0
  416. package/themes/og-measure/slots.ts +88 -0
  417. package/themes/og-measure/still.png +0 -0
  418. package/themes/og-measure/style.json +97 -0
  419. package/themes/og-measure/theme.json +79 -0
  420. package/themes/riso-zine/THEME.md +43 -0
  421. package/themes/riso-zine/effects.ts +321 -0
  422. package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
  423. package/themes/riso-zine/sample/beats.json +1 -0
  424. package/themes/riso-zine/sample/index.html +11 -0
  425. package/themes/riso-zine/sample/kf.json +59 -0
  426. package/themes/riso-zine/sample/lib/og.ts +29 -0
  427. package/themes/riso-zine/sample/lib/shot.ts +86 -0
  428. package/themes/riso-zine/sample/lib/zine.ts +42 -0
  429. package/themes/riso-zine/sample/main.ts +9 -0
  430. package/themes/riso-zine/sample/scenes/end.ts +61 -0
  431. package/themes/riso-zine/sample/scenes/gather.ts +62 -0
  432. package/themes/riso-zine/sample/scenes/models.ts +52 -0
  433. package/themes/riso-zine/sample/scenes/price.ts +82 -0
  434. package/themes/riso-zine/sample/style.json +99 -0
  435. package/themes/riso-zine/slots.ts +81 -0
  436. package/themes/riso-zine/still.png +0 -0
  437. package/themes/riso-zine/style.json +99 -0
  438. package/themes/riso-zine/theme.json +75 -0
  439. package/tools/beats/beats.py +372 -0
  440. package/tools/beats/requirements.txt +3 -0
  441. package/tools/lyrics/align.py +164 -0
  442. package/tools/lyrics/requirements.txt +9 -0
@@ -0,0 +1,217 @@
1
+ import type { Box, Finding, FrameText, MotionEvent, SafeArea } from "../types.ts";
2
+ import type { SceneSpan } from "./layout.ts";
3
+
4
+ // 화면 전체를 이유 없이 흔들면(사인 드리프트·박마다 자세·늘 다가가는 카메라) 주인공이 기울고 가장자리로 밀린다.
5
+ // 픽셀만 봐서는 「화면 안 움직임」과 「카메라가 움직여 생긴 변화」가 안 갈린다 — 그래서 페이지에서 카메라 층의 transform 을 직접 잰다.
6
+
7
+ export interface CamLayer {
8
+ key: string;
9
+ scene: string | null;
10
+ sel: string;
11
+ a: number;
12
+ b: number;
13
+ e: number;
14
+ f: number;
15
+ flat: boolean;
16
+ }
17
+
18
+ export interface CamSample {
19
+ t: number;
20
+ layers: CamLayer[];
21
+ heroes: { scene: string | null; sel: string; box: Box }[];
22
+ }
23
+
24
+ const r1 = (x: number) => Math.round(x * 10) / 10;
25
+
26
+ function sceneOf(t: number, scenes: SceneSpan[]): string | undefined {
27
+ let hit: string | undefined;
28
+ for (const s of scenes) if (s.from <= t + 1e-3) hit = s.id;
29
+ return hit;
30
+ }
31
+
32
+ export interface WanderOptions {
33
+ fps: number;
34
+ // 이음매처럼 화면 전체가 움직이는 게 일인 장면
35
+ skipScenes?: string[];
36
+ // 이보다 짧게 이어진 이동은 등장 움직임으로 본다
37
+ minRunSec?: number;
38
+ // 장면 안에서 이유 없이 움직인 시간을 다 합쳐 이만큼 넘으면 경고
39
+ warnSec?: number;
40
+ }
41
+
42
+ // 이웃 표본 사이에 카메라 층이 움직였는데 그 시각에 카메라 이벤트(__kf.events 의 camera·shake)가 없으면 「이유 없는 이동」이다
43
+ export function cameraWander(samples: CamSample[], events: MotionEvent[], o: WanderOptions): Finding[] {
44
+ const minRun = o.minRunSec ?? 0.3;
45
+ const warnSec = o.warnSec ?? 0.8;
46
+ const skip = new Set(o.skipScenes ?? []);
47
+ const reasons = events.filter((e) => e.kind === "camera" || e.kind === "shake");
48
+ const pad = 1.5 / o.fps;
49
+ const excused = (scene: string | null, a: number, b: number) => reasons.some((e) => (!e.scene || !scene || e.scene === scene) && e.t >= a - pad && e.t <= b + pad);
50
+
51
+ const tracks = new Map<string, { t: number; l: CamLayer }[]>();
52
+ for (const s of samples) for (const l of s.layers) (tracks.get(l.key) ?? tracks.set(l.key, []).get(l.key)!).push({ t: s.t, l });
53
+ const step = samples.length > 1 ? Math.min(...samples.slice(1).map((s, i) => s.t - samples[i].t).filter((d) => d > 0)) : 1 / 30;
54
+
55
+ interface Acc {
56
+ runs: { from: number; to: number }[];
57
+ shift: number;
58
+ rot: number;
59
+ sel: Set<string>;
60
+ tilt: boolean;
61
+ }
62
+ const byScene = new Map<string, Acc>();
63
+ for (const track of tracks.values()) {
64
+ track.sort((x, y) => x.t - y.t);
65
+ let run: { from: number; to: number } | null = null;
66
+ const scene = track[0].l.scene ?? "";
67
+ if (skip.has(scene)) continue;
68
+ const acc = byScene.get(scene) ?? { runs: [], shift: 0, rot: 0, sel: new Set<string>(), tilt: false };
69
+ const close = () => {
70
+ if (run && run.to - run.from >= minRun - 1e-6) acc.runs.push(run);
71
+ run = null;
72
+ };
73
+ for (let i = 1; i < track.length; i++) {
74
+ const p = track[i - 1];
75
+ const q = track[i];
76
+ if (q.t - p.t > step * 2.5) {
77
+ close();
78
+ continue;
79
+ }
80
+ const sp = Math.hypot(p.l.a, p.l.b);
81
+ const sq = Math.hypot(q.l.a, q.l.b);
82
+ const rp = (Math.atan2(p.l.b, p.l.a) * 180) / Math.PI;
83
+ const rq = (Math.atan2(q.l.b, q.l.a) * 180) / Math.PI;
84
+ const moved = Math.abs(q.l.e - p.l.e) > 0.5 || Math.abs(q.l.f - p.l.f) > 0.5 || Math.abs(sq - sp) / (sp || 1) > 0.001 || Math.abs(rq - rp) > 0.02 || p.l.flat !== q.l.flat;
85
+ if (!moved || excused(q.l.scene, p.t, q.t)) {
86
+ close();
87
+ continue;
88
+ }
89
+ if (run) run.to = q.t;
90
+ else run = { from: p.t, to: q.t };
91
+ acc.shift = Math.max(acc.shift, Math.hypot(q.l.e - track[0].l.e, q.l.f - track[0].l.f));
92
+ acc.rot = Math.max(acc.rot, Math.abs(rq));
93
+ acc.tilt ||= !q.l.flat;
94
+ acc.sel.add(q.l.sel);
95
+ }
96
+ close();
97
+ byScene.set(scene, acc);
98
+ }
99
+
100
+ const out: Finding[] = [];
101
+ for (const [scene, acc] of byScene) {
102
+ const total = acc.runs.reduce((a, r) => a + (r.to - r.from), 0);
103
+ if (total < warnSec - 1e-6) continue;
104
+ const longest = Math.max(...acc.runs.map((r) => r.to - r.from));
105
+ const turn = acc.rot > 0.05 || acc.tilt ? ` · 기울기 ${acc.tilt ? "원근(3D)" : `${r1(acc.rot)}°`}` : "";
106
+ out.push({
107
+ rule: "camera.wander",
108
+ severity: "warn",
109
+ from: Math.min(...acc.runs.map((r) => r.from)),
110
+ to: Math.max(...acc.runs.map((r) => r.to)),
111
+ message: `장면 ${scene || "?"} 의 카메라 층이 이유 없이 ${total.toFixed(1)}초 움직인다(가장 길게 ${longest.toFixed(1)}초, 최대 ${Math.round(acc.shift)}px${turn}) — 카메라는 가만히 두고, 드러냄·비교·따라가기·충격 때만 stage.camera(pushIn·pullOut·follow·impact)로. 쉬지 않는 몫은 화면 안 움직임에`,
112
+ detail: { scene, seconds: r1(total), longest: r1(longest), maxShiftPx: Math.round(acc.shift), maxRotateDeg: r1(acc.rot), tilt3d: acc.tilt, layers: [...acc.sel].slice(0, 3), runs: acc.runs.slice(0, 8).map((r) => [r1(r.from), r1(r.to)]) },
113
+ });
114
+ }
115
+ return out.sort((a, b) => a.from - b.from);
116
+ }
117
+
118
+ export interface EdgeOptions {
119
+ width: number;
120
+ height: number;
121
+ fps: number;
122
+ safe?: SafeArea;
123
+ // 안전 영역 가장자리에서 이 몫 안쪽 띠
124
+ band?: number;
125
+ minSec?: number;
126
+ skipScenes?: string[];
127
+ // 장면이 주인공을 짚지 않았을 때 「가장 큰 글」로 칠 최소 글자 크기(1080 기준 px)
128
+ heroPx?: number;
129
+ }
130
+
131
+ // 주인공 상자가 안전 영역 가장자리 4% 띠 안으로 들어가거나 넘어간 채 0.5초 넘게 — 카메라가 밀었거나 처음부터 구석에 놓였다.
132
+ // 장면이 짚은 주인공(data-kf-hero)은 늘 본다. 짚지 않아 「가장 큰 글」로 어림할 때는 그 장면 카메라 층이 처음 자리(배율 1·이동 0)를
133
+ // 벗어난 프레임만 센다 — HUD 구석 숫자·지면 밖으로 넘치는 큰 글처럼 일부러 가장자리에 둔 판을 카메라 탓으로 잡지 않게
134
+ export function heroEdge(samples: CamSample[], texts: FrameText[], scenes: SceneSpan[], o: EdgeOptions): Finding[] {
135
+ const sa = o.safe ?? { top: 0, bottom: 0, left: 0, right: 0 };
136
+ const band = o.band ?? 0.04;
137
+ const minSec = o.minSec ?? 0.5;
138
+ const sw = o.width - sa.left - sa.right;
139
+ const sh = o.height - sa.top - sa.bottom;
140
+ const inner = { x0: sa.left + sw * band, y0: sa.top + sh * band, x1: o.width - sa.right - sw * band, y1: o.height - sa.bottom - sh * band };
141
+ const heroPx = (o.heroPx ?? 72) * (Math.min(o.width, o.height) / 1080);
142
+ const skip = new Set(o.skipScenes ?? []);
143
+
144
+ // 프레임마다 주인공 상자 — 짚은 것이 있으면 그것, 없으면 가장 큰 글(자막 판은 빼고)
145
+ const marked = new Map<number, { sel: string; box: Box }[]>();
146
+ for (const s of samples) if (s.heroes.length) marked.set(Math.round(s.t * o.fps), s.heroes);
147
+ const away = (l: CamLayer) => Math.abs(Math.hypot(l.a, l.b) - 1) > 0.002 || Math.abs(l.b) > 0.0005 || Math.abs(l.e) > 1 || Math.abs(l.f) > 1 || !l.flat;
148
+ const moved = samples.map((s) => ({ t: s.t, scenes: new Set(s.layers.filter(away).map((l) => l.scene ?? "")) }));
149
+ let mi = 0;
150
+ const camAway = (t: number, scene: string) => {
151
+ while (mi + 1 < moved.length && Math.abs(moved[mi + 1].t - t) <= Math.abs(moved[mi].t - t)) mi++;
152
+ const m = moved[mi];
153
+ return !!m && Math.abs(m.t - t) <= 2.5 / o.fps && m.scenes.has(scene);
154
+ };
155
+ const frames: { t: number; what: string; box: Box }[] = [];
156
+ const seen = new Set<number>();
157
+ for (const f of texts) {
158
+ const k = Math.round(f.t * o.fps);
159
+ seen.add(k);
160
+ const hs = marked.get(k);
161
+ if (hs) {
162
+ for (const h of hs) frames.push({ t: f.t, what: h.sel, box: h.box });
163
+ continue;
164
+ }
165
+ let best: FrameText["hits"][number] | undefined;
166
+ for (const h of f.hits) {
167
+ if (!h.box || h.via === "caption" || h.alpha < 0.6 || h.sizePx < heroPx || !/[\p{L}\p{N}]/u.test(h.text)) continue;
168
+ if (!best || h.sizePx > best.sizePx) best = h;
169
+ }
170
+ if (best?.box && camAway(f.t, sceneOf(f.t, scenes) ?? "")) frames.push({ t: f.t, what: `「${best.text.slice(0, 24)}」`, box: best.box });
171
+ }
172
+ for (const [k, hs] of marked) if (!seen.has(k)) for (const h of hs) frames.push({ t: k / o.fps, what: h.sel, box: h.box });
173
+ frames.sort((a, b) => a.t - b.t);
174
+
175
+ const sides = (b: Box) => [b.x0 < inner.x0 && "왼쪽", b.x1 > inner.x1 && "오른쪽", b.y0 < inner.y0 && "위", b.y1 > inner.y1 && "아래"].filter(Boolean) as string[];
176
+ const byScene = new Map<string, { runs: { from: number; to: number }[]; what: Set<string>; side: Set<string>; worst: number }>();
177
+ let run: { from: number; to: number; scene: string } | null = null;
178
+ const close = () => {
179
+ if (run && run.to - run.from >= minSec - 1e-6) byScene.get(run.scene)!.runs.push({ from: run.from, to: run.to });
180
+ run = null;
181
+ };
182
+ const at = new Map<number, typeof frames>();
183
+ for (const f of frames) (at.get(f.t) ?? at.set(f.t, []).get(f.t)!).push(f);
184
+ for (const [t, fs] of [...at].sort((a, b) => a[0] - b[0])) {
185
+ const scene = sceneOf(t, scenes) ?? "";
186
+ const hit = skip.has(scene) ? [] : fs.map((f) => ({ f, s: sides(f.box) })).filter((x) => x.s.length);
187
+ if (!hit.length) {
188
+ close();
189
+ continue;
190
+ }
191
+ const acc = byScene.get(scene) ?? byScene.set(scene, { runs: [], what: new Set(), side: new Set(), worst: 0 }).get(scene)!;
192
+ for (const { f, s } of hit) {
193
+ acc.what.add(f.what);
194
+ s.forEach((x) => acc.side.add(x));
195
+ acc.worst = Math.max(acc.worst, inner.x0 - f.box.x0, f.box.x1 - inner.x1, inner.y0 - f.box.y0, f.box.y1 - inner.y1);
196
+ }
197
+ if (run && (run.scene !== scene || t - run.to > 2.5 / o.fps)) close();
198
+ if (run) run.to = t;
199
+ else run = { from: t, to: t, scene };
200
+ }
201
+ close();
202
+
203
+ const out: Finding[] = [];
204
+ for (const [scene, acc] of byScene) {
205
+ if (!acc.runs.length) continue;
206
+ const total = acc.runs.reduce((a, r) => a + (r.to - r.from), 0);
207
+ out.push({
208
+ rule: "layout.edge",
209
+ severity: "warn",
210
+ from: acc.runs[0].from,
211
+ to: acc.runs[acc.runs.length - 1].to,
212
+ message: `장면 ${scene || "?"} 의 주인공 ${[...acc.what].slice(0, 2).join(" · ")} 이 안전 영역 ${[...acc.side].join("·")} 가장자리 ${Math.round(band * 100)}% 띠에 ${total.toFixed(1)}초 붙어 있다(가장 깊이 ${Math.round(acc.worst)}px) — 주인공은 안전 영역 가운데에. 카메라가 밀었으면 그 이동을 걷는다`,
213
+ detail: { scene, seconds: r1(total), sides: [...acc.side], worstPx: Math.round(acc.worst), heroes: [...acc.what].slice(0, 3), runs: acc.runs.slice(0, 8).map((r) => [r1(r.from), r1(r.to)]) },
214
+ });
215
+ }
216
+ return out.sort((a, b) => a.from - b.from);
217
+ }
@@ -0,0 +1,49 @@
1
+ import type { Box, Finding, FrameText } from "../types.ts";
2
+ import { runs } from "./runs.ts";
3
+ import { assembleLines, roundBox } from "./text.ts";
4
+
5
+ // 화면 자막 판(엔진이 장면 위에 얹은 data-kf-captions)이 장면 글을 덮는 곳. 엔진이 장면을 자막 띠 위로 올리지만,
6
+ // 장면이 일부러 띠 자리까지 내려오게 짰거나(안전 영역 밖 배치) 띠보다 큰 움직임이면 여전히 겹친다
7
+ export function captionOverlap(frames: FrameText[], o: { fps: number; minSec?: number }): Finding[] {
8
+ const minSec = o.minSec ?? 0.3;
9
+ const seen = new Map<string, { times: number[]; box: Box; caption: string }>();
10
+ for (const f of frames) {
11
+ const lines = assembleLines(f.hits).filter((l) => l.box && l.alpha > 0.3);
12
+ const caps = lines.filter((l) => l.hits[0]?.via === "caption");
13
+ if (!caps.length) continue;
14
+ // 글 상자에 판의 안쪽 여백(글자 크기의 절반·⅓)을 더하면 판 자리다
15
+ const size = Math.max(...caps.map((l) => l.sizePx));
16
+ const plate: Box = {
17
+ x0: Math.min(...caps.map((l) => l.box!.x0)) - size * 0.5,
18
+ y0: Math.min(...caps.map((l) => l.box!.y0)) - size * 0.3,
19
+ x1: Math.max(...caps.map((l) => l.box!.x1)) + size * 0.5,
20
+ y1: Math.max(...caps.map((l) => l.box!.y1)) + size * 0.3,
21
+ };
22
+ const caption = caps.map((l) => l.text).join(" ");
23
+ for (const l of lines) {
24
+ if (l.hits[0]?.via === "caption" || !/[\p{L}\p{N}]/u.test(l.text)) continue;
25
+ const b = l.box!;
26
+ const w = Math.min(b.x1, plate.x1) - Math.max(b.x0, plate.x0);
27
+ const h = Math.min(b.y1, plate.y1) - Math.max(b.y0, plate.y0);
28
+ if (w <= 2 || h <= 2) continue;
29
+ let s = seen.get(l.text);
30
+ if (!s) seen.set(l.text, (s = { times: [], box: b, caption }));
31
+ s.times.push(f.t);
32
+ }
33
+ }
34
+ const out: Finding[] = [];
35
+ for (const [text, s] of seen) {
36
+ for (const r of runs(s.times, 1.5 / o.fps)) {
37
+ if (r.to - r.from + 1 / o.fps < minSec) continue;
38
+ out.push({
39
+ rule: "caption.overlap",
40
+ severity: "warn",
41
+ from: r.from,
42
+ to: r.to + 1 / o.fps,
43
+ message: `자막 판이 장면 글 "${text.slice(0, 40)}" 을 덮는다 — 그 글을 안전 영역(ctx.safe) 안 위쪽으로 옮긴다. 자막 띠 자리(안전 영역 아래쪽)는 비워 둔다`,
44
+ detail: { text: text.slice(0, 120), box: roundBox(s.box), caption: s.caption.slice(0, 120) },
45
+ });
46
+ }
47
+ }
48
+ return out;
49
+ }
@@ -0,0 +1,235 @@
1
+ import { FLASH_GRID, type Thumb } from "../image.ts";
2
+ import type { Finding } from "../types.ts";
3
+
4
+ // 빛에 민감한 사람을 위한 섬광 안전(ITU-R BT.1702·WCAG 2.3.1 취지). 박마다 번쩍이는 드롭·컷 연출이 흔해 기계로 센다.
5
+ // 섬광 1회 = 화면 25% 넘는 영역의 평균 상대휘도가 10%p 넘게 올랐다가(또는 내렸다가) 되돌아오는 한 쌍(어두운 쪽이 0.8 아래일 때).
6
+
7
+ export interface FlashOptions {
8
+ // 아무 1초 창에 이보다 많으면 오류(style.json motion.maxFlashPerSec, 기본 3)
9
+ maxPerSec?: number;
10
+ // 장면 경계·드롭처럼 일부러 끊는 시각 — 컷 휘도 차 판정에 쓴다
11
+ cuts?: number[];
12
+ }
13
+
14
+ const G = FLASH_GRID;
15
+ const AREA = 0.25;
16
+ const SWING = 0.1;
17
+
18
+ // 감마 밝기(0..255) → 상대휘도 근사(0..1). 썸네일은 회색조라 채널별 선형화 대신 밝기 하나로 어림한다
19
+ const lin = new Float32Array(256).map((_, v) => {
20
+ const c = v / 255;
21
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
22
+ });
23
+
24
+ function tiles(t: Thumb): Float32Array {
25
+ const w = 160;
26
+ const h = Math.round(t.luma.length / w);
27
+ const sum = new Float32Array(G * G);
28
+ const cnt = new Float32Array(G * G);
29
+ for (let y = 0; y < h; y++) {
30
+ const r = Math.min(G - 1, Math.floor((y * G) / h));
31
+ for (let x = 0; x < w; x++) {
32
+ const k = r * G + Math.min(G - 1, Math.floor((x * G) / w));
33
+ sum[k] += lin[t.luma[y * w + x]];
34
+ cnt[k]++;
35
+ }
36
+ }
37
+ for (let k = 0; k < sum.length; k++) sum[k] /= cnt[k] || 1;
38
+ return sum;
39
+ }
40
+
41
+ interface Turn {
42
+ t: number;
43
+ sign: 1 | -1;
44
+ }
45
+
46
+ // 칸 하나의 시계열에서 오르내림 — 지난 꺾임 뒤의 가장 낮은(높은) 값에서 swing 넘게 반대로 가면 한 번 꺾인 것
47
+ function turns(series: number[], times: number[], swing: number, dark: (lo: number) => boolean): Turn[] {
48
+ const out: Turn[] = [];
49
+ let lo = series[0];
50
+ let hi = series[0];
51
+ let state = 0;
52
+ for (let i = 1; i < series.length; i++) {
53
+ const v = series[i];
54
+ if (state !== 1 && v - lo >= swing && dark(lo)) {
55
+ out.push({ t: times[i], sign: 1 });
56
+ state = 1;
57
+ hi = v;
58
+ continue;
59
+ }
60
+ if (state !== -1 && hi - v >= swing && dark(v)) {
61
+ out.push({ t: times[i], sign: -1 });
62
+ state = -1;
63
+ lo = v;
64
+ continue;
65
+ }
66
+ if (state >= 0) hi = Math.max(hi, v);
67
+ if (state <= 0) lo = Math.min(lo, v);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ // 칸마다 꺾인 시각을 모아, 같은 쪽으로 동시에(이웃 두 표본 안) 꺾인 칸이 화면 25% 를 넘으면 화면 전체가 한 번 꺾인 것
73
+ function frameTurns(perTile: Turn[][], step: number): Turn[] {
74
+ const all = perTile.flatMap((ts, k) => ts.map((x) => ({ ...x, k }))).sort((a, b) => a.t - b.t);
75
+ const out: Turn[] = [];
76
+ const need = Math.ceil(AREA * G * G);
77
+ for (let i = 0; i < all.length; i++) {
78
+ const a = all[i];
79
+ if (out.length && out[out.length - 1].sign === a.sign && a.t - out[out.length - 1].t <= step * 2.5) continue;
80
+ const tiles = new Set<number>();
81
+ for (let j = i; j < all.length && all[j].t - a.t <= step * 2.5; j++) if (all[j].sign === a.sign) tiles.add(all[j].k);
82
+ if (tiles.size >= need) out.push({ t: a.t, sign: a.sign });
83
+ }
84
+ return out;
85
+ }
86
+
87
+ // 반대로 꺾인 이웃 둘 = 섬광 1회 — 아무 1초 창 안의 꺾임 n 번이면 섬광 ⌊n/2⌋ 번. max 를 넘는 구간을 묶어 돌려준다
88
+ function overLimit(ts: Turn[], max: number): { from: number; to: number; count: number }[] {
89
+ const out: { from: number; to: number; count: number }[] = [];
90
+ for (let i = 0; i < ts.length; i++) {
91
+ let j = i;
92
+ while (j + 1 < ts.length && ts[j + 1].t - ts[i].t < 1) j++;
93
+ const count = Math.floor((j - i + 1) / 2);
94
+ if (count <= max) continue;
95
+ const last = out[out.length - 1];
96
+ if (last && ts[i].t <= last.to) {
97
+ last.to = Math.max(last.to, ts[j].t);
98
+ last.count = Math.max(last.count, count);
99
+ } else out.push({ from: ts[i].t, to: ts[j].t, count });
100
+ }
101
+ return out;
102
+ }
103
+
104
+ export function flashes(thumbs: Thumb[], o: FlashOptions = {}): Finding[] {
105
+ if (thumbs.length < 3) return [];
106
+ const max = o.maxPerSec ?? 3;
107
+ const step = thumbs[1].t - thumbs[0].t;
108
+ const times = thumbs.map((t) => t.t);
109
+ const lum = thumbs.map(tiles);
110
+ const out: Finding[] = [];
111
+ const r2 = (x: number) => Math.round(x * 100) / 100;
112
+
113
+ // 1) 휘도 섬광 — 어두운 쪽이 0.8 아래인 오르내림만
114
+ const lt = Array.from({ length: G * G }, (_, k) =>
115
+ turns(
116
+ lum.map((l) => l[k]),
117
+ times,
118
+ SWING,
119
+ (lo) => lo < 0.8,
120
+ ),
121
+ );
122
+ for (const r of overLimit(frameTurns(lt, step), max)) {
123
+ out.push({
124
+ rule: "frame.flash",
125
+ severity: "error",
126
+ from: r.from,
127
+ to: r.to,
128
+ message: `화면 25% 넘는 영역이 1초에 ${r.count}번 번쩍인다 — ${max}번까지(빛에 민감한 사람이 발작할 수 있다). 박마다 섬광이면 두 박·한 마디에 한 번으로, 섬광은 2마디 안에서 끝낸다`,
129
+ detail: { flashesPerSec: r.count, max },
130
+ });
131
+ }
132
+
133
+ // 2) 채도 높은 빨강 깜빡임 — 화면 25% 넘게 빨강이 켜졌다 꺼지기를 1초에 두 번 넘게
134
+ if (thumbs.every((t) => t.red)) {
135
+ const rt = Array.from({ length: G * G }, (_, k) =>
136
+ turns(
137
+ thumbs.map((t) => t.red![k]),
138
+ times,
139
+ 0.5,
140
+ () => true,
141
+ ),
142
+ );
143
+ for (const r of overLimit(frameTurns(rt, step), 1)) {
144
+ out.push({
145
+ rule: "frame.flash-red",
146
+ severity: "error",
147
+ from: r.from,
148
+ to: r.to,
149
+ message: `채도 높은 빨강이 화면 25% 넘게 1초에 ${r.count}번 깜빡인다 — 빨강 전체 깜빡임은 쓰지 않는다. 빨강은 화면 10% 아래 작은 요소로`,
150
+ detail: { flashesPerSec: r.count },
151
+ });
152
+ }
153
+ }
154
+
155
+ // 3) 빠른 줄무늬·나선 — 센 가장자리가 빽빽하고 대비가 큰 칸이 다음 표본에서 크게 바뀐다. 화면 25% 넘게 0.3초 넘으면 경고
156
+ const hot = thumbs.map((t, i) => (i ? stripeShare(thumbs[i - 1], t) : 0));
157
+ let start = -1;
158
+ for (let i = 0; i <= hot.length; i++) {
159
+ const on = i < hot.length && hot[i] > AREA;
160
+ if (on && start < 0) start = i;
161
+ if (!on && start >= 0) {
162
+ const from = times[start];
163
+ const to = times[i - 1];
164
+ if (to - from >= 0.3 - 1e-6) {
165
+ out.push({
166
+ rule: "frame.flash-pattern",
167
+ severity: "warn",
168
+ from,
169
+ to,
170
+ message: `대비 큰 줄무늬·나선이 빠르게 움직이며 화면 ${Math.round(Math.max(...hot.slice(start, i)) * 100)}% 를 덮는다(${(to - from).toFixed(1)}초) — 25% 아래로 줄이거나 느리게`,
171
+ detail: { seconds: r2(to - from) },
172
+ });
173
+ }
174
+ start = -1;
175
+ }
176
+ }
177
+
178
+ // 4) 컷 휘도 차 — 이어지는 컷 앞뒤 화면 평균 상대휘도가 50%p 넘게 뛰면 경고(어둠 ↔ 흰 화면을 박마다 오가는 컷)
179
+ const mean = lum.map((l) => l.reduce((a, b) => a + b, 0) / l.length);
180
+ const near = (t: number) => times.findIndex((x) => x >= t - 1e-3);
181
+ const seen = new Set<number>();
182
+ for (const c of o.cuts ?? []) {
183
+ const i = near(c);
184
+ if (i <= 0 || i >= thumbs.length || seen.has(i)) continue;
185
+ seen.add(i);
186
+ const jump = Math.abs(mean[i] - mean[i - 1]);
187
+ if (jump <= 0.5) continue;
188
+ out.push({
189
+ rule: "frame.flash-cut",
190
+ severity: "warn",
191
+ from: times[i],
192
+ to: times[i],
193
+ message: `${times[i].toFixed(2)}초 컷 앞뒤 화면 밝기(상대휘도)가 ${Math.round(jump * 100)}%p 뛴다 — 50% 아래로(어둠과 흰 화면을 이어 붙이지 않는다)`,
194
+ detail: { jump: r2(jump), before: r2(mean[i - 1]), after: r2(mean[i]) },
195
+ });
196
+ }
197
+ return out;
198
+ }
199
+
200
+ // 칸마다: 센 가장자리 몫 ≥ 0.3 이고 밝기 표준편차 ≥ 50(대비 큰 줄무늬) 이면서 앞 표본과 평균 절대차 ≥ 25(빠르게 움직임) — 그런 칸의 화면 몫
201
+ function stripeShare(a: Thumb, b: Thumb): number {
202
+ const w = 160;
203
+ const h = Math.round(b.luma.length / w);
204
+ if (a.luma.length !== b.luma.length) return 0;
205
+ let n = 0;
206
+ for (let ty = 0; ty < G; ty++) {
207
+ for (let tx = 0; tx < G; tx++) {
208
+ const y0 = Math.floor((ty * h) / G);
209
+ const y1 = Math.floor(((ty + 1) * h) / G);
210
+ const x0 = Math.floor((tx * w) / G);
211
+ const x1 = Math.floor(((tx + 1) * w) / G);
212
+ let edges = 0;
213
+ let px = 0;
214
+ let s = 0;
215
+ let s2 = 0;
216
+ let d = 0;
217
+ for (let y = y0; y < y1; y++) {
218
+ for (let x = x0; x < x1; x++) {
219
+ const i = y * w + x;
220
+ const v = b.luma[i];
221
+ px++;
222
+ s += v;
223
+ s2 += v * v;
224
+ d += Math.abs(v - a.luma[i]);
225
+ if (x + 1 < x1 && y + 1 < y1 && Math.abs(v - b.luma[i + 1]) + Math.abs(v - b.luma[i + w]) > 48) edges++;
226
+ }
227
+ }
228
+ if (!px) continue;
229
+ const m = s / px;
230
+ const std = Math.sqrt(Math.max(0, s2 / px - m * m));
231
+ if (edges / px >= 0.3 && std >= 50 && d / px >= 25) n++;
232
+ }
233
+ }
234
+ return n / (G * G);
235
+ }
@@ -0,0 +1,136 @@
1
+ import { meanAbsDiff, type Thumb } from "../image.ts";
2
+ import type { Finding } from "../types.ts";
3
+
4
+ export interface FreezeOptions {
5
+ // 160×90 회색조 평균 절대차(0..255). 커서 깜빡임 한 번이면 넘는 정도.
6
+ threshold?: number;
7
+ // 틀·style.json 의 motion.maxFreezeSec(틀이 이긴다) — 고정 관찰 화면(설명·뮤비)은 틀에서 늘려 허용한다. 없으면 0.1초
8
+ errorSec?: number;
9
+ warnSec?: number;
10
+ }
11
+
12
+ // 바뀐 면적(픽셀 몫) — 「거의 멈춤」을 숫자로 말해 준다.
13
+ function changedArea(a: Uint8Array, b: Uint8Array): number {
14
+ let n = 0;
15
+ for (let i = 0; i < a.length; i++) if (Math.abs(a[i] - b[i]) > 8) n++;
16
+ return n / a.length;
17
+ }
18
+
19
+ export function diffs(thumbs: Thumb[]): number[] {
20
+ const out = [0];
21
+ for (let i = 1; i < thumbs.length; i++) out.push(meanAbsDiff(thumbs[i - 1].luma, thumbs[i].luma));
22
+ return out;
23
+ }
24
+
25
+ // 이웃 프레임끼리 비교하면 60fps 의 느린 움직임이 멈춤으로 보인다. 멈춤이 시작된 프레임(닻)과 비교해
26
+ // 닻에서 얼마나 멀어졌는지로 잰다 — 10fps 로 찍든 60fps 로 읽든 같은 답이 나온다.
27
+ // 공통 규칙: 단 0.1초도 쉬지 않는다 — 끝 카드·큰 숫자·로고도 예외 없이 0.1초(60fps 6프레임) 넘게 멈추면 오류
28
+ export const MAX_FREEZE_SEC = 0.1;
29
+
30
+ export function freezes(thumbs: Thumb[], o: FreezeOptions = {}): Finding[] {
31
+ const thr = o.threshold ?? 0.35;
32
+ const errorSec = o.errorSec ?? MAX_FREEZE_SEC;
33
+ const warnSec = Math.min(o.warnSec ?? errorSec, errorSec);
34
+ const out: Finding[] = [];
35
+ let a = 0;
36
+ while (a < thumbs.length - 1) {
37
+ let j = a + 1;
38
+ while (j < thumbs.length && meanAbsDiff(thumbs[a].luma, thumbs[j].luma) < thr) j++;
39
+ const end = j - 1;
40
+ const len = thumbs[end].t - thumbs[a].t;
41
+ // 「이보다 오래」 멈출 때만 — 딱 경계 길이는 재는 간격 안의 오차다.
42
+ if (len > warnSec + 1e-6) {
43
+ const severity = len > errorSec + 1e-6 ? "error" : "warn";
44
+ const area = changedArea(thumbs[a].luma, thumbs[end].luma);
45
+ out.push({
46
+ rule: "frame.freeze",
47
+ severity,
48
+ from: thumbs[a].t,
49
+ to: thumbs[end].t,
50
+ message: `화면이 ${len.toFixed(2)}초 ${area > 0 ? `거의 멈췄다(바뀐 면적 ${(area * 100).toFixed(1)}%)` : "멈췄다"}${severity === "warn" ? ` — 경고 기준 ${warnSec}초 넘음` : ` — 기준 ${errorSec}초(틀·style.json maxFreezeSec)`} — 그 사이에 눈에 띄는 움직임 하나를 둔다`,
51
+ detail: { seconds: Math.round(len * 100) / 100, changedArea: Math.round(area * 10000) / 10000 },
52
+ });
53
+ }
54
+ a = Math.max(end, a + 1);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ // 한 색으로 칠해진 화면. 드롭 섬광처럼 한두 프레임은 일부러 그럴 수 있어 길이로 거른다.
60
+ export function blanks(thumbs: Thumb[], duration: number, o: { std?: number; minSec?: number } = {}): Finding[] {
61
+ const stdMax = o.std ?? 2.5;
62
+ const minSec = o.minSec ?? 0.5;
63
+ const out: Finding[] = [];
64
+ let start = -1;
65
+ const step = thumbs.length > 1 ? thumbs[1].t - thumbs[0].t : 0;
66
+ const close = (end: number) => {
67
+ if (start < 0) return;
68
+ const from = thumbs[start].t;
69
+ const to = thumbs[end].t + step;
70
+ const len = to - from;
71
+ const edge = from <= 0.001 || to >= duration - step - 0.001;
72
+ if (len >= minSec) {
73
+ const severity = edge && len <= 1.0 ? "warn" : "error";
74
+ const mean = thumbs[start].mean;
75
+ out.push({
76
+ rule: "frame.blank",
77
+ severity,
78
+ from,
79
+ to,
80
+ message: `${len.toFixed(2)}초 동안 화면이 한 색(밝기 ${Math.round(mean)})뿐이다${edge ? " — 시작·끝이면 짧게 줄인다" : " — 장면이 비었거나 그리기가 실패했다"}`,
81
+ detail: { seconds: Math.round(len * 100) / 100, mean: Math.round(mean) },
82
+ });
83
+ }
84
+ start = -1;
85
+ };
86
+ for (let i = 0; i < thumbs.length; i++) {
87
+ if (thumbs[i].std < stdMax) {
88
+ if (start < 0) start = i;
89
+ } else close(i - 1);
90
+ }
91
+ close(thumbs.length - 1);
92
+ return out;
93
+ }
94
+
95
+ // 이웃 프레임 차가 갑자기 튀는 곳 = 컷. 앞뒤 평소 움직임보다 훨씬 커야 한다.
96
+ export function cuts(thumbs: Thumb[], o: { min?: number; ratio?: number } = {}): number[] {
97
+ const min = o.min ?? 22;
98
+ const ratio = o.ratio ?? 4;
99
+ const d = diffs(thumbs);
100
+ const out: number[] = [];
101
+ for (let i = 1; i < d.length; i++) {
102
+ const around = [d[i - 2], d[i - 1], d[i + 1], d[i + 2]].filter((x) => x !== undefined) as number[];
103
+ const base = around.length ? around.sort((a, b) => a - b)[around.length >> 1] : 0;
104
+ if (d[i] >= min && d[i] >= ratio * (base + 0.5)) out.push(thumbs[i].t);
105
+ }
106
+ return out;
107
+ }
108
+
109
+ // 한 프레임 튐 — 앞뒤 프레임끼리는 비슷한데 가운데 한 프레임만 둘 다와 크게 다르면 깜빡임·한 프레임 오류다.
110
+ // 일부러 하는 컷·섬광은 장면 경계·드롭 시각 근처면 뺀다
111
+ export function spikes(thumbs: Thumb[], o: { exclude?: number[]; ratio?: number; floor?: number } = {}): Finding[] {
112
+ const ratio = o.ratio ?? 3;
113
+ const floor = o.floor ?? 2.5;
114
+ const step = thumbs.length > 1 ? thumbs[1].t - thumbs[0].t : 0;
115
+ const near = (t: number) => (o.exclude ?? []).some((x) => Math.abs(x - t) <= step * 1.5 + 1e-3);
116
+ const out: Finding[] = [];
117
+ for (let i = 1; i < thumbs.length - 1; i++) {
118
+ const a = thumbs[i - 1].luma;
119
+ const b = thumbs[i].luma;
120
+ const c = thumbs[i + 1].luma;
121
+ const jump = Math.min(meanAbsDiff(a, b), meanAbsDiff(b, c));
122
+ if (jump < floor) continue;
123
+ const around = meanAbsDiff(a, c);
124
+ if (jump <= ratio * Math.max(around, 0.5)) continue;
125
+ if (near(thumbs[i].t)) continue;
126
+ out.push({
127
+ rule: "frame.spike",
128
+ severity: "warn",
129
+ from: thumbs[i].t,
130
+ to: thumbs[i].t + step,
131
+ message: `${thumbs[i].t.toFixed(2)}초 한 프레임만 앞뒤와 크게 다르다(앞뒤 차 ${jump.toFixed(1)} · 건너 차 ${around.toFixed(1)}) — 깜빡임이거나 한 프레임 오류. 일부러 한 섬광이면 드롭·장면 경계에 붙인다`,
132
+ detail: { jump: Math.round(jump * 100) / 100, around: Math.round(around * 100) / 100 },
133
+ });
134
+ }
135
+ return out;
136
+ }