agentvfx 0.0.0-stage → 0.3.4

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 (483) hide show
  1. package/AGENTS.md +8 -0
  2. package/GEMINI.md +1 -0
  3. package/LICENSE +172 -0
  4. package/README.md +236 -2
  5. package/package.json +35 -4
  6. package/skills/agentvfx/LICENSE +172 -0
  7. package/skills/agentvfx/NOTICE +44 -0
  8. package/skills/agentvfx/SKILL.md +90 -0
  9. package/skills/agentvfx/engine/core/constants.mjs +84 -0
  10. package/skills/agentvfx/engine/core/flash-constants.mjs +23 -0
  11. package/skills/agentvfx/engine/core/flash.mjs +240 -0
  12. package/skills/agentvfx/engine/core/mark.mjs +28 -0
  13. package/skills/agentvfx/engine/core/palettes.mjs +36 -0
  14. package/skills/agentvfx/engine/core/schedule.mjs +85 -0
  15. package/skills/agentvfx/engine/core/templates.mjs +43 -0
  16. package/skills/agentvfx/engine/core/util.mjs +167 -0
  17. package/skills/agentvfx/engine/core/validate.mjs +334 -0
  18. package/skills/agentvfx/engine/node/analysis.mjs +172 -0
  19. package/skills/agentvfx/engine/node/audio.mjs +621 -0
  20. package/skills/agentvfx/engine/node/board.mjs +24 -0
  21. package/skills/agentvfx/engine/node/chrome.mjs +349 -0
  22. package/skills/agentvfx/engine/node/encode.mjs +110 -0
  23. package/skills/agentvfx/engine/node/eye.mjs +77 -0
  24. package/skills/agentvfx/engine/node/footage.mjs +129 -0
  25. package/skills/agentvfx/engine/node/frame-worker.mjs +39 -0
  26. package/skills/agentvfx/engine/node/host.mjs +31 -0
  27. package/skills/agentvfx/engine/node/job.mjs +181 -0
  28. package/skills/agentvfx/engine/node/png.mjs +112 -0
  29. package/skills/agentvfx/engine/node/render.mjs +161 -0
  30. package/skills/agentvfx/engine/node/stage-capture.mjs +187 -0
  31. package/skills/agentvfx/engine/node/stage-serve.mjs +72 -0
  32. package/skills/agentvfx/engine/node/stills.mjs +100 -0
  33. package/skills/agentvfx/engine/node/ws-server.mjs +132 -0
  34. package/skills/agentvfx/engine/web/gl.mjs +319 -0
  35. package/skills/agentvfx/engine/web/glsl-common.mjs +78 -0
  36. package/skills/agentvfx/engine/web/init.js +23 -0
  37. package/skills/agentvfx/engine/web/prelude.mjs +150 -0
  38. package/skills/agentvfx/engine/web/render.html +11 -0
  39. package/skills/agentvfx/engine/web/render.mjs +388 -0
  40. package/skills/agentvfx/engine/web/scale.mjs +15 -0
  41. package/skills/agentvfx/engine/web/text.mjs +158 -0
  42. package/skills/agentvfx/plates/terminalcore/README.md +61 -0
  43. package/skills/agentvfx/plates/terminalcore/__pycache__/crt.cpython-313.pyc +0 -0
  44. package/skills/agentvfx/plates/terminalcore/__pycache__/font.cpython-313.pyc +0 -0
  45. package/skills/agentvfx/plates/terminalcore/__pycache__/gallery.cpython-313.pyc +0 -0
  46. package/skills/agentvfx/plates/terminalcore/__pycache__/gallery_mats.cpython-313.pyc +0 -0
  47. package/skills/agentvfx/plates/terminalcore/__pycache__/gallery_room.cpython-313.pyc +0 -0
  48. package/skills/agentvfx/plates/terminalcore/__pycache__/materials.cpython-313.pyc +0 -0
  49. package/skills/agentvfx/plates/terminalcore/__pycache__/nodekit.cpython-313.pyc +0 -0
  50. package/skills/agentvfx/plates/terminalcore/__pycache__/room.cpython-313.pyc +0 -0
  51. package/skills/agentvfx/plates/terminalcore/__pycache__/shots.cpython-313.pyc +0 -0
  52. package/skills/agentvfx/plates/terminalcore/__pycache__/textures.cpython-313.pyc +0 -0
  53. package/skills/agentvfx/plates/terminalcore/__pycache__/wall.cpython-313.pyc +0 -0
  54. package/skills/agentvfx/plates/terminalcore/crt.py +143 -0
  55. package/skills/agentvfx/plates/terminalcore/font.py +80 -0
  56. package/skills/agentvfx/plates/terminalcore/gallery.py +61 -0
  57. package/skills/agentvfx/plates/terminalcore/gallery_mats.py +144 -0
  58. package/skills/agentvfx/plates/terminalcore/gallery_room.py +126 -0
  59. package/skills/agentvfx/plates/terminalcore/materials.py +170 -0
  60. package/skills/agentvfx/plates/terminalcore/nodekit.py +87 -0
  61. package/skills/agentvfx/plates/terminalcore/room.py +82 -0
  62. package/skills/agentvfx/plates/terminalcore/scene.py +219 -0
  63. package/skills/agentvfx/plates/terminalcore/shots.py +48 -0
  64. package/skills/agentvfx/plates/terminalcore/textures.py +79 -0
  65. package/skills/agentvfx/plates/terminalcore/wall.py +103 -0
  66. package/skills/agentvfx/references/plates.md +22 -0
  67. package/skills/agentvfx/references/template-format.md +149 -0
  68. package/skills/agentvfx/references/web.md +63 -0
  69. package/skills/agentvfx/runtime/agentvfx.js +708 -0
  70. package/skills/agentvfx/schemas/fx-score.schema.json +98 -0
  71. package/skills/agentvfx/scripts/export-web.mjs +155 -0
  72. package/skills/agentvfx/scripts/loop.mjs +129 -0
  73. package/skills/agentvfx/scripts/make-beat.mjs +133 -0
  74. package/skills/agentvfx/scripts/plate.mjs +82 -0
  75. package/skills/agentvfx/scripts/vfx.mjs +173 -0
  76. package/skills/agentvfx/templates/_test/feedback-counter/fx.json +11 -0
  77. package/skills/agentvfx/templates/_test/feedback-counter/main.frag +4 -0
  78. package/skills/agentvfx/templates/_test/feedback-counter/pass0.frag +4 -0
  79. package/skills/agentvfx/templates/_test/gradient/fx.json +23 -0
  80. package/skills/agentvfx/templates/_test/gradient/main.frag +5 -0
  81. package/skills/agentvfx/templates/_test/identity/fx.json +10 -0
  82. package/skills/agentvfx/templates/_test/identity/main.frag +4 -0
  83. package/skills/agentvfx/templates/_test/strobe/fx.json +14 -0
  84. package/skills/agentvfx/templates/_test/strobe/main.frag +6 -0
  85. package/skills/agentvfx/templates/_test/text-probe/fx.json +15 -0
  86. package/skills/agentvfx/templates/_test/text-probe/main.frag +6 -0
  87. package/skills/agentvfx/templates/brutal/NOTES.md +14 -0
  88. package/skills/agentvfx/templates/brutal/_src/boot-log/def.mjs +57 -0
  89. package/skills/agentvfx/templates/brutal/_src/boot-log/main.src.frag +129 -0
  90. package/skills/agentvfx/templates/brutal/_src/build.mjs +33 -0
  91. package/skills/agentvfx/templates/brutal/_src/halftone-field/def.mjs +72 -0
  92. package/skills/agentvfx/templates/brutal/_src/halftone-field/main.src.frag +94 -0
  93. package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-left.src.glsl +100 -0
  94. package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-right.src.glsl +61 -0
  95. package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-tiles.src.glsl +153 -0
  96. package/skills/agentvfx/templates/brutal/_src/hud-deck/def.mjs +82 -0
  97. package/skills/agentvfx/templates/brutal/_src/hud-deck/main.src.frag +110 -0
  98. package/skills/agentvfx/templates/brutal/_src/hud-deck/pass0.src.frag +38 -0
  99. package/skills/agentvfx/templates/brutal/_src/shared/defs.mjs +28 -0
  100. package/skills/agentvfx/templates/brutal/_src/shared/icons.glsl +72 -0
  101. package/skills/agentvfx/templates/brutal/_src/shared/lib.glsl +115 -0
  102. package/skills/agentvfx/templates/brutal/_src/shared/screen.glsl +43 -0
  103. package/skills/agentvfx/templates/brutal/boot-log/demo-acid.json +1 -0
  104. package/skills/agentvfx/templates/brutal/boot-log/demo-phosphor.json +1 -0
  105. package/skills/agentvfx/templates/brutal/boot-log/demo-ultramarine.json +1 -0
  106. package/skills/agentvfx/templates/brutal/boot-log/fx.json +257 -0
  107. package/skills/agentvfx/templates/brutal/boot-log/main.frag +242 -0
  108. package/skills/agentvfx/templates/brutal/collage/_src/00-lib.glsl +73 -0
  109. package/skills/agentvfx/templates/brutal/collage/_src/10-spectrum-scope.glsl +73 -0
  110. package/skills/agentvfx/templates/brutal/collage/_src/20-meter-readout-cross.glsl +87 -0
  111. package/skills/agentvfx/templates/brutal/collage/_src/30-footage-fillers.glsl +92 -0
  112. package/skills/agentvfx/templates/brutal/collage/_src/40-editorial.glsl +232 -0
  113. package/skills/agentvfx/templates/brutal/collage/_src/50-typefx.glsl +163 -0
  114. package/skills/agentvfx/templates/brutal/collage/_src/60-scene-lib.glsl +104 -0
  115. package/skills/agentvfx/templates/brutal/collage/_src/61-scene-specimen.glsl +71 -0
  116. package/skills/agentvfx/templates/brutal/collage/_src/62-scene-wiring.glsl +96 -0
  117. package/skills/agentvfx/templates/brutal/collage/_src/63-scene-fieldcut.glsl +119 -0
  118. package/skills/agentvfx/templates/brutal/collage/_src/64-scene-pattern.glsl +64 -0
  119. package/skills/agentvfx/templates/brutal/collage/_src/641-scene-anatomy.glsl +49 -0
  120. package/skills/agentvfx/templates/brutal/collage/_src/642-scene-deck.glsl +78 -0
  121. package/skills/agentvfx/templates/brutal/collage/_src/643-scene-bootlog.glsl +60 -0
  122. package/skills/agentvfx/templates/brutal/collage/_src/644-scene-tunnel.glsl +48 -0
  123. package/skills/agentvfx/templates/brutal/collage/_src/645-scene-runner.glsl +42 -0
  124. package/skills/agentvfx/templates/brutal/collage/_src/646-scene-ascii.glsl +66 -0
  125. package/skills/agentvfx/templates/brutal/collage/_src/647-scene-cubewall.glsl +80 -0
  126. package/skills/agentvfx/templates/brutal/collage/_src/648-scene-vortex.glsl +46 -0
  127. package/skills/agentvfx/templates/brutal/collage/_src/649-scene-sticker.glsl +72 -0
  128. package/skills/agentvfx/templates/brutal/collage/_src/65-scene-main.glsl +1 -0
  129. package/skills/agentvfx/templates/brutal/collage/_src/650-scene-ribbon.glsl +55 -0
  130. package/skills/agentvfx/templates/brutal/collage/_src/651-scene-slab.glsl +38 -0
  131. package/skills/agentvfx/templates/brutal/collage/_src/652-scene-framedial.glsl +79 -0
  132. package/skills/agentvfx/templates/brutal/collage/_src/653-scene-tilt.glsl +40 -0
  133. package/skills/agentvfx/templates/brutal/collage/_src/654-scene-marquee.glsl +45 -0
  134. package/skills/agentvfx/templates/brutal/collage/_src/655-scene-lens.glsl +57 -0
  135. package/skills/agentvfx/templates/brutal/collage/_src/656-scene-liquid.glsl +35 -0
  136. package/skills/agentvfx/templates/brutal/collage/_src/657-scene-panelgrid.glsl +75 -0
  137. package/skills/agentvfx/templates/brutal/collage/_src/658-scene-mirror.glsl +49 -0
  138. package/skills/agentvfx/templates/brutal/collage/_src/66-scene-main.glsl +27 -0
  139. package/skills/agentvfx/templates/brutal/collage/_src/90-main.glsl +120 -0
  140. package/skills/agentvfx/templates/brutal/collage/_src/build.mjs +24 -0
  141. package/skills/agentvfx/templates/brutal/collage/_src/sync-presets.mjs +16 -0
  142. package/skills/agentvfx/templates/brutal/collage/demo-bone-lime.json +1 -0
  143. package/skills/agentvfx/templates/brutal/collage/demo-lime-sparse.json +1 -0
  144. package/skills/agentvfx/templates/brutal/collage/demo-orange-dense.json +1 -0
  145. package/skills/agentvfx/templates/brutal/collage/demo-ultra-slow.json +1 -0
  146. package/skills/agentvfx/templates/brutal/collage/fx.json +4882 -0
  147. package/skills/agentvfx/templates/brutal/collage/layout.mjs +97 -0
  148. package/skills/agentvfx/templates/brutal/collage/layouts.json +366 -0
  149. package/skills/agentvfx/templates/brutal/collage/main.frag +2367 -0
  150. package/skills/agentvfx/templates/brutal/collage/pass0.frag +265 -0
  151. package/skills/agentvfx/templates/brutal/halftone-field/demo-acid.json +1 -0
  152. package/skills/agentvfx/templates/brutal/halftone-field/demo-ascii.json +1 -0
  153. package/skills/agentvfx/templates/brutal/halftone-field/demo-dither.json +1 -0
  154. package/skills/agentvfx/templates/brutal/halftone-field/demo-orange.json +1 -0
  155. package/skills/agentvfx/templates/brutal/halftone-field/demo-phosphor.json +1 -0
  156. package/skills/agentvfx/templates/brutal/halftone-field/demo-ultramarine.json +1 -0
  157. package/skills/agentvfx/templates/brutal/halftone-field/fx.json +474 -0
  158. package/skills/agentvfx/templates/brutal/halftone-field/main.frag +248 -0
  159. package/skills/agentvfx/templates/brutal/hud-deck/demo-acid.json +1 -0
  160. package/skills/agentvfx/templates/brutal/hud-deck/demo-night.json +1 -0
  161. package/skills/agentvfx/templates/brutal/hud-deck/demo-octagon.json +1 -0
  162. package/skills/agentvfx/templates/brutal/hud-deck/demo-ultramarine.json +1 -0
  163. package/skills/agentvfx/templates/brutal/hud-deck/fx.json +601 -0
  164. package/skills/agentvfx/templates/brutal/hud-deck/main.frag +645 -0
  165. package/skills/agentvfx/templates/brutal/hud-deck/pass0.frag +38 -0
  166. package/skills/agentvfx/templates/brutal/lattice/demo-cobalt-fine.json +1 -0
  167. package/skills/agentvfx/templates/brutal/lattice/demo-ink-yellow.json +1 -0
  168. package/skills/agentvfx/templates/brutal/lattice/demo-vermilion.json +1 -0
  169. package/skills/agentvfx/templates/brutal/lattice/demo-violet-slow.json +1 -0
  170. package/skills/agentvfx/templates/brutal/lattice/fx.json +130 -0
  171. package/skills/agentvfx/templates/brutal/lattice/main.frag +175 -0
  172. package/skills/agentvfx/templates/brutal/lattice/pass0.frag +29 -0
  173. package/skills/agentvfx/templates/glitch/block-glitch/demo-mosaic.json +1 -0
  174. package/skills/agentvfx/templates/glitch/block-glitch/demo-onset.json +1 -0
  175. package/skills/agentvfx/templates/glitch/block-glitch/demo-static.json +1 -0
  176. package/skills/agentvfx/templates/glitch/block-glitch/demo-tear.json +1 -0
  177. package/skills/agentvfx/templates/glitch/block-glitch/fx.json +54 -0
  178. package/skills/agentvfx/templates/glitch/block-glitch/main.frag +132 -0
  179. package/skills/agentvfx/templates/glitch/block-glitch/pass0.frag +12 -0
  180. package/skills/agentvfx/templates/glitch/pixel-sort/NOTES.md +13 -0
  181. package/skills/agentvfx/templates/glitch/pixel-sort/build-passes.mjs +162 -0
  182. package/skills/agentvfx/templates/glitch/pixel-sort/demo-chunk.json +1 -0
  183. package/skills/agentvfx/templates/glitch/pixel-sort/demo-hue.json +1 -0
  184. package/skills/agentvfx/templates/glitch/pixel-sort/demo-kick.json +1 -0
  185. package/skills/agentvfx/templates/glitch/pixel-sort/demo-streak.json +1 -0
  186. package/skills/agentvfx/templates/glitch/pixel-sort/demo-vertical.json +1 -0
  187. package/skills/agentvfx/templates/glitch/pixel-sort/fx.json +60 -0
  188. package/skills/agentvfx/templates/glitch/pixel-sort/main.frag +83 -0
  189. package/skills/agentvfx/templates/glitch/pixel-sort/pass0.frag +64 -0
  190. package/skills/agentvfx/templates/glitch/pixel-sort/pass1.frag +66 -0
  191. package/skills/agentvfx/templates/glitch/pixel-sort/pass2.frag +66 -0
  192. package/skills/agentvfx/templates/glitch/pixel-sort/pass3.frag +66 -0
  193. package/skills/agentvfx/templates/glitch/pixel-sort/pass4.frag +66 -0
  194. package/skills/agentvfx/templates/glitch/pixel-sort/pass5.frag +66 -0
  195. package/skills/agentvfx/templates/glitch/pixel-sort/pass6.frag +66 -0
  196. package/skills/agentvfx/templates/glitch/pixel-sort/pass7.frag +66 -0
  197. package/skills/agentvfx/templates/glitch/pixel-sort/pass8.frag +66 -0
  198. package/skills/agentvfx/templates/glitch/rgb-split/demo-diagonal-stutter.json +1 -0
  199. package/skills/agentvfx/templates/glitch/rgb-split/demo-hard.json +1 -0
  200. package/skills/agentvfx/templates/glitch/rgb-split/demo-kick.json +1 -0
  201. package/skills/agentvfx/templates/glitch/rgb-split/demo-radial.json +1 -0
  202. package/skills/agentvfx/templates/glitch/rgb-split/fx.json +46 -0
  203. package/skills/agentvfx/templates/glitch/rgb-split/main.frag +76 -0
  204. package/skills/agentvfx/templates/glitch/vhs/demo-clean.json +1 -0
  205. package/skills/agentvfx/templates/glitch/vhs/demo-crash.json +1 -0
  206. package/skills/agentvfx/templates/glitch/vhs/demo-tape.json +1 -0
  207. package/skills/agentvfx/templates/glitch/vhs/demo-worn.json +1 -0
  208. package/skills/agentvfx/templates/glitch/vhs/fx.json +46 -0
  209. package/skills/agentvfx/templates/glitch/vhs/main.frag +96 -0
  210. package/skills/agentvfx/templates/impact/impact-1bit/demo-onebit-bayer.json +1 -0
  211. package/skills/agentvfx/templates/impact/impact-1bit/fx.json +45 -0
  212. package/skills/agentvfx/templates/impact/impact-1bit/main.frag +64 -0
  213. package/skills/agentvfx/templates/impact/impact-invert/demo-invert-snap.json +1 -0
  214. package/skills/agentvfx/templates/impact/impact-invert/fx.json +37 -0
  215. package/skills/agentvfx/templates/impact/impact-invert/main.frag +44 -0
  216. package/skills/agentvfx/templates/impact/punch-shake/demo-punch-hard.json +1 -0
  217. package/skills/agentvfx/templates/impact/punch-shake/fx.json +36 -0
  218. package/skills/agentvfx/templates/impact/punch-shake/main.frag +62 -0
  219. package/skills/agentvfx/templates/impact/rgb-hit/demo-rgb-slam.json +1 -0
  220. package/skills/agentvfx/templates/impact/rgb-hit/fx.json +39 -0
  221. package/skills/agentvfx/templates/impact/rgb-hit/main.frag +62 -0
  222. package/skills/agentvfx/templates/impact/signal-cut/demo-lime-hold.json +1 -0
  223. package/skills/agentvfx/templates/impact/signal-cut/demo-lime.json +1 -0
  224. package/skills/agentvfx/templates/impact/signal-cut/demo-orange.json +1 -0
  225. package/skills/agentvfx/templates/impact/signal-cut/demo-ultramarine.json +1 -0
  226. package/skills/agentvfx/templates/impact/signal-cut/fx.json +83 -0
  227. package/skills/agentvfx/templates/impact/signal-cut/main.frag +101 -0
  228. package/skills/agentvfx/templates/impact/signal-cut/pass0.frag +68 -0
  229. package/skills/agentvfx/templates/impact/speedline-burst/demo-speed-white.json +1 -0
  230. package/skills/agentvfx/templates/impact/speedline-burst/fx.json +44 -0
  231. package/skills/agentvfx/templates/impact/speedline-burst/main.frag +68 -0
  232. package/skills/agentvfx/templates/impact/strobe-burst/demo-strobe-white-16.json +1 -0
  233. package/skills/agentvfx/templates/impact/strobe-burst/fx.json +42 -0
  234. package/skills/agentvfx/templates/impact/strobe-burst/main.frag +43 -0
  235. package/skills/agentvfx/templates/light/anamorphic-streak/demo-anamorphic-cinema.json +10 -0
  236. package/skills/agentvfx/templates/light/anamorphic-streak/demo-data.json +10 -0
  237. package/skills/agentvfx/templates/light/anamorphic-streak/fx.json +47 -0
  238. package/skills/agentvfx/templates/light/anamorphic-streak/main.frag +45 -0
  239. package/skills/agentvfx/templates/light/anamorphic-streak/pass0.frag +22 -0
  240. package/skills/agentvfx/templates/light/anamorphic-streak/pass1.frag +16 -0
  241. package/skills/agentvfx/templates/light/anamorphic-streak/pass2.frag +16 -0
  242. package/skills/agentvfx/templates/light/anamorphic-streak/pass3.frag +16 -0
  243. package/skills/agentvfx/templates/light/bloom-glow/demo-clip01-low.json +11 -0
  244. package/skills/agentvfx/templates/light/bloom-glow/demo-neon-night.json +10 -0
  245. package/skills/agentvfx/templates/light/bloom-glow/demo-phosphor.json +11 -0
  246. package/skills/agentvfx/templates/light/bloom-glow/fx.json +53 -0
  247. package/skills/agentvfx/templates/light/bloom-glow/main.frag +85 -0
  248. package/skills/agentvfx/templates/light/bloom-glow/pass0.frag +21 -0
  249. package/skills/agentvfx/templates/light/bloom-glow/pass1.frag +50 -0
  250. package/skills/agentvfx/templates/light/bloom-glow/pass2.frag +44 -0
  251. package/skills/agentvfx/templates/light/bloom-glow/pass3.frag +32 -0
  252. package/skills/agentvfx/templates/light/bloom-glow/pass4.frag +48 -0
  253. package/skills/agentvfx/templates/light/bloom-glow/pass5.frag +46 -0
  254. package/skills/agentvfx/templates/light/god-rays/demo-golden-hour.json +10 -0
  255. package/skills/agentvfx/templates/light/god-rays/demo-point-lamp.json +10 -0
  256. package/skills/agentvfx/templates/light/god-rays/fx.json +39 -0
  257. package/skills/agentvfx/templates/light/god-rays/main.frag +54 -0
  258. package/skills/agentvfx/templates/light/lens-flare/demo-clip01-bar.json +11 -0
  259. package/skills/agentvfx/templates/light/lens-flare/demo-eye-bar.json +10 -0
  260. package/skills/agentvfx/templates/light/lens-flare/demo-sun-ghosts.json +10 -0
  261. package/skills/agentvfx/templates/light/lens-flare/fx.json +49 -0
  262. package/skills/agentvfx/templates/light/lens-flare/main.frag +86 -0
  263. package/skills/agentvfx/templates/light/lens-flare/pass0.frag +24 -0
  264. package/skills/agentvfx/templates/light/light-leak/demo-slit.json +10 -0
  265. package/skills/agentvfx/templates/light/light-leak/demo-vintage-burn.json +10 -0
  266. package/skills/agentvfx/templates/light/light-leak/fx.json +40 -0
  267. package/skills/agentvfx/templates/light/light-leak/main.frag +46 -0
  268. package/skills/agentvfx/templates/light/light-stage/fx.json +29 -0
  269. package/skills/agentvfx/templates/light/light-stage/main.frag +56 -0
  270. package/skills/agentvfx/templates/light/light-sweep/demo-hard-bar.json +12 -0
  271. package/skills/agentvfx/templates/light/light-sweep/demo-logo-sweep.json +10 -0
  272. package/skills/agentvfx/templates/light/light-sweep/demo-onset.json +11 -0
  273. package/skills/agentvfx/templates/light/light-sweep/fx.json +45 -0
  274. package/skills/agentvfx/templates/light/light-sweep/main.frag +29 -0
  275. package/skills/agentvfx/templates/light/light-sweep/pass0.frag +17 -0
  276. package/skills/agentvfx/templates/light/light-sweep/pass1.frag +15 -0
  277. package/skills/agentvfx/templates/light/neon-saber/demo-hairline.json +10 -0
  278. package/skills/agentvfx/templates/light/neon-saber/demo-saber-laser.json +10 -0
  279. package/skills/agentvfx/templates/light/neon-saber/fx.json +48 -0
  280. package/skills/agentvfx/templates/light/neon-saber/main.frag +27 -0
  281. package/skills/agentvfx/templates/light/neon-saber/pass0.frag +46 -0
  282. package/skills/agentvfx/templates/light/neon-saber/pass1.frag +15 -0
  283. package/skills/agentvfx/templates/psychedelic/echo-flow/demo-trail.json +1 -0
  284. package/skills/agentvfx/templates/psychedelic/echo-flow/fx.json +38 -0
  285. package/skills/agentvfx/templates/psychedelic/echo-flow/main.frag +19 -0
  286. package/skills/agentvfx/templates/psychedelic/echo-flow/pass0.frag +16 -0
  287. package/skills/agentvfx/templates/psychedelic/echo-flow/pass1.frag +20 -0
  288. package/skills/agentvfx/templates/psychedelic/echo-flow/pass2.frag +11 -0
  289. package/skills/agentvfx/templates/psychedelic/echo-flow/pass3.frag +4 -0
  290. package/skills/agentvfx/templates/psychedelic/hologram-scan/demo-cyan.json +1 -0
  291. package/skills/agentvfx/templates/psychedelic/hologram-scan/fx.json +36 -0
  292. package/skills/agentvfx/templates/psychedelic/hologram-scan/main.frag +30 -0
  293. package/skills/agentvfx/templates/psychedelic/kaleido-fold/demo-six.json +1 -0
  294. package/skills/agentvfx/templates/psychedelic/kaleido-fold/fx.json +37 -0
  295. package/skills/agentvfx/templates/psychedelic/kaleido-fold/main.frag +10 -0
  296. package/skills/agentvfx/templates/psychedelic/kaleido-fold/pass0.frag +34 -0
  297. package/skills/agentvfx/templates/psychedelic/mosh-melt/demo-classic.json +1 -0
  298. package/skills/agentvfx/templates/psychedelic/mosh-melt/fx.json +38 -0
  299. package/skills/agentvfx/templates/psychedelic/mosh-melt/main.frag +20 -0
  300. package/skills/agentvfx/templates/psychedelic/mosh-melt/pass0.frag +22 -0
  301. package/skills/agentvfx/templates/psychedelic/sacred/NOTES.md +13 -0
  302. package/skills/agentvfx/templates/psychedelic/sacred/demo-classic.json +1 -0
  303. package/skills/agentvfx/templates/psychedelic/sacred/demo-halo.json +1 -0
  304. package/skills/agentvfx/templates/psychedelic/sacred/demo-slow-tide.json +1 -0
  305. package/skills/agentvfx/templates/psychedelic/sacred/demo-thermal.json +1 -0
  306. package/skills/agentvfx/templates/psychedelic/sacred/fx.json +263 -0
  307. package/skills/agentvfx/templates/psychedelic/sacred/main.frag +109 -0
  308. package/skills/agentvfx/templates/psychedelic/sacred/pass0.frag +25 -0
  309. package/skills/agentvfx/templates/psychedelic/sacred/pass1.frag +23 -0
  310. package/skills/agentvfx/templates/psychedelic/tidal-bands/demo-classic.json +1 -0
  311. package/skills/agentvfx/templates/psychedelic/tidal-bands/fx.json +37 -0
  312. package/skills/agentvfx/templates/psychedelic/tidal-bands/main.frag +54 -0
  313. package/skills/agentvfx/templates/scope/_chrome.glsl +242 -0
  314. package/skills/agentvfx/templates/scope/goniometer/demo-boot-green.json +1 -0
  315. package/skills/agentvfx/templates/scope/goniometer/demo-field-lime.json +1 -0
  316. package/skills/agentvfx/templates/scope/goniometer/demo-field-ultramarine.json +1 -0
  317. package/skills/agentvfx/templates/scope/goniometer/demo-glow-phosphor.json +1 -0
  318. package/skills/agentvfx/templates/scope/goniometer/demo-hud-black.json +1 -0
  319. package/skills/agentvfx/templates/scope/goniometer/fx.json +635 -0
  320. package/skills/agentvfx/templates/scope/goniometer/main.frag +33 -0
  321. package/skills/agentvfx/templates/scope/goniometer/pass0.frag +58 -0
  322. package/skills/agentvfx/templates/scope/goniometer/pass1.frag +43 -0
  323. package/skills/agentvfx/templates/scope/goniometer/pass2.frag +12 -0
  324. package/skills/agentvfx/templates/scope/goniometer/pass3.frag +12 -0
  325. package/skills/agentvfx/templates/scope/goniometer/pass4.frag +12 -0
  326. package/skills/agentvfx/templates/scope/goniometer/pass5.frag +12 -0
  327. package/skills/agentvfx/templates/scope/goniometer/pass6.frag +251 -0
  328. package/skills/agentvfx/templates/scope/scope-polar/demo-boot-green.json +1 -0
  329. package/skills/agentvfx/templates/scope/scope-polar/demo-field-lime.json +1 -0
  330. package/skills/agentvfx/templates/scope/scope-polar/demo-field-ultramarine.json +1 -0
  331. package/skills/agentvfx/templates/scope/scope-polar/demo-glow-phosphor.json +1 -0
  332. package/skills/agentvfx/templates/scope/scope-polar/demo-hud-black.json +1 -0
  333. package/skills/agentvfx/templates/scope/scope-polar/fx.json +686 -0
  334. package/skills/agentvfx/templates/scope/scope-polar/main.frag +39 -0
  335. package/skills/agentvfx/templates/scope/scope-polar/pass0.frag +84 -0
  336. package/skills/agentvfx/templates/scope/scope-polar/pass1.frag +49 -0
  337. package/skills/agentvfx/templates/scope/scope-polar/pass2.frag +12 -0
  338. package/skills/agentvfx/templates/scope/scope-polar/pass3.frag +12 -0
  339. package/skills/agentvfx/templates/scope/scope-polar/pass4.frag +12 -0
  340. package/skills/agentvfx/templates/scope/scope-polar/pass5.frag +12 -0
  341. package/skills/agentvfx/templates/scope/scope-polar/pass6.frag +251 -0
  342. package/skills/agentvfx/templates/scope/scope-triggered/demo-boot-green.json +1 -0
  343. package/skills/agentvfx/templates/scope/scope-triggered/demo-field-lime.json +1 -0
  344. package/skills/agentvfx/templates/scope/scope-triggered/demo-field-ultramarine.json +1 -0
  345. package/skills/agentvfx/templates/scope/scope-triggered/demo-glow-phosphor.json +1 -0
  346. package/skills/agentvfx/templates/scope/scope-triggered/demo-hud-black.json +1 -0
  347. package/skills/agentvfx/templates/scope/scope-triggered/fx.json +712 -0
  348. package/skills/agentvfx/templates/scope/scope-triggered/main.frag +36 -0
  349. package/skills/agentvfx/templates/scope/scope-triggered/pass0.frag +78 -0
  350. package/skills/agentvfx/templates/scope/scope-triggered/pass1.frag +50 -0
  351. package/skills/agentvfx/templates/scope/scope-triggered/pass2.frag +12 -0
  352. package/skills/agentvfx/templates/scope/scope-triggered/pass3.frag +12 -0
  353. package/skills/agentvfx/templates/scope/scope-triggered/pass4.frag +12 -0
  354. package/skills/agentvfx/templates/scope/scope-triggered/pass5.frag +12 -0
  355. package/skills/agentvfx/templates/scope/scope-triggered/pass6.frag +251 -0
  356. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-boot-green.json +1 -0
  357. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-field-lime.json +1 -0
  358. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-field-ultramarine.json +1 -0
  359. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-glow-phosphor.json +1 -0
  360. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-hud-black.json +1 -0
  361. package/skills/agentvfx/templates/scope/spectrogram-scroll/fx.json +595 -0
  362. package/skills/agentvfx/templates/scope/spectrogram-scroll/main.frag +83 -0
  363. package/skills/agentvfx/templates/scope/spectrogram-scroll/pass0.frag +52 -0
  364. package/skills/agentvfx/templates/scope/spectrogram-scroll/pass1.frag +251 -0
  365. package/skills/agentvfx/templates/scope/xy-phosphor/demo-boot-green.json +1 -0
  366. package/skills/agentvfx/templates/scope/xy-phosphor/demo-field-lime.json +1 -0
  367. package/skills/agentvfx/templates/scope/xy-phosphor/demo-field-ultramarine.json +1 -0
  368. package/skills/agentvfx/templates/scope/xy-phosphor/demo-glow-phosphor.json +1 -0
  369. package/skills/agentvfx/templates/scope/xy-phosphor/demo-hud-black.json +1 -0
  370. package/skills/agentvfx/templates/scope/xy-phosphor/fx.json +642 -0
  371. package/skills/agentvfx/templates/scope/xy-phosphor/main.frag +33 -0
  372. package/skills/agentvfx/templates/scope/xy-phosphor/pass0.frag +56 -0
  373. package/skills/agentvfx/templates/scope/xy-phosphor/pass1.frag +44 -0
  374. package/skills/agentvfx/templates/scope/xy-phosphor/pass2.frag +12 -0
  375. package/skills/agentvfx/templates/scope/xy-phosphor/pass3.frag +12 -0
  376. package/skills/agentvfx/templates/scope/xy-phosphor/pass4.frag +12 -0
  377. package/skills/agentvfx/templates/scope/xy-phosphor/pass5.frag +12 -0
  378. package/skills/agentvfx/templates/scope/xy-phosphor/pass6.frag +251 -0
  379. package/skills/agentvfx/templates/spectrum/_src/bars-linear/fx.src.json +61 -0
  380. package/skills/agentvfx/templates/spectrum/_src/bars-linear/main.src.frag +112 -0
  381. package/skills/agentvfx/templates/spectrum/_src/bars-linear/pass0.src.frag +2 -0
  382. package/skills/agentvfx/templates/spectrum/_src/bars-mirror/fx.src.json +51 -0
  383. package/skills/agentvfx/templates/spectrum/_src/bars-mirror/main.src.frag +72 -0
  384. package/skills/agentvfx/templates/spectrum/_src/bars-mirror/pass0.src.frag +2 -0
  385. package/skills/agentvfx/templates/spectrum/_src/bass-circle/fx.src.json +56 -0
  386. package/skills/agentvfx/templates/spectrum/_src/bass-circle/main.src.frag +136 -0
  387. package/skills/agentvfx/templates/spectrum/_src/bass-circle/pass0.src.frag +2 -0
  388. package/skills/agentvfx/templates/spectrum/_src/build.mjs +68 -0
  389. package/skills/agentvfx/templates/spectrum/_src/label-bars/fx.src.json +54 -0
  390. package/skills/agentvfx/templates/spectrum/_src/label-bars/main.src.frag +102 -0
  391. package/skills/agentvfx/templates/spectrum/_src/label-bars/pass0.src.frag +2 -0
  392. package/skills/agentvfx/templates/spectrum/_src/shared/bars-fn.glsl +64 -0
  393. package/skills/agentvfx/templates/spectrum/_src/shared/bars-pass.glsl +22 -0
  394. package/skills/agentvfx/templates/spectrum/_src/shared/clock.glsl +7 -0
  395. package/skills/agentvfx/templates/spectrum/_src/shared/hud.glsl +50 -0
  396. package/skills/agentvfx/templates/spectrum/_src/shared/stage.glsl +74 -0
  397. package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/fx.src.json +58 -0
  398. package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/main.src.frag +119 -0
  399. package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/pass0.src.frag +33 -0
  400. package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/fx.src.json +56 -0
  401. package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/main.src.frag +95 -0
  402. package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/pass0.src.frag +2 -0
  403. package/skills/agentvfx/templates/spectrum/_src/waveform-line/fx.src.json +60 -0
  404. package/skills/agentvfx/templates/spectrum/_src/waveform-line/main.src.frag +98 -0
  405. package/skills/agentvfx/templates/spectrum/_src/waveform-line/pass0.src.frag +43 -0
  406. package/skills/agentvfx/templates/spectrum/bars-linear/demo-graph-acid.json +1 -0
  407. package/skills/agentvfx/templates/spectrum/bars-linear/demo-graph-black.json +1 -0
  408. package/skills/agentvfx/templates/spectrum/bars-linear/fx.json +632 -0
  409. package/skills/agentvfx/templates/spectrum/bars-linear/main.frag +233 -0
  410. package/skills/agentvfx/templates/spectrum/bars-linear/pass0.frag +91 -0
  411. package/skills/agentvfx/templates/spectrum/bars-mirror/demo-field-acid.json +1 -0
  412. package/skills/agentvfx/templates/spectrum/bars-mirror/demo-field-ultramarine.json +1 -0
  413. package/skills/agentvfx/templates/spectrum/bars-mirror/fx.json +539 -0
  414. package/skills/agentvfx/templates/spectrum/bars-mirror/main.frag +193 -0
  415. package/skills/agentvfx/templates/spectrum/bars-mirror/pass0.frag +91 -0
  416. package/skills/agentvfx/templates/spectrum/bass-circle/demo-bracket-black.json +1 -0
  417. package/skills/agentvfx/templates/spectrum/bass-circle/demo-bracket-ultramarine.json +1 -0
  418. package/skills/agentvfx/templates/spectrum/bass-circle/fx.json +552 -0
  419. package/skills/agentvfx/templates/spectrum/bass-circle/main.frag +258 -0
  420. package/skills/agentvfx/templates/spectrum/bass-circle/pass0.frag +91 -0
  421. package/skills/agentvfx/templates/spectrum/label-bars/demo-deck-black.json +1 -0
  422. package/skills/agentvfx/templates/spectrum/label-bars/demo-deck-ultramarine.json +1 -0
  423. package/skills/agentvfx/templates/spectrum/label-bars/fx.json +552 -0
  424. package/skills/agentvfx/templates/spectrum/label-bars/main.frag +223 -0
  425. package/skills/agentvfx/templates/spectrum/label-bars/pass0.frag +91 -0
  426. package/skills/agentvfx/templates/spectrum/spectrum-particles/demo-cloud-black.json +1 -0
  427. package/skills/agentvfx/templates/spectrum/spectrum-particles/demo-cloud-ultramarine.json +1 -0
  428. package/skills/agentvfx/templates/spectrum/spectrum-particles/fx.json +584 -0
  429. package/skills/agentvfx/templates/spectrum/spectrum-particles/main.frag +240 -0
  430. package/skills/agentvfx/templates/spectrum/spectrum-particles/pass0.frag +101 -0
  431. package/skills/agentvfx/templates/spectrum/spectrum-radial/demo-starburst-black.json +1 -0
  432. package/skills/agentvfx/templates/spectrum/spectrum-radial/demo-starburst-ultramarine.json +1 -0
  433. package/skills/agentvfx/templates/spectrum/spectrum-radial/fx.json +566 -0
  434. package/skills/agentvfx/templates/spectrum/spectrum-radial/main.frag +217 -0
  435. package/skills/agentvfx/templates/spectrum/spectrum-radial/pass0.frag +91 -0
  436. package/skills/agentvfx/templates/spectrum/waveform-line/demo-strip-acid.json +1 -0
  437. package/skills/agentvfx/templates/spectrum/waveform-line/demo-strip-black.json +1 -0
  438. package/skills/agentvfx/templates/spectrum/waveform-line/fx.json +481 -0
  439. package/skills/agentvfx/templates/spectrum/waveform-line/main.frag +219 -0
  440. package/skills/agentvfx/templates/spectrum/waveform-line/pass0.frag +48 -0
  441. package/skills/agentvfx/templates/spectrum/waveform-line/pass1.frag +86 -0
  442. package/skills/agentvfx/test/_render_light.mjs +38 -0
  443. package/skills/agentvfx/test/audio-stereo.test.mjs +45 -0
  444. package/skills/agentvfx/test/audio.test.mjs +257 -0
  445. package/skills/agentvfx/test/brutal.test.mjs +190 -0
  446. package/skills/agentvfx/test/concept-collage-layout.test.mjs +236 -0
  447. package/skills/agentvfx/test/concept-collage-scenes.test.mjs +305 -0
  448. package/skills/agentvfx/test/concept-collage-typefx.test.mjs +153 -0
  449. package/skills/agentvfx/test/concept-collage.test.mjs +84 -0
  450. package/skills/agentvfx/test/concept-lattice.test.mjs +133 -0
  451. package/skills/agentvfx/test/concept-signal-cut.test.mjs +172 -0
  452. package/skills/agentvfx/test/credit-overlay.test.mjs +58 -0
  453. package/skills/agentvfx/test/credit.test.mjs +42 -0
  454. package/skills/agentvfx/test/doctor.test.mjs +31 -0
  455. package/skills/agentvfx/test/encode.test.mjs +22 -0
  456. package/skills/agentvfx/test/fixtures/r7-collage-LAYOUTS.json +5780 -0
  457. package/skills/agentvfx/test/fixtures/score-bad-duration.json +7 -0
  458. package/skills/agentvfx/test/fixtures/score-bad-safety.json +8 -0
  459. package/skills/agentvfx/test/fixtures/score-identity-footage.json +8 -0
  460. package/skills/agentvfx/test/fixtures/score-missing-template.json +7 -0
  461. package/skills/agentvfx/test/fixtures/score-ok.json +8 -0
  462. package/skills/agentvfx/test/fixtures/score-strobe-12hz.json +8 -0
  463. package/skills/agentvfx/test/fixtures/score-text-probe.json +1 -0
  464. package/skills/agentvfx/test/flash-audit.test.mjs +150 -0
  465. package/skills/agentvfx/test/flash.test.mjs +83 -0
  466. package/skills/agentvfx/test/footage.test.mjs +88 -0
  467. package/skills/agentvfx/test/glitch.test.mjs +272 -0
  468. package/skills/agentvfx/test/impact.test.mjs +231 -0
  469. package/skills/agentvfx/test/imports.test.mjs +39 -0
  470. package/skills/agentvfx/test/light.test.mjs +200 -0
  471. package/skills/agentvfx/test/loop.test.mjs +80 -0
  472. package/skills/agentvfx/test/psychedelic.test.mjs +99 -0
  473. package/skills/agentvfx/test/render.test.mjs +73 -0
  474. package/skills/agentvfx/test/sacred.test.mjs +175 -0
  475. package/skills/agentvfx/test/schema.test.mjs +103 -0
  476. package/skills/agentvfx/test/scope.test.mjs +306 -0
  477. package/skills/agentvfx/test/spectrum.test.mjs +165 -0
  478. package/skills/agentvfx/test/text.test.mjs +142 -0
  479. package/skills/agentvfx/test/web.test.mjs +397 -0
  480. package/skills/agentvfx/test-media/SOURCES.md +39 -0
  481. package/skills/agentvfx/test-media/beat-132.mp3 +0 -0
  482. package/skills/agentvfx/test-media/clip-01.mp4 +0 -0
  483. package/skills/agentvfx/test-media/clip-02.mp4 +0 -0
@@ -0,0 +1,39 @@
1
+ // agentvfx (agentvfx licence, see LICENSE). Adapted from lit-typographic-motion 1.0.45 engine/node/frame-worker.mjs.
2
+ // One frame-analysis worker: decode a captured PNG to RGBA and analyse it off the capture thread
3
+ // (the SHA-256 of the decoded RGBA, the flash-audit cells on the frame's own grid, the 0.5th and
4
+ // 99.5th percentile luminance, the busy-cell share). The capture loop steps the next frame while a
5
+ // pool of these decodes. Imported outside a worker (the import test), it does nothing.
6
+ import { parentPort } from "node:worker_threads";
7
+ import { analyzeFrame, busyShare } from "./analysis.mjs";
8
+ import { decodePng } from "./png.mjs";
9
+
10
+ parentPort?.on("message", ({ id, png, width, height }) => {
11
+ try {
12
+ const image = decodePng(Buffer.from(png));
13
+ if (image.width !== width || image.height !== height) {
14
+ parentPort.postMessage({ id, error: `captured ${image.width}x${image.height}, expected ${width}x${height}` });
15
+ return;
16
+ }
17
+ const rgba = image.channels === 4 ? image.pixels : toRgba(image.pixels, image.channels);
18
+ // A stage frame is opaque: analyzeFrame's alpha-as-ink count is meaningless here and is dropped.
19
+ const { cells, luminanceP995, luminanceP005, rgbaSha256 } = analyzeFrame(rgba, width, height);
20
+ const busy = busyShare(rgba, width, height);
21
+ const buffer = rgba.buffer.slice(rgba.byteOffset, rgba.byteOffset + rgba.byteLength);
22
+ parentPort.postMessage({ id, rgba: buffer, cells, luminanceP995, luminanceP005, rgbaSha256, busy }, [buffer, cells.R.buffer, cells.G.buffer, cells.B.buffer]);
23
+ } catch (error) {
24
+ parentPort.postMessage({ id, error: error.message });
25
+ }
26
+ });
27
+
28
+ function toRgba(pixels, channels) {
29
+ const n = pixels.length / channels;
30
+ const out = Buffer.alloc(n * 4);
31
+ for (let i = 0; i < n; i++) {
32
+ const s = i * channels;
33
+ out[i * 4] = pixels[s];
34
+ out[i * 4 + 1] = pixels[channels >= 3 ? s + 1 : s];
35
+ out[i * 4 + 2] = pixels[channels >= 3 ? s + 2 : s];
36
+ out[i * 4 + 3] = 255;
37
+ }
38
+ return out;
39
+ }
@@ -0,0 +1,31 @@
1
+ // Host lookups that differ on Windows. `where` ends each line with \r\n, so a plain split("\n")
2
+ // keeps the \r and the path is never found. npm on Windows is npm.cmd, and since the
3
+ // CVE-2024-27980 fix (Node 18.20.2, 20.12.2, 21.7.3 and every 22+) Node refuses to spawn a .cmd
4
+ // without a shell (spawnSync EINVAL). When npm itself started us, npm_execpath names its npm-cli.js
5
+ // and this Node runs it with no shell at all; otherwise the .cmd goes through cmd.exe with every
6
+ // argument quoted, and an argument cmd.exe could still reinterpret inside quotes is refused.
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ /** The first match for `name` on PATH, or null. */
10
+ export function which(name, env = process.env, { platform = process.platform, spawn = spawnSync } = {}) {
11
+ const probe = spawn(platform === "win32" ? "where" : "which", [name], { encoding: "utf8", env });
12
+ if (probe.status !== 0) return null;
13
+ return String(probe.stdout ?? "").split(/\r?\n/u).map((line) => line.trim()).find(Boolean) ?? null;
14
+ }
15
+
16
+ const CMD_SAFE = /^[\w@.,:=+/\\-]+$/u;
17
+ const CMD_UNSAFE = /["%\r\n]/u;
18
+
19
+ export const quoteForCmd = (arg) => {
20
+ const text = String(arg);
21
+ if (CMD_UNSAFE.test(text)) throw new Error(`refusing to pass ${JSON.stringify(text)} through cmd.exe`);
22
+ // Trailing backslashes are doubled so the closing quote still ends the argument for node.
23
+ return CMD_SAFE.test(text) ? text : `"${text.replace(/(\\+)$/u, "$1$1")}"`;
24
+ };
25
+
26
+ /** { command, args, shell } for spawn/spawnSync. */
27
+ export function npmInvocation(args, { platform = process.platform, env = process.env, execPath = process.execPath } = {}) {
28
+ if (platform !== "win32") return { command: "npm", args, shell: false };
29
+ if (/npm-cli\.c?js$/iu.test(env.npm_execpath ?? "")) return { command: execPath, args: [env.npm_execpath, ...args], shell: false };
30
+ return { command: "npm.cmd", args: args.map(quoteForCmd), shell: true };
31
+ }
@@ -0,0 +1,181 @@
1
+ // agentvfx (agentvfx licence, see LICENSE). One render job, end to end: resolve the score's footage and audio, analyse
2
+ // the audio, render the master (stage mode: full intensity, never tamed), keep the eye's stills, run
3
+ // the flash audit as a report, write SAFETY.txt and the MP4 comment when limits are exceeded, and, only
4
+ // when asked (safety.mode "broadcast-safe" or safeVariant), render a substituted broadcast-safe file.
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { EXIT } from "../core/constants.mjs";
9
+ import { BlockedError } from "./chrome.mjs";
10
+ import { createFlashAuditor } from "../core/flash.mjs";
11
+ import { CREDIT_TEXT, creditText } from "../core/mark.mjs";
12
+ import { buildSchedule } from "../core/schedule.mjs";
13
+ import { findFfmpeg } from "./encode.mjs";
14
+ import { createEye } from "./eye.mjs";
15
+ import { extractAudio, extractAudioFile, openFootage } from "./footage.mjs";
16
+ import { renderScore } from "./render.mjs";
17
+
18
+ export const FLASH_COMMENT = "contains intense flashing";
19
+ const SAFE = { maxPerSecond: 3, minGapMs: 334 };
20
+
21
+ async function loadAudio() {
22
+ return import("./audio.mjs");
23
+ }
24
+
25
+ /** Broadcast-safe hits: keep flashing hits at most 3 per 1 s with leading edges >= 334 ms apart; substitute the rest. */
26
+ export function safeScore(score, lib, features) {
27
+ const fps = score.fps;
28
+ const sched = buildSchedule({ ...score, cuts: [], bindings: [] }, lib, { features });
29
+ const starts = (score.hits ?? []).map((h, i) => ({ i, h, start: sched.frames.findIndex((fr) => fr.layers.some((l) => l.key === `h${i}`)) }));
30
+ const flashing = (id) => ["possible", "strobe"].includes(lib.get(id)?.fx.X_AGENTVFX.flash);
31
+ const kept = [];
32
+ const substitutions = [];
33
+ const hits = [];
34
+ for (const { i, h, start } of starts.sort((a, b) => a.start - b.start || a.i - b.i)) {
35
+ if (!flashing(h.template)) {
36
+ hits.push(h);
37
+ continue;
38
+ }
39
+ const gapOk = kept.every((k) => Math.abs(start - k) * 1000 / fps >= SAFE.minGapMs);
40
+ const windowOk = kept.filter((k) => start - k < fps).length < SAFE.maxPerSecond;
41
+ const sub = lib.get(h.template).fx.X_AGENTVFX.safeSubstitute;
42
+ if (start >= 0 && gapOk && windowOk) {
43
+ kept.push(start);
44
+ // A kept hit is one flash, not a burst: a params-only substitute (e.g. pulses: 1) still applies.
45
+ hits.push(sub?.params && !sub.template ? { ...h, params: { ...(h.params ?? {}), ...sub.params } } : h);
46
+ continue;
47
+ }
48
+ if (sub) {
49
+ // A hit swaps to the substitute template (a motion or isoluminant hit); without one, the
50
+ // substitute params tame the same template.
51
+ const replaced = sub.template
52
+ ? { ...h, template: sub.template, preset: undefined, params: undefined }
53
+ : { ...h, params: { ...(h.params ?? {}), ...(sub.params ?? {}) } };
54
+ hits.push(replaced);
55
+ substitutions.push({ hit: i, frame: start, from: h.template, to: replaced.template, params: sub.params ?? {} });
56
+ } else substitutions.push({ hit: i, frame: start, from: h.template, to: null, dropped: true });
57
+ }
58
+ const tame = (layer) => {
59
+ const ref = typeof layer === "string" ? { template: layer } : layer;
60
+ const sub = lib.get(ref.template)?.fx.X_AGENTVFX.flash === "strobe" ? lib.get(ref.template).fx.X_AGENTVFX.safeSubstitute : null;
61
+ return sub ? { ...ref, params: { ...(ref.params ?? {}), ...(sub.params ?? {}) } } : layer;
62
+ };
63
+ const base = tame({ template: score.template, preset: score.preset, params: score.params });
64
+ return {
65
+ score: { ...score, params: base.params, stack: score.stack?.map(tame), cuts: score.cuts?.map((c) => ({ ...c, layers: c.layers.map(tame) })), hits: hits.length ? hits : undefined },
66
+ substitutions,
67
+ };
68
+ }
69
+
70
+ function remuxComment(file, comment) {
71
+ const tmp = `${file}.tmp.mp4`;
72
+ const r = spawnSync(findFfmpeg(), ["-v", "error", "-y", "-i", file, "-map", "0", "-c", "copy", "-metadata", `comment=${comment}`, "-movflags", "+faststart", tmp], { encoding: "utf8" });
73
+ if (r.status !== 0) throw new Error(`could not tag ${file}: ${r.stderr}`);
74
+ renameSync(tmp, file);
75
+ }
76
+
77
+ function safetyText(flash, film) {
78
+ return [
79
+ "SAFETY NOTICE: this clip contains intense flashing.",
80
+ "",
81
+ `File: ${path.basename(film)}`,
82
+ `Worst 1 s window: ${flash.worstWindow?.transitions ?? "?"} transitions from ${flash.worstWindow?.startSec ?? "?"} s`,
83
+ `Max flashes per second: ${flash.flashesPerSecondMax}`,
84
+ `Red flashes: ${flash.redFlashes}`,
85
+ `Extended flashing (4-6 transitions/s for 4 s): ${flash.extendedFlashing}`,
86
+ "",
87
+ "Limits exceeded: WCAG 2.2 SC 2.3.1 general flash / ITU-R BT.1702-3 (more than 3 flashes in any 1 s,",
88
+ "or any saturated-red flash). This file was rendered at full intensity for stage use (stage mode).",
89
+ "For broadcast or web posting, render with \"safety\": { \"safeVariant\": true } to also get film.safe.mp4.",
90
+ "",
91
+ ].join("\n");
92
+ }
93
+
94
+ async function pass({ score, lib, outDir, film, footageFile, audioWav, features, width, height, replay, events }) {
95
+ const longEdge = width && height ? Math.max(width, height) : 1920;
96
+ const footage = footageFile ? openFootage(footageFile, { fps: score.fps, startSec: score.input?.startSec ?? 0, durationSec: score.durationSec, longEdge }) : null;
97
+ const w = width ?? footage?.width ?? 1920;
98
+ const h = height ?? footage?.height ?? 1080;
99
+ const sched = buildSchedule(score, lib, { features });
100
+ const eventsAll = sched.events.length ? sched.events : (features?.meta?.onsets ?? []).filter((f) => f < sched.frameCount);
101
+ const eye = createEye({ width: w, height: h, frameCount: sched.frameCount, events: events ?? eventsAll });
102
+ const auditor = createFlashAuditor({ fps: score.fps, width: w, height: h });
103
+ try {
104
+ const report = await renderScore(score, lib, {
105
+ outDir, width: w, height: h, features, footage, audioFile: audioWav, replay,
106
+ onFrame: (f, a) => {
107
+ auditor.push({ width: w, height: h, data: a.rgba });
108
+ if (eye.wants(f)) eye.push(f, a.rgba);
109
+ },
110
+ });
111
+ if (film !== report.film) renameSync(report.film, film);
112
+ return { report: { ...report, film }, flash: auditor.finish(), eye, footage: footage ? { width: footage.width, height: footage.height, matrix: footage.matrix, transport: "cdp-fetch-interception" } : null };
113
+ } finally {
114
+ footage?.close();
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Run a validated score. Writes film.mp4, stills/, gate-report.json, features.json (with audio),
120
+ * CREDIT.txt (the attribution line), SAFETY.txt (stage mode, limits exceeded), film.safe.mp4 (safe variant) or withheld/ (safe file failed).
121
+ */
122
+ export async function runJob(score, lib, { outDir, width, height, replay = true, cwd = process.cwd() }) {
123
+ mkdirSync(outDir, { recursive: true });
124
+ const footageFile = score.input?.video ? path.resolve(cwd, score.input.video) : null;
125
+ if (footageFile && !existsSync(footageFile)) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: input.video not found: ${footageFile}`);
126
+ const window = { startSec: score.input?.startSec ?? 0, durationSec: score.durationSec };
127
+ const tmpDirs = [];
128
+ try {
129
+ let audio = null;
130
+ if (score.input?.audio) audio = extractAudioFile(path.resolve(cwd, score.input.audio), window);
131
+ else if (footageFile) audio = extractAudio(footageFile, window);
132
+ if (audio) tmpDirs.push(audio.dir);
133
+ let features = null;
134
+ if (audio) {
135
+ const { analyzeAudio, writeFeatures } = await loadAudio();
136
+ features = await analyzeAudio(audio.file, { fps: score.fps });
137
+ await writeFeatures(features, path.join(outDir, "features.json"));
138
+ }
139
+ const stageMode = score.safety.mode === "stage";
140
+ const gate = { ok: true, score, safetyMode: score.safety.mode };
141
+ const master = await pass({ score: stageMode ? score : safeScore(score, lib, features).score, lib, outDir, film: path.join(outDir, "film.mp4"), footageFile, audioWav: audio?.file ?? null, features, width, height, replay });
142
+ gate.render = master.report;
143
+ gate.flash = master.flash;
144
+ gate.footage = master.footage;
145
+ gate.stills = master.eye.write(outDir);
146
+ gate.determinism = master.report.determinism;
147
+ if (master.report.determinism.pass === false) gate.ok = false;
148
+ if (stageMode && master.flash.exceeds) {
149
+ writeFileSync(path.join(outDir, "SAFETY.txt"), safetyText(master.flash, master.report.film));
150
+ remuxComment(master.report.film, FLASH_COMMENT);
151
+ gate.safetyNotice = { file: path.join(outDir, "SAFETY.txt"), mp4Comment: FLASH_COMMENT };
152
+ }
153
+ if (!stageMode && master.flash.exceeds) {
154
+ const held = path.join(outDir, "withheld");
155
+ mkdirSync(held, { recursive: true });
156
+ renameSync(master.report.film, path.join(held, "film.mp4"));
157
+ gate.ok = false;
158
+ gate.withheld = path.join(held, "film.mp4");
159
+ }
160
+ if (!stageMode) gate.substitutions = safeScore(score, lib, features).substitutions;
161
+ if (stageMode && score.safety.safeVariant) {
162
+ const safe = safeScore(score, lib, features);
163
+ const safeDir = path.join(outDir, ".safe");
164
+ const res = await pass({ score: safe.score, lib, outDir: safeDir, film: path.join(outDir, "film.safe.mp4"), footageFile, audioWav: audio?.file ?? null, features, width, height, replay: false, events: master.eye.plan.strips.map((s) => s.centre) });
165
+ rmSync(safeDir, { recursive: true, force: true });
166
+ gate.safeVariant = { film: res.report.film, flash: res.flash, substitutions: safe.substitutions };
167
+ if (res.flash.exceeds) {
168
+ const held = path.join(outDir, "withheld");
169
+ mkdirSync(held, { recursive: true });
170
+ renameSync(res.report.film, path.join(held, "film.safe.mp4"));
171
+ gate.safeVariant.withheld = path.join(held, "film.safe.mp4");
172
+ }
173
+ }
174
+ writeFileSync(path.join(outDir, "CREDIT.txt"), creditText());
175
+ gate.credit = { text: CREDIT_TEXT, file: path.join(outDir, "CREDIT.txt") };
176
+ writeFileSync(path.join(outDir, "gate-report.json"), `${JSON.stringify(gate, null, 2)}\n`);
177
+ return gate;
178
+ } finally {
179
+ for (const d of tmpDirs) rmSync(d, { recursive: true, force: true });
180
+ }
181
+ }
@@ -0,0 +1,112 @@
1
+ // Minimal PNG encode/decode with node:zlib, so stills, masks, sheets and preview frames never need
2
+ // an image dependency. Encodes 8-bit grayscale, RGB and RGBA; decodes non-interlaced 8-bit PNGs.
3
+ import { deflateSync, inflateSync } from "node:zlib";
4
+
5
+ const CRC_TABLE = (() => {
6
+ const table = new Uint32Array(256);
7
+ for (let n = 0; n < 256; n++) {
8
+ let c = n;
9
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
10
+ table[n] = c >>> 0;
11
+ }
12
+ return table;
13
+ })();
14
+
15
+ function crc32(buffers) {
16
+ let c = 0xffffffff;
17
+ for (const buf of buffers) for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
18
+ return (c ^ 0xffffffff) >>> 0;
19
+ }
20
+
21
+ function chunk(type, data) {
22
+ const len = Buffer.alloc(4);
23
+ len.writeUInt32BE(data.length);
24
+ const name = Buffer.from(type, "ascii");
25
+ const crc = Buffer.alloc(4);
26
+ crc.writeUInt32BE(crc32([name, data]));
27
+ return Buffer.concat([len, name, data, crc]);
28
+ }
29
+
30
+ const COLOR_TYPE = { 1: 0, 3: 2, 4: 6 };
31
+
32
+ /** Encode `channels` (1 gray, 3 RGB, 4 RGBA) 8-bit pixels, rows top to bottom. */
33
+ export function encodePng(pixels, width, height, channels = 4, { level = 6 } = {}) {
34
+ const stride = width * channels;
35
+ const raw = Buffer.alloc((stride + 1) * height);
36
+ for (let y = 0; y < height; y++) {
37
+ raw[y * (stride + 1)] = 1; // filter: Sub
38
+ const row = y * stride;
39
+ const out = y * (stride + 1) + 1;
40
+ for (let x = 0; x < stride; x++) raw[out + x] = (pixels[row + x] - (x >= channels ? pixels[row + x - channels] : 0)) & 0xff;
41
+ }
42
+ const ihdr = Buffer.alloc(13);
43
+ ihdr.writeUInt32BE(width, 0);
44
+ ihdr.writeUInt32BE(height, 4);
45
+ ihdr[8] = 8;
46
+ ihdr[9] = COLOR_TYPE[channels];
47
+ return Buffer.concat([
48
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
49
+ chunk("IHDR", ihdr),
50
+ chunk("IDAT", deflateSync(raw, { level })),
51
+ chunk("IEND", Buffer.alloc(0)),
52
+ ]);
53
+ }
54
+
55
+ /** RGBA -> RGB (drops alpha). */
56
+ export function rgbaToRgb(rgba, width, height) {
57
+ const out = Buffer.alloc(width * height * 3);
58
+ for (let i = 0, j = 0; i < rgba.length; i += 4, j += 3) {
59
+ out[j] = rgba[i]; out[j + 1] = rgba[i + 1]; out[j + 2] = rgba[i + 2];
60
+ }
61
+ return out;
62
+ }
63
+
64
+ /** Decode an 8-bit non-interlaced PNG to { width, height, channels, pixels }. */
65
+ export function decodePng(buf) {
66
+ if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error("not a PNG");
67
+ let off = 8;
68
+ let width = 0;
69
+ let height = 0;
70
+ let channels = 0;
71
+ const idat = [];
72
+ while (off < buf.length) {
73
+ const len = buf.readUInt32BE(off);
74
+ const type = buf.toString("ascii", off + 4, off + 8);
75
+ const data = buf.subarray(off + 8, off + 8 + len);
76
+ if (type === "IHDR") {
77
+ width = data.readUInt32BE(0);
78
+ height = data.readUInt32BE(4);
79
+ if (data[8] !== 8 || data[12] !== 0) throw new Error("only 8-bit non-interlaced PNG is supported");
80
+ channels = { 0: 1, 2: 3, 6: 4 }[data[9]];
81
+ if (!channels) throw new Error(`unsupported PNG colour type ${data[9]}`);
82
+ } else if (type === "IDAT") idat.push(data);
83
+ else if (type === "IEND") break;
84
+ off += 12 + len;
85
+ }
86
+ const raw = inflateSync(Buffer.concat(idat));
87
+ const stride = width * channels;
88
+ const pixels = Buffer.alloc(stride * height);
89
+ for (let y = 0; y < height; y++) {
90
+ const filter = raw[y * (stride + 1)];
91
+ const src = y * (stride + 1) + 1;
92
+ const dst = y * stride;
93
+ for (let x = 0; x < stride; x++) {
94
+ const a = x >= channels ? pixels[dst + x - channels] : 0;
95
+ const b = y > 0 ? pixels[dst - stride + x] : 0;
96
+ const c = x >= channels && y > 0 ? pixels[dst - stride + x - channels] : 0;
97
+ let v = raw[src + x];
98
+ if (filter === 1) v += a;
99
+ else if (filter === 2) v += b;
100
+ else if (filter === 3) v += (a + b) >> 1;
101
+ else if (filter === 4) {
102
+ const p = a + b - c;
103
+ const pa = Math.abs(p - a);
104
+ const pb = Math.abs(p - b);
105
+ const pc = Math.abs(p - c);
106
+ v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
107
+ }
108
+ pixels[dst + x] = v & 0xff;
109
+ }
110
+ }
111
+ return { width, height, channels, pixels };
112
+ }
@@ -0,0 +1,161 @@
1
+ // agentvfx (agentvfx licence, see LICENSE). The host side of a render: open the render page in headless Chrome (GPU
2
+ // rung), prepare the templates, step every frame in order, capture, analyse in a worker pool, pipe
3
+ // RGBA to ffmpeg, then replay sampled frames in a fresh browser from frame 0 and compare bytes
4
+ // (determinism; exit 18 semantics after lit-typographic-motion's stage replay).
5
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import path from "node:path";
8
+ import { EXIT } from "../core/constants.mjs";
9
+ import { resolvePalette } from "../core/palettes.mjs";
10
+ import { buildSchedule } from "../core/schedule.mjs";
11
+ import { BlockedError } from "./chrome.mjs";
12
+ import { findFfmpeg, startMaster } from "./encode.mjs";
13
+ import { FramePool, StageBrowser, analyzeChecked } from "./stage-capture.mjs";
14
+ import { seedToUnit } from "../web/prelude.mjs";
15
+ import { CREDIT_OVERLAY } from "../core/mark.mjs";
16
+
17
+ const PAGE = "lit/engine/render.html";
18
+ const srgbToLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
19
+
20
+ /** Passes of one template as the page needs them: [{ file, target, persistent }]. */
21
+ export function templatePasses(tpl) {
22
+ const passes = tpl.fx.PASSES;
23
+ if (!Array.isArray(passes) || passes.length === 0) return [{ file: "main.frag", target: null, persistent: false }];
24
+ return passes.map((p, i) => ({ file: i === passes.length - 1 ? "main.frag" : `pass${i}.frag`, target: i === passes.length - 1 ? null : p.TARGET ?? null, persistent: Boolean(p.PERSISTENT) }));
25
+ }
26
+
27
+ /** Frames the determinism replay samples: first, last and evenly spaced, `n` in all. */
28
+ export function sampleFrames(frameCount, n = 8) {
29
+ if (frameCount <= n) return Array.from({ length: frameCount }, (_, i) => i);
30
+ return [...new Set(Array.from({ length: n }, (_, i) => Math.round((i * (frameCount - 1)) / (n - 1))))];
31
+ }
32
+
33
+ /** The first differing 64x64 block between two RGBA frames, or null. */
34
+ export function firstDifference(a, b, width, height, block = 64) {
35
+ for (let by = 0; by < height; by += block) {
36
+ for (let bx = 0; bx < width; bx += block) {
37
+ for (let y = by; y < Math.min(by + block, height); y++) {
38
+ for (let x = bx; x < Math.min(bx + block, width); x++) {
39
+ const o = (y * width + x) * 4;
40
+ if (a[o] !== b[o] || a[o + 1] !== b[o + 1] || a[o + 2] !== b[o + 2]) return { x: bx, y: by, size: block };
41
+ }
42
+ }
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function pagePrepare({ schedule, lib, width, height, seed, palette, probeNondeterminism, credit = null }) {
49
+ const templates = {};
50
+ for (const id of schedule.templatesUsed) {
51
+ const tpl = lib.get(id);
52
+ templates[id] = { rel: tpl.rel, inputs: tpl.fx.INPUTS ?? [], passes: templatePasses(tpl), text: tpl.fx.X_AGENTVFX.text ?? null };
53
+ }
54
+ return { width, height, fps: schedule.fps, seed01: seedToUnit(seed), palette, templates, probeNondeterminism, credit };
55
+ }
56
+
57
+ function audioFor(features, f) {
58
+ const fr = features?.frames?.[f];
59
+ if (!fr) return null;
60
+ const { xy, ...rest } = fr;
61
+ return rest;
62
+ }
63
+
64
+ async function openRenderer({ prepare, width, height, seed, outDir, log, tag }) {
65
+ const profileDir = mkdtempSync(path.join(tmpdir(), `agentvfx-${tag}-`));
66
+ const browser = await StageBrowser.open({ stageDir: outDir, page: PAGE, prepare, width, height, seed, profileDir, rung: "gpu", log });
67
+ if (!browser.prepared?.ok) {
68
+ await browser.close();
69
+ throw new BlockedError(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: render page failed to prepare: ${browser.prepared?.error ?? "no answer"}`);
70
+ }
71
+ return browser;
72
+ }
73
+
74
+ async function stepFrame(browser, f, schedule, features, footage) {
75
+ if (footage) {
76
+ const frame = await footage.frame(f);
77
+ const route = `/lit/frame/${f}.rgba`;
78
+ browser.dynamic = (p) => (p === route ? { body: frame, type: "application/octet-stream" } : null);
79
+ await browser.evaluate(`window.__litFootageFetch(${JSON.stringify(route)}, ${footage.width}, ${footage.height})`);
80
+ browser.dynamic = null;
81
+ }
82
+ const plan = { layers: schedule.frames[f].layers.map(({ key, template, params, t, labels, textStyle }) => ({ key, template, params, t, labels, textStyle })), audio: audioFor(features, f), footage: Boolean(footage) };
83
+ try {
84
+ await browser.evaluate(`window.__litStep(${f}, ${JSON.stringify(plan)})`);
85
+ } catch (error) {
86
+ throw new BlockedError(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: frame ${f}: ${error.message}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Render a validated score. Options: outDir, width/height (default 1920x1080), features (audio
92
+ * analysis), footage ({ width, height, frame(f) -> Promise<Buffer RGBA> }), audioFile (muxed),
93
+ * replay (default true), probeNondeterminism (test hook), onFrame(f, analysed) for stills/flash.
94
+ * Returns the render report; writes outDir/film.mp4.
95
+ */
96
+ export async function renderScore(score, lib, opts) {
97
+ const { outDir, features = null, footage = null, audioFile = null, replay = true, probeNondeterminism = false, onFrame = null, comment = null, log = () => {} } = opts;
98
+ const width = opts.width ?? footage?.width ?? 1920;
99
+ const height = opts.height ?? footage?.height ?? 1080;
100
+ mkdirSync(outDir, { recursive: true });
101
+ const ffmpeg = findFfmpeg();
102
+ if (!ffmpeg) throw new BlockedError(EXIT.BLOCKED_NO_FFMPEG_FOR_VIDEO, "BLOCKED_NO_FFMPEG_FOR_VIDEO: ffmpeg not found");
103
+ const schedule = buildSchedule(score, lib, { features });
104
+ const palette = resolvePalette(score.palette ?? "rainbow").map((c) => c.map(srgbToLinear));
105
+ const prepare = pagePrepare({ schedule, lib, width, height, seed: score.seed, palette, probeNondeterminism, credit: score.credit?.overlay === false ? null : CREDIT_OVERLAY });
106
+ const samples = sampleFrames(schedule.frameCount);
107
+ const kept = new Map();
108
+ const shas = [];
109
+ const film = path.join(outDir, "film.mp4");
110
+ const pool = new FramePool();
111
+ let browser = null;
112
+ let master = null;
113
+ const t0 = Date.now();
114
+ try {
115
+ browser = await openRenderer({ prepare, width, height, seed: score.seed, outDir, log, tag: "master" });
116
+ const renderer = browser.prepared.renderer;
117
+ master = startMaster({ ffmpeg, width, height, fps: schedule.fps, out: film, audio: audioFile, comment });
118
+ let pending = Promise.resolve();
119
+ for (let f = 0; f < schedule.frameCount; f++) {
120
+ await stepFrame(browser, f, schedule, features, footage);
121
+ const png = await browser.capture();
122
+ const analysed = analyzeChecked(pool, png, width, height);
123
+ const prev = pending;
124
+ pending = (async () => {
125
+ const a = await analysed;
126
+ await prev;
127
+ shas[f] = a.rgbaSha256;
128
+ if (samples.includes(f)) kept.set(f, a.rgba);
129
+ if (onFrame) await onFrame(f, a);
130
+ await master.write(a.rgba);
131
+ })();
132
+ }
133
+ await pending;
134
+ const enc = await master.finish();
135
+ master = null;
136
+ if (enc.code !== 0) throw new Error(`ffmpeg failed (${enc.code}): ${enc.stderr}`);
137
+ await browser.close();
138
+ browser = null;
139
+ const report = { renderer, width, height, fps: schedule.fps, frames: schedule.frameCount, durationSec: schedule.frameCount / schedule.fps, renderMs: Date.now() - t0, events: schedule.events, film };
140
+ if (!replay) return { ...report, determinism: { checked: false } };
141
+ browser = await openRenderer({ prepare, width, height, seed: score.seed, outDir, log, tag: "replay" });
142
+ const last = samples[samples.length - 1];
143
+ for (let f = 0; f <= last; f++) {
144
+ await stepFrame(browser, f, schedule, features, footage);
145
+ if (!samples.includes(f)) continue;
146
+ const a = await analyzeChecked(pool, await browser.capture(), width, height);
147
+ if (a.rgbaSha256 !== shas[f]) {
148
+ return { ...report, determinism: { checked: true, pass: false, sampled: samples, frame: f, firstDifference: firstDifference(kept.get(f), a.rgba, width, height) } };
149
+ }
150
+ }
151
+ return { ...report, determinism: { checked: true, pass: true, sampled: samples } };
152
+ } finally {
153
+ if (master) await master.finish().catch(() => {});
154
+ if (browser) await browser.close().catch(() => {});
155
+ await pool.close();
156
+ }
157
+ }
158
+
159
+ export function cleanDir(dir) {
160
+ rmSync(dir, { recursive: true, force: true });
161
+ }