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.
- package/AGENTS.md +8 -0
- package/GEMINI.md +1 -0
- package/LICENSE +172 -0
- package/README.md +236 -2
- package/package.json +35 -4
- package/skills/agentvfx/LICENSE +172 -0
- package/skills/agentvfx/NOTICE +44 -0
- package/skills/agentvfx/SKILL.md +90 -0
- package/skills/agentvfx/engine/core/constants.mjs +84 -0
- package/skills/agentvfx/engine/core/flash-constants.mjs +23 -0
- package/skills/agentvfx/engine/core/flash.mjs +240 -0
- package/skills/agentvfx/engine/core/mark.mjs +28 -0
- package/skills/agentvfx/engine/core/palettes.mjs +36 -0
- package/skills/agentvfx/engine/core/schedule.mjs +85 -0
- package/skills/agentvfx/engine/core/templates.mjs +43 -0
- package/skills/agentvfx/engine/core/util.mjs +167 -0
- package/skills/agentvfx/engine/core/validate.mjs +334 -0
- package/skills/agentvfx/engine/node/analysis.mjs +172 -0
- package/skills/agentvfx/engine/node/audio.mjs +621 -0
- package/skills/agentvfx/engine/node/board.mjs +24 -0
- package/skills/agentvfx/engine/node/chrome.mjs +349 -0
- package/skills/agentvfx/engine/node/encode.mjs +110 -0
- package/skills/agentvfx/engine/node/eye.mjs +77 -0
- package/skills/agentvfx/engine/node/footage.mjs +129 -0
- package/skills/agentvfx/engine/node/frame-worker.mjs +39 -0
- package/skills/agentvfx/engine/node/host.mjs +31 -0
- package/skills/agentvfx/engine/node/job.mjs +181 -0
- package/skills/agentvfx/engine/node/png.mjs +112 -0
- package/skills/agentvfx/engine/node/render.mjs +161 -0
- package/skills/agentvfx/engine/node/stage-capture.mjs +187 -0
- package/skills/agentvfx/engine/node/stage-serve.mjs +72 -0
- package/skills/agentvfx/engine/node/stills.mjs +100 -0
- package/skills/agentvfx/engine/node/ws-server.mjs +132 -0
- package/skills/agentvfx/engine/web/gl.mjs +319 -0
- package/skills/agentvfx/engine/web/glsl-common.mjs +78 -0
- package/skills/agentvfx/engine/web/init.js +23 -0
- package/skills/agentvfx/engine/web/prelude.mjs +150 -0
- package/skills/agentvfx/engine/web/render.html +11 -0
- package/skills/agentvfx/engine/web/render.mjs +388 -0
- package/skills/agentvfx/engine/web/scale.mjs +15 -0
- package/skills/agentvfx/engine/web/text.mjs +158 -0
- package/skills/agentvfx/plates/terminalcore/README.md +61 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/crt.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/font.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/gallery.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/gallery_mats.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/gallery_room.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/materials.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/nodekit.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/room.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/shots.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/textures.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/__pycache__/wall.cpython-313.pyc +0 -0
- package/skills/agentvfx/plates/terminalcore/crt.py +143 -0
- package/skills/agentvfx/plates/terminalcore/font.py +80 -0
- package/skills/agentvfx/plates/terminalcore/gallery.py +61 -0
- package/skills/agentvfx/plates/terminalcore/gallery_mats.py +144 -0
- package/skills/agentvfx/plates/terminalcore/gallery_room.py +126 -0
- package/skills/agentvfx/plates/terminalcore/materials.py +170 -0
- package/skills/agentvfx/plates/terminalcore/nodekit.py +87 -0
- package/skills/agentvfx/plates/terminalcore/room.py +82 -0
- package/skills/agentvfx/plates/terminalcore/scene.py +219 -0
- package/skills/agentvfx/plates/terminalcore/shots.py +48 -0
- package/skills/agentvfx/plates/terminalcore/textures.py +79 -0
- package/skills/agentvfx/plates/terminalcore/wall.py +103 -0
- package/skills/agentvfx/references/plates.md +22 -0
- package/skills/agentvfx/references/template-format.md +149 -0
- package/skills/agentvfx/references/web.md +63 -0
- package/skills/agentvfx/runtime/agentvfx.js +708 -0
- package/skills/agentvfx/schemas/fx-score.schema.json +98 -0
- package/skills/agentvfx/scripts/export-web.mjs +155 -0
- package/skills/agentvfx/scripts/loop.mjs +129 -0
- package/skills/agentvfx/scripts/make-beat.mjs +133 -0
- package/skills/agentvfx/scripts/plate.mjs +82 -0
- package/skills/agentvfx/scripts/vfx.mjs +173 -0
- package/skills/agentvfx/templates/_test/feedback-counter/fx.json +11 -0
- package/skills/agentvfx/templates/_test/feedback-counter/main.frag +4 -0
- package/skills/agentvfx/templates/_test/feedback-counter/pass0.frag +4 -0
- package/skills/agentvfx/templates/_test/gradient/fx.json +23 -0
- package/skills/agentvfx/templates/_test/gradient/main.frag +5 -0
- package/skills/agentvfx/templates/_test/identity/fx.json +10 -0
- package/skills/agentvfx/templates/_test/identity/main.frag +4 -0
- package/skills/agentvfx/templates/_test/strobe/fx.json +14 -0
- package/skills/agentvfx/templates/_test/strobe/main.frag +6 -0
- package/skills/agentvfx/templates/_test/text-probe/fx.json +15 -0
- package/skills/agentvfx/templates/_test/text-probe/main.frag +6 -0
- package/skills/agentvfx/templates/brutal/NOTES.md +14 -0
- package/skills/agentvfx/templates/brutal/_src/boot-log/def.mjs +57 -0
- package/skills/agentvfx/templates/brutal/_src/boot-log/main.src.frag +129 -0
- package/skills/agentvfx/templates/brutal/_src/build.mjs +33 -0
- package/skills/agentvfx/templates/brutal/_src/halftone-field/def.mjs +72 -0
- package/skills/agentvfx/templates/brutal/_src/halftone-field/main.src.frag +94 -0
- package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-left.src.glsl +100 -0
- package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-right.src.glsl +61 -0
- package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-tiles.src.glsl +153 -0
- package/skills/agentvfx/templates/brutal/_src/hud-deck/def.mjs +82 -0
- package/skills/agentvfx/templates/brutal/_src/hud-deck/main.src.frag +110 -0
- package/skills/agentvfx/templates/brutal/_src/hud-deck/pass0.src.frag +38 -0
- package/skills/agentvfx/templates/brutal/_src/shared/defs.mjs +28 -0
- package/skills/agentvfx/templates/brutal/_src/shared/icons.glsl +72 -0
- package/skills/agentvfx/templates/brutal/_src/shared/lib.glsl +115 -0
- package/skills/agentvfx/templates/brutal/_src/shared/screen.glsl +43 -0
- package/skills/agentvfx/templates/brutal/boot-log/demo-acid.json +1 -0
- package/skills/agentvfx/templates/brutal/boot-log/demo-phosphor.json +1 -0
- package/skills/agentvfx/templates/brutal/boot-log/demo-ultramarine.json +1 -0
- package/skills/agentvfx/templates/brutal/boot-log/fx.json +257 -0
- package/skills/agentvfx/templates/brutal/boot-log/main.frag +242 -0
- package/skills/agentvfx/templates/brutal/collage/_src/00-lib.glsl +73 -0
- package/skills/agentvfx/templates/brutal/collage/_src/10-spectrum-scope.glsl +73 -0
- package/skills/agentvfx/templates/brutal/collage/_src/20-meter-readout-cross.glsl +87 -0
- package/skills/agentvfx/templates/brutal/collage/_src/30-footage-fillers.glsl +92 -0
- package/skills/agentvfx/templates/brutal/collage/_src/40-editorial.glsl +232 -0
- package/skills/agentvfx/templates/brutal/collage/_src/50-typefx.glsl +163 -0
- package/skills/agentvfx/templates/brutal/collage/_src/60-scene-lib.glsl +104 -0
- package/skills/agentvfx/templates/brutal/collage/_src/61-scene-specimen.glsl +71 -0
- package/skills/agentvfx/templates/brutal/collage/_src/62-scene-wiring.glsl +96 -0
- package/skills/agentvfx/templates/brutal/collage/_src/63-scene-fieldcut.glsl +119 -0
- package/skills/agentvfx/templates/brutal/collage/_src/64-scene-pattern.glsl +64 -0
- package/skills/agentvfx/templates/brutal/collage/_src/641-scene-anatomy.glsl +49 -0
- package/skills/agentvfx/templates/brutal/collage/_src/642-scene-deck.glsl +78 -0
- package/skills/agentvfx/templates/brutal/collage/_src/643-scene-bootlog.glsl +60 -0
- package/skills/agentvfx/templates/brutal/collage/_src/644-scene-tunnel.glsl +48 -0
- package/skills/agentvfx/templates/brutal/collage/_src/645-scene-runner.glsl +42 -0
- package/skills/agentvfx/templates/brutal/collage/_src/646-scene-ascii.glsl +66 -0
- package/skills/agentvfx/templates/brutal/collage/_src/647-scene-cubewall.glsl +80 -0
- package/skills/agentvfx/templates/brutal/collage/_src/648-scene-vortex.glsl +46 -0
- package/skills/agentvfx/templates/brutal/collage/_src/649-scene-sticker.glsl +72 -0
- package/skills/agentvfx/templates/brutal/collage/_src/65-scene-main.glsl +1 -0
- package/skills/agentvfx/templates/brutal/collage/_src/650-scene-ribbon.glsl +55 -0
- package/skills/agentvfx/templates/brutal/collage/_src/651-scene-slab.glsl +38 -0
- package/skills/agentvfx/templates/brutal/collage/_src/652-scene-framedial.glsl +79 -0
- package/skills/agentvfx/templates/brutal/collage/_src/653-scene-tilt.glsl +40 -0
- package/skills/agentvfx/templates/brutal/collage/_src/654-scene-marquee.glsl +45 -0
- package/skills/agentvfx/templates/brutal/collage/_src/655-scene-lens.glsl +57 -0
- package/skills/agentvfx/templates/brutal/collage/_src/656-scene-liquid.glsl +35 -0
- package/skills/agentvfx/templates/brutal/collage/_src/657-scene-panelgrid.glsl +75 -0
- package/skills/agentvfx/templates/brutal/collage/_src/658-scene-mirror.glsl +49 -0
- package/skills/agentvfx/templates/brutal/collage/_src/66-scene-main.glsl +27 -0
- package/skills/agentvfx/templates/brutal/collage/_src/90-main.glsl +120 -0
- package/skills/agentvfx/templates/brutal/collage/_src/build.mjs +24 -0
- package/skills/agentvfx/templates/brutal/collage/_src/sync-presets.mjs +16 -0
- package/skills/agentvfx/templates/brutal/collage/demo-bone-lime.json +1 -0
- package/skills/agentvfx/templates/brutal/collage/demo-lime-sparse.json +1 -0
- package/skills/agentvfx/templates/brutal/collage/demo-orange-dense.json +1 -0
- package/skills/agentvfx/templates/brutal/collage/demo-ultra-slow.json +1 -0
- package/skills/agentvfx/templates/brutal/collage/fx.json +4882 -0
- package/skills/agentvfx/templates/brutal/collage/layout.mjs +97 -0
- package/skills/agentvfx/templates/brutal/collage/layouts.json +366 -0
- package/skills/agentvfx/templates/brutal/collage/main.frag +2367 -0
- package/skills/agentvfx/templates/brutal/collage/pass0.frag +265 -0
- package/skills/agentvfx/templates/brutal/halftone-field/demo-acid.json +1 -0
- package/skills/agentvfx/templates/brutal/halftone-field/demo-ascii.json +1 -0
- package/skills/agentvfx/templates/brutal/halftone-field/demo-dither.json +1 -0
- package/skills/agentvfx/templates/brutal/halftone-field/demo-orange.json +1 -0
- package/skills/agentvfx/templates/brutal/halftone-field/demo-phosphor.json +1 -0
- package/skills/agentvfx/templates/brutal/halftone-field/demo-ultramarine.json +1 -0
- package/skills/agentvfx/templates/brutal/halftone-field/fx.json +474 -0
- package/skills/agentvfx/templates/brutal/halftone-field/main.frag +248 -0
- package/skills/agentvfx/templates/brutal/hud-deck/demo-acid.json +1 -0
- package/skills/agentvfx/templates/brutal/hud-deck/demo-night.json +1 -0
- package/skills/agentvfx/templates/brutal/hud-deck/demo-octagon.json +1 -0
- package/skills/agentvfx/templates/brutal/hud-deck/demo-ultramarine.json +1 -0
- package/skills/agentvfx/templates/brutal/hud-deck/fx.json +601 -0
- package/skills/agentvfx/templates/brutal/hud-deck/main.frag +645 -0
- package/skills/agentvfx/templates/brutal/hud-deck/pass0.frag +38 -0
- package/skills/agentvfx/templates/brutal/lattice/demo-cobalt-fine.json +1 -0
- package/skills/agentvfx/templates/brutal/lattice/demo-ink-yellow.json +1 -0
- package/skills/agentvfx/templates/brutal/lattice/demo-vermilion.json +1 -0
- package/skills/agentvfx/templates/brutal/lattice/demo-violet-slow.json +1 -0
- package/skills/agentvfx/templates/brutal/lattice/fx.json +130 -0
- package/skills/agentvfx/templates/brutal/lattice/main.frag +175 -0
- package/skills/agentvfx/templates/brutal/lattice/pass0.frag +29 -0
- package/skills/agentvfx/templates/glitch/block-glitch/demo-mosaic.json +1 -0
- package/skills/agentvfx/templates/glitch/block-glitch/demo-onset.json +1 -0
- package/skills/agentvfx/templates/glitch/block-glitch/demo-static.json +1 -0
- package/skills/agentvfx/templates/glitch/block-glitch/demo-tear.json +1 -0
- package/skills/agentvfx/templates/glitch/block-glitch/fx.json +54 -0
- package/skills/agentvfx/templates/glitch/block-glitch/main.frag +132 -0
- package/skills/agentvfx/templates/glitch/block-glitch/pass0.frag +12 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/NOTES.md +13 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/build-passes.mjs +162 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/demo-chunk.json +1 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/demo-hue.json +1 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/demo-kick.json +1 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/demo-streak.json +1 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/demo-vertical.json +1 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/fx.json +60 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/main.frag +83 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass0.frag +64 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass1.frag +66 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass2.frag +66 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass3.frag +66 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass4.frag +66 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass5.frag +66 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass6.frag +66 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass7.frag +66 -0
- package/skills/agentvfx/templates/glitch/pixel-sort/pass8.frag +66 -0
- package/skills/agentvfx/templates/glitch/rgb-split/demo-diagonal-stutter.json +1 -0
- package/skills/agentvfx/templates/glitch/rgb-split/demo-hard.json +1 -0
- package/skills/agentvfx/templates/glitch/rgb-split/demo-kick.json +1 -0
- package/skills/agentvfx/templates/glitch/rgb-split/demo-radial.json +1 -0
- package/skills/agentvfx/templates/glitch/rgb-split/fx.json +46 -0
- package/skills/agentvfx/templates/glitch/rgb-split/main.frag +76 -0
- package/skills/agentvfx/templates/glitch/vhs/demo-clean.json +1 -0
- package/skills/agentvfx/templates/glitch/vhs/demo-crash.json +1 -0
- package/skills/agentvfx/templates/glitch/vhs/demo-tape.json +1 -0
- package/skills/agentvfx/templates/glitch/vhs/demo-worn.json +1 -0
- package/skills/agentvfx/templates/glitch/vhs/fx.json +46 -0
- package/skills/agentvfx/templates/glitch/vhs/main.frag +96 -0
- package/skills/agentvfx/templates/impact/impact-1bit/demo-onebit-bayer.json +1 -0
- package/skills/agentvfx/templates/impact/impact-1bit/fx.json +45 -0
- package/skills/agentvfx/templates/impact/impact-1bit/main.frag +64 -0
- package/skills/agentvfx/templates/impact/impact-invert/demo-invert-snap.json +1 -0
- package/skills/agentvfx/templates/impact/impact-invert/fx.json +37 -0
- package/skills/agentvfx/templates/impact/impact-invert/main.frag +44 -0
- package/skills/agentvfx/templates/impact/punch-shake/demo-punch-hard.json +1 -0
- package/skills/agentvfx/templates/impact/punch-shake/fx.json +36 -0
- package/skills/agentvfx/templates/impact/punch-shake/main.frag +62 -0
- package/skills/agentvfx/templates/impact/rgb-hit/demo-rgb-slam.json +1 -0
- package/skills/agentvfx/templates/impact/rgb-hit/fx.json +39 -0
- package/skills/agentvfx/templates/impact/rgb-hit/main.frag +62 -0
- package/skills/agentvfx/templates/impact/signal-cut/demo-lime-hold.json +1 -0
- package/skills/agentvfx/templates/impact/signal-cut/demo-lime.json +1 -0
- package/skills/agentvfx/templates/impact/signal-cut/demo-orange.json +1 -0
- package/skills/agentvfx/templates/impact/signal-cut/demo-ultramarine.json +1 -0
- package/skills/agentvfx/templates/impact/signal-cut/fx.json +83 -0
- package/skills/agentvfx/templates/impact/signal-cut/main.frag +101 -0
- package/skills/agentvfx/templates/impact/signal-cut/pass0.frag +68 -0
- package/skills/agentvfx/templates/impact/speedline-burst/demo-speed-white.json +1 -0
- package/skills/agentvfx/templates/impact/speedline-burst/fx.json +44 -0
- package/skills/agentvfx/templates/impact/speedline-burst/main.frag +68 -0
- package/skills/agentvfx/templates/impact/strobe-burst/demo-strobe-white-16.json +1 -0
- package/skills/agentvfx/templates/impact/strobe-burst/fx.json +42 -0
- package/skills/agentvfx/templates/impact/strobe-burst/main.frag +43 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/demo-anamorphic-cinema.json +10 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/demo-data.json +10 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/fx.json +47 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/main.frag +45 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/pass0.frag +22 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/pass1.frag +16 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/pass2.frag +16 -0
- package/skills/agentvfx/templates/light/anamorphic-streak/pass3.frag +16 -0
- package/skills/agentvfx/templates/light/bloom-glow/demo-clip01-low.json +11 -0
- package/skills/agentvfx/templates/light/bloom-glow/demo-neon-night.json +10 -0
- package/skills/agentvfx/templates/light/bloom-glow/demo-phosphor.json +11 -0
- package/skills/agentvfx/templates/light/bloom-glow/fx.json +53 -0
- package/skills/agentvfx/templates/light/bloom-glow/main.frag +85 -0
- package/skills/agentvfx/templates/light/bloom-glow/pass0.frag +21 -0
- package/skills/agentvfx/templates/light/bloom-glow/pass1.frag +50 -0
- package/skills/agentvfx/templates/light/bloom-glow/pass2.frag +44 -0
- package/skills/agentvfx/templates/light/bloom-glow/pass3.frag +32 -0
- package/skills/agentvfx/templates/light/bloom-glow/pass4.frag +48 -0
- package/skills/agentvfx/templates/light/bloom-glow/pass5.frag +46 -0
- package/skills/agentvfx/templates/light/god-rays/demo-golden-hour.json +10 -0
- package/skills/agentvfx/templates/light/god-rays/demo-point-lamp.json +10 -0
- package/skills/agentvfx/templates/light/god-rays/fx.json +39 -0
- package/skills/agentvfx/templates/light/god-rays/main.frag +54 -0
- package/skills/agentvfx/templates/light/lens-flare/demo-clip01-bar.json +11 -0
- package/skills/agentvfx/templates/light/lens-flare/demo-eye-bar.json +10 -0
- package/skills/agentvfx/templates/light/lens-flare/demo-sun-ghosts.json +10 -0
- package/skills/agentvfx/templates/light/lens-flare/fx.json +49 -0
- package/skills/agentvfx/templates/light/lens-flare/main.frag +86 -0
- package/skills/agentvfx/templates/light/lens-flare/pass0.frag +24 -0
- package/skills/agentvfx/templates/light/light-leak/demo-slit.json +10 -0
- package/skills/agentvfx/templates/light/light-leak/demo-vintage-burn.json +10 -0
- package/skills/agentvfx/templates/light/light-leak/fx.json +40 -0
- package/skills/agentvfx/templates/light/light-leak/main.frag +46 -0
- package/skills/agentvfx/templates/light/light-stage/fx.json +29 -0
- package/skills/agentvfx/templates/light/light-stage/main.frag +56 -0
- package/skills/agentvfx/templates/light/light-sweep/demo-hard-bar.json +12 -0
- package/skills/agentvfx/templates/light/light-sweep/demo-logo-sweep.json +10 -0
- package/skills/agentvfx/templates/light/light-sweep/demo-onset.json +11 -0
- package/skills/agentvfx/templates/light/light-sweep/fx.json +45 -0
- package/skills/agentvfx/templates/light/light-sweep/main.frag +29 -0
- package/skills/agentvfx/templates/light/light-sweep/pass0.frag +17 -0
- package/skills/agentvfx/templates/light/light-sweep/pass1.frag +15 -0
- package/skills/agentvfx/templates/light/neon-saber/demo-hairline.json +10 -0
- package/skills/agentvfx/templates/light/neon-saber/demo-saber-laser.json +10 -0
- package/skills/agentvfx/templates/light/neon-saber/fx.json +48 -0
- package/skills/agentvfx/templates/light/neon-saber/main.frag +27 -0
- package/skills/agentvfx/templates/light/neon-saber/pass0.frag +46 -0
- package/skills/agentvfx/templates/light/neon-saber/pass1.frag +15 -0
- package/skills/agentvfx/templates/psychedelic/echo-flow/demo-trail.json +1 -0
- package/skills/agentvfx/templates/psychedelic/echo-flow/fx.json +38 -0
- package/skills/agentvfx/templates/psychedelic/echo-flow/main.frag +19 -0
- package/skills/agentvfx/templates/psychedelic/echo-flow/pass0.frag +16 -0
- package/skills/agentvfx/templates/psychedelic/echo-flow/pass1.frag +20 -0
- package/skills/agentvfx/templates/psychedelic/echo-flow/pass2.frag +11 -0
- package/skills/agentvfx/templates/psychedelic/echo-flow/pass3.frag +4 -0
- package/skills/agentvfx/templates/psychedelic/hologram-scan/demo-cyan.json +1 -0
- package/skills/agentvfx/templates/psychedelic/hologram-scan/fx.json +36 -0
- package/skills/agentvfx/templates/psychedelic/hologram-scan/main.frag +30 -0
- package/skills/agentvfx/templates/psychedelic/kaleido-fold/demo-six.json +1 -0
- package/skills/agentvfx/templates/psychedelic/kaleido-fold/fx.json +37 -0
- package/skills/agentvfx/templates/psychedelic/kaleido-fold/main.frag +10 -0
- package/skills/agentvfx/templates/psychedelic/kaleido-fold/pass0.frag +34 -0
- package/skills/agentvfx/templates/psychedelic/mosh-melt/demo-classic.json +1 -0
- package/skills/agentvfx/templates/psychedelic/mosh-melt/fx.json +38 -0
- package/skills/agentvfx/templates/psychedelic/mosh-melt/main.frag +20 -0
- package/skills/agentvfx/templates/psychedelic/mosh-melt/pass0.frag +22 -0
- package/skills/agentvfx/templates/psychedelic/sacred/NOTES.md +13 -0
- package/skills/agentvfx/templates/psychedelic/sacred/demo-classic.json +1 -0
- package/skills/agentvfx/templates/psychedelic/sacred/demo-halo.json +1 -0
- package/skills/agentvfx/templates/psychedelic/sacred/demo-slow-tide.json +1 -0
- package/skills/agentvfx/templates/psychedelic/sacred/demo-thermal.json +1 -0
- package/skills/agentvfx/templates/psychedelic/sacred/fx.json +263 -0
- package/skills/agentvfx/templates/psychedelic/sacred/main.frag +109 -0
- package/skills/agentvfx/templates/psychedelic/sacred/pass0.frag +25 -0
- package/skills/agentvfx/templates/psychedelic/sacred/pass1.frag +23 -0
- package/skills/agentvfx/templates/psychedelic/tidal-bands/demo-classic.json +1 -0
- package/skills/agentvfx/templates/psychedelic/tidal-bands/fx.json +37 -0
- package/skills/agentvfx/templates/psychedelic/tidal-bands/main.frag +54 -0
- package/skills/agentvfx/templates/scope/_chrome.glsl +242 -0
- package/skills/agentvfx/templates/scope/goniometer/demo-boot-green.json +1 -0
- package/skills/agentvfx/templates/scope/goniometer/demo-field-lime.json +1 -0
- package/skills/agentvfx/templates/scope/goniometer/demo-field-ultramarine.json +1 -0
- package/skills/agentvfx/templates/scope/goniometer/demo-glow-phosphor.json +1 -0
- package/skills/agentvfx/templates/scope/goniometer/demo-hud-black.json +1 -0
- package/skills/agentvfx/templates/scope/goniometer/fx.json +635 -0
- package/skills/agentvfx/templates/scope/goniometer/main.frag +33 -0
- package/skills/agentvfx/templates/scope/goniometer/pass0.frag +58 -0
- package/skills/agentvfx/templates/scope/goniometer/pass1.frag +43 -0
- package/skills/agentvfx/templates/scope/goniometer/pass2.frag +12 -0
- package/skills/agentvfx/templates/scope/goniometer/pass3.frag +12 -0
- package/skills/agentvfx/templates/scope/goniometer/pass4.frag +12 -0
- package/skills/agentvfx/templates/scope/goniometer/pass5.frag +12 -0
- package/skills/agentvfx/templates/scope/goniometer/pass6.frag +251 -0
- package/skills/agentvfx/templates/scope/scope-polar/demo-boot-green.json +1 -0
- package/skills/agentvfx/templates/scope/scope-polar/demo-field-lime.json +1 -0
- package/skills/agentvfx/templates/scope/scope-polar/demo-field-ultramarine.json +1 -0
- package/skills/agentvfx/templates/scope/scope-polar/demo-glow-phosphor.json +1 -0
- package/skills/agentvfx/templates/scope/scope-polar/demo-hud-black.json +1 -0
- package/skills/agentvfx/templates/scope/scope-polar/fx.json +686 -0
- package/skills/agentvfx/templates/scope/scope-polar/main.frag +39 -0
- package/skills/agentvfx/templates/scope/scope-polar/pass0.frag +84 -0
- package/skills/agentvfx/templates/scope/scope-polar/pass1.frag +49 -0
- package/skills/agentvfx/templates/scope/scope-polar/pass2.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-polar/pass3.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-polar/pass4.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-polar/pass5.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-polar/pass6.frag +251 -0
- package/skills/agentvfx/templates/scope/scope-triggered/demo-boot-green.json +1 -0
- package/skills/agentvfx/templates/scope/scope-triggered/demo-field-lime.json +1 -0
- package/skills/agentvfx/templates/scope/scope-triggered/demo-field-ultramarine.json +1 -0
- package/skills/agentvfx/templates/scope/scope-triggered/demo-glow-phosphor.json +1 -0
- package/skills/agentvfx/templates/scope/scope-triggered/demo-hud-black.json +1 -0
- package/skills/agentvfx/templates/scope/scope-triggered/fx.json +712 -0
- package/skills/agentvfx/templates/scope/scope-triggered/main.frag +36 -0
- package/skills/agentvfx/templates/scope/scope-triggered/pass0.frag +78 -0
- package/skills/agentvfx/templates/scope/scope-triggered/pass1.frag +50 -0
- package/skills/agentvfx/templates/scope/scope-triggered/pass2.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-triggered/pass3.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-triggered/pass4.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-triggered/pass5.frag +12 -0
- package/skills/agentvfx/templates/scope/scope-triggered/pass6.frag +251 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-boot-green.json +1 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-field-lime.json +1 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-field-ultramarine.json +1 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-glow-phosphor.json +1 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-hud-black.json +1 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/fx.json +595 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/main.frag +83 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/pass0.frag +52 -0
- package/skills/agentvfx/templates/scope/spectrogram-scroll/pass1.frag +251 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/demo-boot-green.json +1 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/demo-field-lime.json +1 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/demo-field-ultramarine.json +1 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/demo-glow-phosphor.json +1 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/demo-hud-black.json +1 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/fx.json +642 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/main.frag +33 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/pass0.frag +56 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/pass1.frag +44 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/pass2.frag +12 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/pass3.frag +12 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/pass4.frag +12 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/pass5.frag +12 -0
- package/skills/agentvfx/templates/scope/xy-phosphor/pass6.frag +251 -0
- package/skills/agentvfx/templates/spectrum/_src/bars-linear/fx.src.json +61 -0
- package/skills/agentvfx/templates/spectrum/_src/bars-linear/main.src.frag +112 -0
- package/skills/agentvfx/templates/spectrum/_src/bars-linear/pass0.src.frag +2 -0
- package/skills/agentvfx/templates/spectrum/_src/bars-mirror/fx.src.json +51 -0
- package/skills/agentvfx/templates/spectrum/_src/bars-mirror/main.src.frag +72 -0
- package/skills/agentvfx/templates/spectrum/_src/bars-mirror/pass0.src.frag +2 -0
- package/skills/agentvfx/templates/spectrum/_src/bass-circle/fx.src.json +56 -0
- package/skills/agentvfx/templates/spectrum/_src/bass-circle/main.src.frag +136 -0
- package/skills/agentvfx/templates/spectrum/_src/bass-circle/pass0.src.frag +2 -0
- package/skills/agentvfx/templates/spectrum/_src/build.mjs +68 -0
- package/skills/agentvfx/templates/spectrum/_src/label-bars/fx.src.json +54 -0
- package/skills/agentvfx/templates/spectrum/_src/label-bars/main.src.frag +102 -0
- package/skills/agentvfx/templates/spectrum/_src/label-bars/pass0.src.frag +2 -0
- package/skills/agentvfx/templates/spectrum/_src/shared/bars-fn.glsl +64 -0
- package/skills/agentvfx/templates/spectrum/_src/shared/bars-pass.glsl +22 -0
- package/skills/agentvfx/templates/spectrum/_src/shared/clock.glsl +7 -0
- package/skills/agentvfx/templates/spectrum/_src/shared/hud.glsl +50 -0
- package/skills/agentvfx/templates/spectrum/_src/shared/stage.glsl +74 -0
- package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/fx.src.json +58 -0
- package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/main.src.frag +119 -0
- package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/pass0.src.frag +33 -0
- package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/fx.src.json +56 -0
- package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/main.src.frag +95 -0
- package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/pass0.src.frag +2 -0
- package/skills/agentvfx/templates/spectrum/_src/waveform-line/fx.src.json +60 -0
- package/skills/agentvfx/templates/spectrum/_src/waveform-line/main.src.frag +98 -0
- package/skills/agentvfx/templates/spectrum/_src/waveform-line/pass0.src.frag +43 -0
- package/skills/agentvfx/templates/spectrum/bars-linear/demo-graph-acid.json +1 -0
- package/skills/agentvfx/templates/spectrum/bars-linear/demo-graph-black.json +1 -0
- package/skills/agentvfx/templates/spectrum/bars-linear/fx.json +632 -0
- package/skills/agentvfx/templates/spectrum/bars-linear/main.frag +233 -0
- package/skills/agentvfx/templates/spectrum/bars-linear/pass0.frag +91 -0
- package/skills/agentvfx/templates/spectrum/bars-mirror/demo-field-acid.json +1 -0
- package/skills/agentvfx/templates/spectrum/bars-mirror/demo-field-ultramarine.json +1 -0
- package/skills/agentvfx/templates/spectrum/bars-mirror/fx.json +539 -0
- package/skills/agentvfx/templates/spectrum/bars-mirror/main.frag +193 -0
- package/skills/agentvfx/templates/spectrum/bars-mirror/pass0.frag +91 -0
- package/skills/agentvfx/templates/spectrum/bass-circle/demo-bracket-black.json +1 -0
- package/skills/agentvfx/templates/spectrum/bass-circle/demo-bracket-ultramarine.json +1 -0
- package/skills/agentvfx/templates/spectrum/bass-circle/fx.json +552 -0
- package/skills/agentvfx/templates/spectrum/bass-circle/main.frag +258 -0
- package/skills/agentvfx/templates/spectrum/bass-circle/pass0.frag +91 -0
- package/skills/agentvfx/templates/spectrum/label-bars/demo-deck-black.json +1 -0
- package/skills/agentvfx/templates/spectrum/label-bars/demo-deck-ultramarine.json +1 -0
- package/skills/agentvfx/templates/spectrum/label-bars/fx.json +552 -0
- package/skills/agentvfx/templates/spectrum/label-bars/main.frag +223 -0
- package/skills/agentvfx/templates/spectrum/label-bars/pass0.frag +91 -0
- package/skills/agentvfx/templates/spectrum/spectrum-particles/demo-cloud-black.json +1 -0
- package/skills/agentvfx/templates/spectrum/spectrum-particles/demo-cloud-ultramarine.json +1 -0
- package/skills/agentvfx/templates/spectrum/spectrum-particles/fx.json +584 -0
- package/skills/agentvfx/templates/spectrum/spectrum-particles/main.frag +240 -0
- package/skills/agentvfx/templates/spectrum/spectrum-particles/pass0.frag +101 -0
- package/skills/agentvfx/templates/spectrum/spectrum-radial/demo-starburst-black.json +1 -0
- package/skills/agentvfx/templates/spectrum/spectrum-radial/demo-starburst-ultramarine.json +1 -0
- package/skills/agentvfx/templates/spectrum/spectrum-radial/fx.json +566 -0
- package/skills/agentvfx/templates/spectrum/spectrum-radial/main.frag +217 -0
- package/skills/agentvfx/templates/spectrum/spectrum-radial/pass0.frag +91 -0
- package/skills/agentvfx/templates/spectrum/waveform-line/demo-strip-acid.json +1 -0
- package/skills/agentvfx/templates/spectrum/waveform-line/demo-strip-black.json +1 -0
- package/skills/agentvfx/templates/spectrum/waveform-line/fx.json +481 -0
- package/skills/agentvfx/templates/spectrum/waveform-line/main.frag +219 -0
- package/skills/agentvfx/templates/spectrum/waveform-line/pass0.frag +48 -0
- package/skills/agentvfx/templates/spectrum/waveform-line/pass1.frag +86 -0
- package/skills/agentvfx/test/_render_light.mjs +38 -0
- package/skills/agentvfx/test/audio-stereo.test.mjs +45 -0
- package/skills/agentvfx/test/audio.test.mjs +257 -0
- package/skills/agentvfx/test/brutal.test.mjs +190 -0
- package/skills/agentvfx/test/concept-collage-layout.test.mjs +236 -0
- package/skills/agentvfx/test/concept-collage-scenes.test.mjs +305 -0
- package/skills/agentvfx/test/concept-collage-typefx.test.mjs +153 -0
- package/skills/agentvfx/test/concept-collage.test.mjs +84 -0
- package/skills/agentvfx/test/concept-lattice.test.mjs +133 -0
- package/skills/agentvfx/test/concept-signal-cut.test.mjs +172 -0
- package/skills/agentvfx/test/credit-overlay.test.mjs +58 -0
- package/skills/agentvfx/test/credit.test.mjs +42 -0
- package/skills/agentvfx/test/doctor.test.mjs +31 -0
- package/skills/agentvfx/test/encode.test.mjs +22 -0
- package/skills/agentvfx/test/fixtures/r7-collage-LAYOUTS.json +5780 -0
- package/skills/agentvfx/test/fixtures/score-bad-duration.json +7 -0
- package/skills/agentvfx/test/fixtures/score-bad-safety.json +8 -0
- package/skills/agentvfx/test/fixtures/score-identity-footage.json +8 -0
- package/skills/agentvfx/test/fixtures/score-missing-template.json +7 -0
- package/skills/agentvfx/test/fixtures/score-ok.json +8 -0
- package/skills/agentvfx/test/fixtures/score-strobe-12hz.json +8 -0
- package/skills/agentvfx/test/fixtures/score-text-probe.json +1 -0
- package/skills/agentvfx/test/flash-audit.test.mjs +150 -0
- package/skills/agentvfx/test/flash.test.mjs +83 -0
- package/skills/agentvfx/test/footage.test.mjs +88 -0
- package/skills/agentvfx/test/glitch.test.mjs +272 -0
- package/skills/agentvfx/test/impact.test.mjs +231 -0
- package/skills/agentvfx/test/imports.test.mjs +39 -0
- package/skills/agentvfx/test/light.test.mjs +200 -0
- package/skills/agentvfx/test/loop.test.mjs +80 -0
- package/skills/agentvfx/test/psychedelic.test.mjs +99 -0
- package/skills/agentvfx/test/render.test.mjs +73 -0
- package/skills/agentvfx/test/sacred.test.mjs +175 -0
- package/skills/agentvfx/test/schema.test.mjs +103 -0
- package/skills/agentvfx/test/scope.test.mjs +306 -0
- package/skills/agentvfx/test/spectrum.test.mjs +165 -0
- package/skills/agentvfx/test/text.test.mjs +142 -0
- package/skills/agentvfx/test/web.test.mjs +397 -0
- package/skills/agentvfx/test-media/SOURCES.md +39 -0
- package/skills/agentvfx/test-media/beat-132.mp3 +0 -0
- package/skills/agentvfx/test-media/clip-01.mp4 +0 -0
- 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
|
+
}
|