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,349 @@
|
|
|
1
|
+
// Headless Chrome over the DevTools pipe transport (fd 3/4, NUL-delimited JSON), so no control
|
|
2
|
+
// port ever listens. Re-expresses the driver mechanism of mexicat/pdoom-video app/scripts/render.ts
|
|
3
|
+
// (MIT, see ../NOTICE) without a driver package. Launch walks the MO-A-51 flag ladder and a rung
|
|
4
|
+
// only counts when a real page gets a WebGL2 context; the page is served from disk through CDP
|
|
5
|
+
// Fetch interception, so nothing listens for it either.
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { CHROME_ALWAYS, CHROME_RUNGS, EXIT, UNKNOWN_RENDERER } from "../core/constants.mjs";
|
|
11
|
+
import { which } from "./host.mjs";
|
|
12
|
+
|
|
13
|
+
export class BlockedError extends Error {
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CANDIDATES = {
|
|
21
|
+
darwin: [
|
|
22
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
23
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
24
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
25
|
+
path.join(homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
|
|
26
|
+
],
|
|
27
|
+
linux: ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"],
|
|
28
|
+
win32: [
|
|
29
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
30
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Resolve Chrome: $CHROME_PATH, then the platform's usual locations. Returns null when absent. */
|
|
35
|
+
export function findChrome(env = process.env) {
|
|
36
|
+
if (env.CHROME_PATH) return existsSync(env.CHROME_PATH) ? env.CHROME_PATH : null;
|
|
37
|
+
for (const candidate of CANDIDATES[process.platform] ?? []) {
|
|
38
|
+
if (path.isAbsolute(candidate)) {
|
|
39
|
+
if (existsSync(candidate)) return candidate;
|
|
40
|
+
} else {
|
|
41
|
+
const found = which(candidate);
|
|
42
|
+
if (found) return found;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function chromeVersion(chrome) {
|
|
49
|
+
const probe = spawnSync(chrome, ["--version"], { encoding: "utf8", timeout: 15000 });
|
|
50
|
+
return probe.status === 0 ? probe.stdout.trim() : `unknown (${(probe.stderr || probe.error?.message || "").trim().split(/\r?\n/u)[0]})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The stage path's flag set (director brief 6d): the software rung (SwiftShader) by default, or the
|
|
55
|
+
* platform's GPU rung for a 3D film (WebGL on the GPU; page raster stays on the CPU either way), the compositor and animation flags that keep a captured frame a function of the page's state,
|
|
56
|
+
* sRGB at scale 1, the window at the format's size, no background networking, and a resolver rule
|
|
57
|
+
* that leaves only the synthetic stage origin resolvable.
|
|
58
|
+
*/
|
|
59
|
+
export function stageFlags({ width, height, rung = "software" }) {
|
|
60
|
+
const base = rung === "gpu" ? CHROME_RUNGS[process.platform]?.[0]?.filter((f) => f !== "--enable-gpu-rasterization") : CHROME_RUNGS.software;
|
|
61
|
+
if (!base) throw new Error(`no GPU rung on ${process.platform}`);
|
|
62
|
+
return [
|
|
63
|
+
...base, "--disable-gpu-rasterization", "--disable-partial-raster",
|
|
64
|
+
"--run-all-compositor-stages-before-draw", "--disable-checker-imaging", "--disable-new-content-rendering-timeout",
|
|
65
|
+
"--disable-threaded-animation", "--disable-threaded-scrolling", "--disable-image-animation-resync", "--disable-lcd-text",
|
|
66
|
+
"--force-color-profile=srgb", "--hide-scrollbars", "--mute-audio", "--force-device-scale-factor=1", `--window-size=${width},${height}`,
|
|
67
|
+
"--disable-background-networking", "--disable-component-update", "--disable-sync", "--no-pings", "--metrics-recording-only",
|
|
68
|
+
"--host-resolver-rules=MAP * ~NOTFOUND , EXCLUDE lit.stage",
|
|
69
|
+
...CHROME_ALWAYS,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The ladder for this platform: GPU rung(s) first, SwiftShader last (MO-A-51). */
|
|
74
|
+
export function flagLadder({ softwareOnly = false } = {}) {
|
|
75
|
+
const gpu = CHROME_RUNGS[process.platform] ?? [];
|
|
76
|
+
const rungs = softwareOnly ? [CHROME_RUNGS.software] : [...gpu, CHROME_RUNGS.software];
|
|
77
|
+
return rungs.map((rung) => [...rung, ...CHROME_ALWAYS]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Messages arrive as raw bytes: each chunk is scanned once for the NUL terminator and kept as a
|
|
81
|
+
// Buffer until its message is complete, so a large screenshot never rescans what came before and a
|
|
82
|
+
// multi-byte character split across two chunks is decoded whole.
|
|
83
|
+
export class Pipe {
|
|
84
|
+
constructor(child) {
|
|
85
|
+
this.child = child;
|
|
86
|
+
this.id = 0;
|
|
87
|
+
this.pending = new Map();
|
|
88
|
+
this.listeners = new Map();
|
|
89
|
+
this.parts = [];
|
|
90
|
+
this.closed = false;
|
|
91
|
+
child.stdio[4].on("data", (chunk) => this.receive(chunk));
|
|
92
|
+
child.stdio[4].on("error", () => {});
|
|
93
|
+
child.stdio[3].on("error", () => {});
|
|
94
|
+
child.on("exit", () => {
|
|
95
|
+
this.closed = true;
|
|
96
|
+
for (const { reject } of this.pending.values()) reject(new Error("Chrome exited"));
|
|
97
|
+
this.pending.clear();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
receive(chunk) {
|
|
101
|
+
let start = 0;
|
|
102
|
+
for (;;) {
|
|
103
|
+
const end = chunk.indexOf(0, start);
|
|
104
|
+
if (end < 0) {
|
|
105
|
+
if (start < chunk.length) this.parts.push(chunk.subarray(start));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.parts.push(chunk.subarray(start, end));
|
|
109
|
+
start = end + 1;
|
|
110
|
+
const message = JSON.parse(Buffer.concat(this.parts).toString("utf8"));
|
|
111
|
+
this.parts = [];
|
|
112
|
+
if (message.id !== undefined && this.pending.has(message.id)) {
|
|
113
|
+
const { resolve, reject } = this.pending.get(message.id);
|
|
114
|
+
this.pending.delete(message.id);
|
|
115
|
+
if (message.error) reject(new Error(`${message.error.message}${message.error.data ? `: ${message.error.data}` : ""}`));
|
|
116
|
+
else resolve(message.result);
|
|
117
|
+
} else if (message.method) {
|
|
118
|
+
for (const fn of this.listeners.get(message.method) ?? []) fn(message.params, message.sessionId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
send(method, params = {}, sessionId) {
|
|
123
|
+
if (this.closed) return Promise.reject(new Error("Chrome pipe closed"));
|
|
124
|
+
const id = ++this.id;
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
this.pending.set(id, { resolve, reject });
|
|
127
|
+
this.child.stdio[3].write(`${JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) })}\0`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
on(method, fn) {
|
|
131
|
+
if (!this.listeners.has(method)) this.listeners.set(method, []);
|
|
132
|
+
this.listeners.get(method).push(fn);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const withTimeout = (promise, ms, message) => Promise.race([
|
|
137
|
+
promise,
|
|
138
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms).unref()),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const CONTENT_TYPES = { ".html": "text/html", ".mjs": "text/javascript", ".js": "text/javascript", ".json": "application/json", ".ttf": "font/ttf", ".otf": "font/otf", ".svg": "image/svg+xml" };
|
|
142
|
+
|
|
143
|
+
// A loopback origin: the page may then open its WebSocket to 127.0.0.1 without tripping Chrome's
|
|
144
|
+
// private-network rules. Every request to it is fulfilled by Fetch interception, so nothing is
|
|
145
|
+
// ever sent to a real localhost server.
|
|
146
|
+
export const APP_ORIGIN = "http://localhost";
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One headless Chrome with one page. `routes(pathname)` returns a file path or { body, type } for a
|
|
150
|
+
* request under APP_ORIGIN, or null for 404.
|
|
151
|
+
*/
|
|
152
|
+
/**
|
|
153
|
+
* The full argument list for one headless launch. `--use-mock-keychain` and `--password-store=basic`
|
|
154
|
+
* keep Chrome away from the OS keychain: with a fresh profile (or an isolated HOME) it would
|
|
155
|
+
* otherwise look for its safe-storage item and macOS would put a keychain dialog on screen.
|
|
156
|
+
*/
|
|
157
|
+
export function launchArgs(profileDir, flags) {
|
|
158
|
+
return [
|
|
159
|
+
"--headless", `--user-data-dir=${profileDir}`, "--remote-debugging-pipe", "--no-first-run", "--no-default-browser-check",
|
|
160
|
+
"--use-mock-keychain", "--password-store=basic",
|
|
161
|
+
"--disable-extensions", "--hide-scrollbars", "--mute-audio", ...flags, "about:blank",
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Chrome is tied to this process on POSIX: a shell becomes the leader of a new process group,
|
|
166
|
+
// starts a watcher that leaves the group's descriptors and signals alone, and then execs Chrome,
|
|
167
|
+
// so Chrome keeps the shell's PID. The watcher polls this process and Chrome; once either is gone
|
|
168
|
+
// it sends TERM, then KILL, to the whole group. A launcher killed mid-run, even by SIGKILL, so
|
|
169
|
+
// never leaves Chrome or its helpers alive under PID 1.
|
|
170
|
+
const TETHER = [
|
|
171
|
+
'parent=$1; group=$$; shift',
|
|
172
|
+
'( trap "" TERM INT HUP',
|
|
173
|
+
' while kill -0 "$parent" 2>/dev/null && kill -0 "$group" 2>/dev/null; do sleep 0.1; done',
|
|
174
|
+
' kill -TERM -"$group" 2>/dev/null; sleep 0.2; kill -KILL -"$group" 2>/dev/null',
|
|
175
|
+
') </dev/null >/dev/null 2>&1 3>&- 4>&- &',
|
|
176
|
+
'exec "$@"',
|
|
177
|
+
].join("\n");
|
|
178
|
+
|
|
179
|
+
/** `spawn` for Chrome: tethered in its own process group on POSIX, a plain child on Windows. */
|
|
180
|
+
function spawnTethered(chrome, args, options) {
|
|
181
|
+
if (process.platform === "win32") return spawn(chrome, args, options);
|
|
182
|
+
return spawn("/bin/sh", ["-c", TETHER, "litclaude-chrome", String(process.pid), chrome, ...args], { ...options, detached: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class ChromeSession {
|
|
186
|
+
static async launch({ chrome, flags, profileDir, routes, timeoutMs = 30000 }) {
|
|
187
|
+
mkdirSync(profileDir, { recursive: true });
|
|
188
|
+
const args = launchArgs(profileDir, flags);
|
|
189
|
+
let child;
|
|
190
|
+
try {
|
|
191
|
+
child = spawnTethered(chrome, args, { stdio: ["ignore", "ignore", "pipe", "pipe", "pipe"] });
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new BlockedError(EXIT.BLOCKED_NO_CHROME, `BLOCKED_NO_CHROME: Chrome failed to launch: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
let stderr = "";
|
|
196
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
197
|
+
const spawnError = new Promise((_, reject) => child.on("error", (error) => reject(error)));
|
|
198
|
+
const session = new ChromeSession(child, profileDir, routes, flags);
|
|
199
|
+
try {
|
|
200
|
+
await withTimeout(Promise.race([session.pipe.send("Browser.getVersion"), spawnError]), timeoutMs, "Chrome did not answer on the DevTools pipe");
|
|
201
|
+
} catch (error) {
|
|
202
|
+
session.kill();
|
|
203
|
+
const first = stderr.split("\n").map((l) => l.trim()).find(Boolean) ?? error.message;
|
|
204
|
+
throw new BlockedError(EXIT.BLOCKED_NO_CHROME, `BLOCKED_NO_CHROME: Chrome failed to launch headless: ${first}`);
|
|
205
|
+
}
|
|
206
|
+
session.stderr = () => stderr;
|
|
207
|
+
return session;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
constructor(child, profileDir, routes, flags) {
|
|
211
|
+
this.child = child;
|
|
212
|
+
this.profileDir = profileDir;
|
|
213
|
+
this.routes = routes;
|
|
214
|
+
this.flags = flags;
|
|
215
|
+
this.pipe = new Pipe(child);
|
|
216
|
+
this.logs = [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async openPage() {
|
|
220
|
+
const { targetId } = await this.pipe.send("Target.createTarget", { url: "about:blank" });
|
|
221
|
+
const { sessionId } = await this.pipe.send("Target.attachToTarget", { targetId, flatten: true });
|
|
222
|
+
this.sessionId = sessionId;
|
|
223
|
+
this.pipe.on("Runtime.exceptionThrown", (p) => this.logs.push(`[exception] ${p.exceptionDetails?.exception?.description ?? p.exceptionDetails?.text}`));
|
|
224
|
+
this.pipe.on("Runtime.consoleAPICalled", (p) => {
|
|
225
|
+
if (p.type === "error" || p.type === "warning") this.logs.push(`[${p.type}] ${p.args.map((a) => a.value ?? a.description).join(" ")}`);
|
|
226
|
+
});
|
|
227
|
+
this.pipe.on("Fetch.requestPaused", (p, sid) => this.serve(p, sid));
|
|
228
|
+
// Chrome gates a page's connections to 127.0.0.1 behind a loopback-network permission; grant
|
|
229
|
+
// it for the app origin only (older builds do not know the name, which is fine).
|
|
230
|
+
await this.pipe.send("Browser.grantPermissions", { permissions: ["loopbackNetwork"], origin: APP_ORIGIN }).catch(() => {});
|
|
231
|
+
await this.send("Runtime.enable");
|
|
232
|
+
await this.send("Fetch.enable", { patterns: [{ urlPattern: `${APP_ORIGIN}/*`, requestStage: "Request" }] });
|
|
233
|
+
await this.send("Emulation.setDeviceMetricsOverride", { width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async serve(params, sid) {
|
|
237
|
+
const url = new URL(params.request.url);
|
|
238
|
+
let resolved = null;
|
|
239
|
+
try {
|
|
240
|
+
resolved = this.routes(decodeURIComponent(url.pathname));
|
|
241
|
+
} catch {
|
|
242
|
+
resolved = null;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
if (!resolved) {
|
|
246
|
+
await this.pipe.send("Fetch.fulfillRequest", { requestId: params.requestId, responseCode: 404, body: "" }, sid);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const body = typeof resolved === "string" ? readFileSync(resolved) : Buffer.from(resolved.body);
|
|
250
|
+
const type = typeof resolved === "string" ? CONTENT_TYPES[path.extname(resolved)] ?? "application/octet-stream" : resolved.type;
|
|
251
|
+
await this.pipe.send("Fetch.fulfillRequest", {
|
|
252
|
+
requestId: params.requestId, responseCode: 200,
|
|
253
|
+
responseHeaders: [{ name: "Content-Type", value: type }, { name: "Cache-Control", value: "no-store" }],
|
|
254
|
+
body: body.toString("base64"),
|
|
255
|
+
}, sid);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.logs.push(`[serve] ${url.pathname}: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
send(method, params = {}) {
|
|
262
|
+
return this.pipe.send(method, params, this.sessionId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Evaluate an expression in the page; resolves promises and returns the JSON value. */
|
|
266
|
+
async evaluate(expression, { timeoutMs = 600000 } = {}) {
|
|
267
|
+
const result = await withTimeout(this.send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }), timeoutMs, `page evaluation timed out: ${expression.slice(0, 80)}`);
|
|
268
|
+
if (result.exceptionDetails) throw new Error(`page error: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
|
|
269
|
+
return result.result.value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** WebGL2 in a blank page: { webgl2, renderer } (MO-A-51 rung check). */
|
|
273
|
+
async probeWebgl2() {
|
|
274
|
+
return this.evaluate(`(() => {
|
|
275
|
+
const gl = document.createElement('canvas').getContext('webgl2');
|
|
276
|
+
if (!gl) return { webgl2: false, renderer: null };
|
|
277
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
278
|
+
return { webgl2: true, renderer: ext ? String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)) : ${JSON.stringify(UNKNOWN_RENDERER)} };
|
|
279
|
+
})()`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async navigate(url) {
|
|
283
|
+
await this.send("Page.enable");
|
|
284
|
+
await this.send("Page.navigate", { url });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
kill() {
|
|
288
|
+
try {
|
|
289
|
+
if (process.platform === "win32") this.child.kill("SIGKILL");
|
|
290
|
+
else process.kill(-this.child.pid, "SIGKILL");
|
|
291
|
+
} catch {
|
|
292
|
+
// already gone
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async close() {
|
|
297
|
+
try {
|
|
298
|
+
await withTimeout(this.pipe.send("Browser.close"), 5000, "close timed out");
|
|
299
|
+
} catch {
|
|
300
|
+
this.kill();
|
|
301
|
+
}
|
|
302
|
+
const exited = (ms) => new Promise((resolve) => {
|
|
303
|
+
if (this.child.exitCode !== null || this.child.signalCode) resolve();
|
|
304
|
+
else this.child.once("exit", resolve);
|
|
305
|
+
setTimeout(resolve, ms).unref();
|
|
306
|
+
});
|
|
307
|
+
await exited(5000);
|
|
308
|
+
// A browser that acknowledged Browser.close but is still alive would keep this process open.
|
|
309
|
+
if (this.child.exitCode === null && !this.child.signalCode) {
|
|
310
|
+
this.kill();
|
|
311
|
+
await exited(2000);
|
|
312
|
+
}
|
|
313
|
+
rmSync(this.profileDir, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Walk the ladder: launch, check WebGL2 in a real page, keep the first rung that has it. Throws
|
|
319
|
+
* BlockedError 10 when Chrome itself cannot run, 11 when Chrome runs on every rung but no WebGL2.
|
|
320
|
+
*/
|
|
321
|
+
export async function launchWithLadder({ chrome, profileRoot, routes, softwareOnly = false, log = () => {} }) {
|
|
322
|
+
if (!chrome) throw new BlockedError(EXIT.BLOCKED_NO_CHROME, "BLOCKED_NO_CHROME: Chrome/Chromium not found (set CHROME_PATH or install Google Chrome)");
|
|
323
|
+
const failures = [];
|
|
324
|
+
let index = 0;
|
|
325
|
+
for (const flags of flagLadder({ softwareOnly })) {
|
|
326
|
+
const profileDir = path.join(profileRoot, `c${process.pid % 100000}-${index++}`);
|
|
327
|
+
let session;
|
|
328
|
+
try {
|
|
329
|
+
session = await ChromeSession.launch({ chrome, flags, profileDir, routes });
|
|
330
|
+
} catch (error) {
|
|
331
|
+
rmSync(profileDir, { recursive: true, force: true });
|
|
332
|
+
if (error instanceof BlockedError) throw error;
|
|
333
|
+
throw new BlockedError(EXIT.BLOCKED_NO_CHROME, `BLOCKED_NO_CHROME: ${error.message}`);
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
await session.openPage();
|
|
337
|
+
const probe = await session.probeWebgl2();
|
|
338
|
+
if (probe.webgl2) {
|
|
339
|
+
log(`chrome rung ok: ${flags.join(" ")} -> ${probe.renderer}`);
|
|
340
|
+
return { session, flags, renderer: probe.renderer };
|
|
341
|
+
}
|
|
342
|
+
failures.push(`${flags[0]}: getContext('webgl2') returned null`);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
failures.push(`${flags[0]}: ${error.message}`);
|
|
345
|
+
}
|
|
346
|
+
await session.close();
|
|
347
|
+
}
|
|
348
|
+
throw new BlockedError(EXIT.BLOCKED_NO_WEBGL2, `BLOCKED_NO_WEBGL2: Chrome ran but no rung produced a WebGL2 context (${failures.join("; ")})`);
|
|
349
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// agentvfx (agentvfx licence, see LICENSE). Adapted from lit-typographic-motion 1.0.45 engine/node/encode.mjs.
|
|
2
|
+
// ffmpeg is always the last stage and always a separately installed host binary: the master is one
|
|
3
|
+
// pinned encode (BT.709 matrix and tags, tv range, CRF 16, tune grain). The preview walks the encoder
|
|
4
|
+
// ladder (ffmpeg libwebp_anim, then img2webp, then GIF) over PNG frames the renderer wrote.
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { ENCODE_ARGS, PREVIEW } from "../core/constants.mjs";
|
|
9
|
+
import { which } from "./host.mjs";
|
|
10
|
+
import { ffmpegMetadataArgs } from "../core/mark.mjs";
|
|
11
|
+
|
|
12
|
+
export { which };
|
|
13
|
+
|
|
14
|
+
export const findFfmpeg = (env = process.env) => (env.FFMPEG_PATH && existsSync(env.FFMPEG_PATH) ? env.FFMPEG_PATH : which("ffmpeg", env));
|
|
15
|
+
export const findFfprobe = (env = process.env) => (env.FFPROBE_PATH && existsSync(env.FFPROBE_PATH) ? env.FFPROBE_PATH : which("ffprobe", env));
|
|
16
|
+
export const findImg2webp = (env = process.env) => which("img2webp", env);
|
|
17
|
+
|
|
18
|
+
export function ffmpegVersion(ffmpeg) {
|
|
19
|
+
const probe = spawnSync(ffmpeg, ["-version"], { encoding: "utf8" });
|
|
20
|
+
return probe.status === 0 ? probe.stdout.split(/\r?\n/u)[0].trim() : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The preview-encoder rungs available on this host, in ladder order. */
|
|
24
|
+
export function previewRungs(ffmpeg, env = process.env) {
|
|
25
|
+
const rungs = [];
|
|
26
|
+
if (ffmpeg) {
|
|
27
|
+
const list = spawnSync(ffmpeg, ["-hide_banner", "-encoders"], { encoding: "utf8" }).stdout ?? "";
|
|
28
|
+
if (/\blibwebp_anim\b/u.test(list)) rungs.push("libwebp_anim");
|
|
29
|
+
}
|
|
30
|
+
if (findImg2webp(env)) rungs.push("img2webp");
|
|
31
|
+
if (ffmpeg) rungs.push("gif");
|
|
32
|
+
return rungs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Start the master encode; returns { write(bytes), finish() }. Writes respect stdin backpressure. */
|
|
36
|
+
export function startMaster({ ffmpeg, width, height, fps, out, audio = null, comment = null }) {
|
|
37
|
+
const args = ["-y", "-hide_banner", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${width}x${height}`, "-r", String(fps), "-i", "pipe:0"];
|
|
38
|
+
if (audio) args.push("-i", audio);
|
|
39
|
+
args.push(...ENCODE_ARGS);
|
|
40
|
+
// The track is prepared to the film's exact sample count, so nothing is cut to the shorter stream.
|
|
41
|
+
if (audio) args.push("-map", "0:v:0", "-map", "1:a:0", "-c:a", "aac", "-b:a", "256k");
|
|
42
|
+
args.push(...ffmpegMetadataArgs({ comment }), out);
|
|
43
|
+
const child = spawn(ffmpeg, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
44
|
+
let stderr = "";
|
|
45
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
46
|
+
let exitCode = null;
|
|
47
|
+
const exited = new Promise((resolve) => child.on("close", (code) => { exitCode = code ?? -1; resolve(code); }));
|
|
48
|
+
child.stdin.on("error", () => {});
|
|
49
|
+
const dead = () => new Error(`ffmpeg exited (${exitCode}) during the encode: ${stderr.trim() || "no message"}`);
|
|
50
|
+
return {
|
|
51
|
+
args,
|
|
52
|
+
// A write never waits on 'drain' alone: if ffmpeg dies mid-encode the write rejects instead of hanging.
|
|
53
|
+
async write(bytes) {
|
|
54
|
+
if (exitCode !== null) throw dead();
|
|
55
|
+
if (!child.stdin.write(bytes)) {
|
|
56
|
+
await Promise.race([new Promise((resolve) => child.stdin.once("drain", resolve)), exited]);
|
|
57
|
+
if (exitCode !== null) throw dead();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async finish() {
|
|
61
|
+
child.stdin.end();
|
|
62
|
+
const code = await exited;
|
|
63
|
+
return { code, stderr: stderr.trim() };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function run(cmd, args) {
|
|
69
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
70
|
+
return { code: r.status, stderr: (r.stderr || r.error?.message || "").trim() };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Encode one preview rung from numbered PNGs (00000.png ...) with one encoder. */
|
|
74
|
+
export function encodePreview({ encoder, ffmpeg, img2webp, dir, fps, out }) {
|
|
75
|
+
const frames = readdirSync(dir).filter((f) => /^\d{5}\.png$/u.test(f)).sort();
|
|
76
|
+
if (encoder === "libwebp_anim") {
|
|
77
|
+
return run(ffmpeg, ["-y", "-hide_banner", "-loglevel", "error", "-framerate", String(fps), "-i", path.join(dir, "%05d.png"), "-c:v", "libwebp_anim", "-lossless", "0", "-q:v", String(PREVIEW.webpQuality), "-loop", "0", out]);
|
|
78
|
+
}
|
|
79
|
+
if (encoder === "img2webp") {
|
|
80
|
+
const args = ["-loop", "0", "-lossy", "-q", String(PREVIEW.webpQuality), "-m", "4"];
|
|
81
|
+
let elapsed = 0;
|
|
82
|
+
frames.forEach((f, i) => {
|
|
83
|
+
const next = Math.round(((i + 1) * 1000) / fps);
|
|
84
|
+
args.push("-d", String(next - elapsed), path.join(dir, f));
|
|
85
|
+
elapsed = next;
|
|
86
|
+
});
|
|
87
|
+
args.push("-o", out);
|
|
88
|
+
return run(img2webp, args);
|
|
89
|
+
}
|
|
90
|
+
return run(ffmpeg, ["-y", "-hide_banner", "-loglevel", "error", "-framerate", String(fps), "-i", path.join(dir, "%05d.png"),
|
|
91
|
+
"-vf", "split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=4", "-loop", "0", out]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** ffprobe the first video stream. Returns null when ffprobe is missing or fails. */
|
|
95
|
+
export function probeVideo(file, env = process.env) {
|
|
96
|
+
const ffprobe = findFfprobe(env);
|
|
97
|
+
if (!ffprobe || !existsSync(file)) return null;
|
|
98
|
+
const r = spawnSync(ffprobe, ["-v", "error", "-select_streams", "v:0", "-count_frames", "-show_entries",
|
|
99
|
+
"stream=width,height,r_frame_rate,pix_fmt,color_space,color_range,color_transfer,color_primaries,nb_read_frames,codec_name:format=duration,size",
|
|
100
|
+
"-of", "json", file], { encoding: "utf8" });
|
|
101
|
+
if (r.status !== 0) return null;
|
|
102
|
+
const j = JSON.parse(r.stdout);
|
|
103
|
+
const s = j.streams?.[0] ?? {};
|
|
104
|
+
const [num, den] = String(s.r_frame_rate ?? "0/1").split("/").map(Number);
|
|
105
|
+
return {
|
|
106
|
+
codec: s.codec_name, width: s.width, height: s.height, fps: den ? num / den : 0, pixFmt: s.pix_fmt, colorSpace: s.color_space,
|
|
107
|
+
colorRange: s.color_range, colorTransfer: s.color_transfer, colorPrimaries: s.color_primaries, frames: Number(s.nb_read_frames ?? 0),
|
|
108
|
+
duration: Number(j.format?.duration ?? 0), bytes: statSync(file).size,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// agentvfx (agentvfx licence, see LICENSE). The eye: every render leaves stills the model must open before reporting.
|
|
2
|
+
// stills/sheet.png (12 evenly spaced frames, 4x3), stills/strip-<k>.png (7 consecutive frames centred
|
|
3
|
+
// on each hit, cut or onset, or on 25/50/75 % when there are none; at most 6 strips) and
|
|
4
|
+
// stills/poster.png (full size, 40 % in). Frames are downscaled as they arrive, so memory stays small.
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { areaDownscale } from "./analysis.mjs";
|
|
8
|
+
import { encodePng, rgbaToRgb } from "./png.mjs";
|
|
9
|
+
|
|
10
|
+
const GAP = 8;
|
|
11
|
+
const BG = [18, 18, 18];
|
|
12
|
+
const MAX_STRIPS = 6;
|
|
13
|
+
|
|
14
|
+
/** Which frames the eye needs: { sheet: [f], strips: [{ centre, frames: [f] }], poster: f }. */
|
|
15
|
+
export function eyePlan(frameCount, events = []) {
|
|
16
|
+
const sheet = [...new Set(Array.from({ length: 12 }, (_, i) => Math.round((i * (frameCount - 1)) / 11)))];
|
|
17
|
+
const centres = (events.length ? events : [0.25, 0.5, 0.75].map((p) => Math.round(p * (frameCount - 1)))).slice(0, MAX_STRIPS);
|
|
18
|
+
const strips = centres.map((c) => ({ centre: c, frames: Array.from({ length: 7 }, (_, i) => Math.min(frameCount - 1, Math.max(0, c - 3 + i))) }));
|
|
19
|
+
return { sheet, strips, poster: Math.round(0.4 * (frameCount - 1)) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compose(thumbs, cols, tw, th) {
|
|
23
|
+
const rows = Math.ceil(thumbs.length / cols);
|
|
24
|
+
const W = cols * tw + (cols + 1) * GAP;
|
|
25
|
+
const H = rows * th + (rows + 1) * GAP;
|
|
26
|
+
const out = Buffer.alloc(W * H * 4);
|
|
27
|
+
for (let i = 0; i < W * H; i++) out.set([...BG, 255], i * 4);
|
|
28
|
+
thumbs.forEach((t, i) => {
|
|
29
|
+
const ox = GAP + (i % cols) * (tw + GAP);
|
|
30
|
+
const oy = GAP + Math.floor(i / cols) * (th + GAP);
|
|
31
|
+
for (let y = 0; y < th; y++) t.copy(out, ((oy + y) * W + ox) * 4, y * tw * 4, (y + 1) * tw * 4);
|
|
32
|
+
});
|
|
33
|
+
return { rgba: out, width: W, height: H };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const png = (img) => encodePng(rgbaToRgb(img.rgba, img.width, img.height), img.width, img.height, 3, { level: 6 });
|
|
37
|
+
|
|
38
|
+
/** Collects the frames it needs during the render; write(outDir) saves the PNGs and returns their paths. */
|
|
39
|
+
export function createEye({ width, height, frameCount, events }) {
|
|
40
|
+
const plan = eyePlan(frameCount, events);
|
|
41
|
+
const sheetW = 480;
|
|
42
|
+
const sheetH = Math.max(2, Math.round((sheetW * height) / width));
|
|
43
|
+
const stripW = 320;
|
|
44
|
+
const stripH = Math.max(2, Math.round((stripW * height) / width));
|
|
45
|
+
const sheetSet = new Set(plan.sheet);
|
|
46
|
+
const stripSet = new Set(plan.strips.flatMap((s) => s.frames));
|
|
47
|
+
const sheetThumbs = new Map();
|
|
48
|
+
const stripThumbs = new Map();
|
|
49
|
+
let poster = null;
|
|
50
|
+
return {
|
|
51
|
+
plan,
|
|
52
|
+
wants: (f) => sheetSet.has(f) || stripSet.has(f) || f === plan.poster,
|
|
53
|
+
push(f, rgba) {
|
|
54
|
+
if (sheetSet.has(f)) sheetThumbs.set(f, areaDownscale(rgba, width, height, sheetW, sheetH));
|
|
55
|
+
if (stripSet.has(f)) stripThumbs.set(f, areaDownscale(rgba, width, height, stripW, stripH));
|
|
56
|
+
if (f === plan.poster) poster = Buffer.from(rgba);
|
|
57
|
+
},
|
|
58
|
+
write(outDir) {
|
|
59
|
+
const dir = path.join(outDir, "stills");
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
const files = {};
|
|
62
|
+
const sheet = compose(plan.sheet.map((f) => sheetThumbs.get(f)), 4, sheetW, sheetH);
|
|
63
|
+
files.sheet = path.join(dir, "sheet.png");
|
|
64
|
+
writeFileSync(files.sheet, png(sheet));
|
|
65
|
+
files.strips = plan.strips.map((s, k) => {
|
|
66
|
+
const file = path.join(dir, `strip-${k}.png`);
|
|
67
|
+
writeFileSync(file, png(compose(s.frames.map((f) => stripThumbs.get(f)), 7, stripW, stripH)));
|
|
68
|
+
return { file, centre: s.centre, frames: s.frames };
|
|
69
|
+
});
|
|
70
|
+
if (poster) {
|
|
71
|
+
files.poster = path.join(dir, "poster.png");
|
|
72
|
+
writeFileSync(files.poster, png({ rgba: poster, width, height }));
|
|
73
|
+
}
|
|
74
|
+
return files;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// agentvfx (agentvfx licence, see LICENSE). Footage ingest: probe a clip, decode the requested window with ffmpeg at a
|
|
2
|
+
// constant frame rate, scale so the long edge is 1920 (even sizes), convert to RGBA with the clip's
|
|
3
|
+
// colour matrix (BT.709 when untagged), and hand frames to the renderer strictly in order. A request
|
|
4
|
+
// for frame 0 after later frames restarts the decoder (the determinism replay). The window's audio is
|
|
5
|
+
// extracted to a 48 kHz stereo WAV for analysis and muxing.
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdtempSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { EXIT } from "../core/constants.mjs";
|
|
11
|
+
import { BlockedError } from "./chrome.mjs";
|
|
12
|
+
import { findFfmpeg, findFfprobe } from "./encode.mjs";
|
|
13
|
+
|
|
14
|
+
const even = (n) => Math.max(2, 2 * Math.round(n / 2));
|
|
15
|
+
|
|
16
|
+
/** ffprobe a clip: { width, height, duration, fps, colorSpace, hasAudio }. */
|
|
17
|
+
export function probeClip(file) {
|
|
18
|
+
const ffprobe = findFfprobe();
|
|
19
|
+
const r = spawnSync(ffprobe, ["-v", "error", "-show_entries", "stream=codec_type,width,height,r_frame_rate,color_space:format=duration", "-of", "json", file], { encoding: "utf8" });
|
|
20
|
+
if (r.status !== 0) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: ffprobe could not read ${file}: ${r.stderr.trim()}`);
|
|
21
|
+
const j = JSON.parse(r.stdout);
|
|
22
|
+
const v = j.streams.find((s) => s.codec_type === "video");
|
|
23
|
+
if (!v) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: ${file} has no video stream`);
|
|
24
|
+
const [num, den] = String(v.r_frame_rate ?? "0/1").split("/").map(Number);
|
|
25
|
+
return { width: v.width, height: v.height, duration: Number(j.format?.duration ?? 0), fps: den ? num / den : 0, colorSpace: v.color_space ?? null, hasAudio: j.streams.some((s) => s.codec_type === "audio") };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Output size: keep the aspect, long edge `longEdge`, even dimensions. */
|
|
29
|
+
export function fitSize(width, height, longEdge = 1920) {
|
|
30
|
+
return width >= height ? { width: even(longEdge), height: even((longEdge * height) / width) } : { width: even((longEdge * width) / height), height: even(longEdge) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Open a footage source for a render window. Returns { width, height, frames, info, frame(f), close() }.
|
|
35
|
+
* Throws BLOCKED_ASSET_MISSING naming `input.video` when the file is missing or too short.
|
|
36
|
+
*/
|
|
37
|
+
export function openFootage(file, { fps, startSec = 0, durationSec, longEdge = 1920 }) {
|
|
38
|
+
if (!file || !existsSync(file)) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: input.video not found: ${file}`);
|
|
39
|
+
const info = probeClip(file);
|
|
40
|
+
if (info.duration && info.duration + 0.05 < startSec + durationSec) {
|
|
41
|
+
throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: input.video ${file} is ${info.duration.toFixed(2)} s; the window ${startSec}-${startSec + durationSec} s does not fit`);
|
|
42
|
+
}
|
|
43
|
+
const { width, height } = fitSize(info.width, info.height, longEdge);
|
|
44
|
+
const frames = Math.round(durationSec * fps);
|
|
45
|
+
const matrix = info.colorSpace && info.colorSpace !== "unknown" ? "auto" : "bt709";
|
|
46
|
+
const ffmpeg = findFfmpeg();
|
|
47
|
+
const frameBytes = width * height * 4;
|
|
48
|
+
let proc = null;
|
|
49
|
+
let next = 0;
|
|
50
|
+
let chunks = [];
|
|
51
|
+
let have = 0;
|
|
52
|
+
let waiters = [];
|
|
53
|
+
let ended = false;
|
|
54
|
+
let stderr = "";
|
|
55
|
+
|
|
56
|
+
const start = () => {
|
|
57
|
+
stop();
|
|
58
|
+
next = 0;
|
|
59
|
+
chunks = [];
|
|
60
|
+
have = 0;
|
|
61
|
+
ended = false;
|
|
62
|
+
stderr = "";
|
|
63
|
+
const vf = `fps=${fps}:round=near,scale=${width}:${height}:flags=lanczos:in_color_matrix=${matrix}:in_range=auto,format=rgba`;
|
|
64
|
+
proc = spawn(ffmpeg, ["-v", "error", "-ss", String(startSec), "-i", file, "-t", String(durationSec), "-an", "-vf", vf, "-frames:v", String(frames), "-f", "rawvideo", "-pix_fmt", "rgba", "pipe:1"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
65
|
+
proc.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
66
|
+
proc.stdout.on("data", (d) => {
|
|
67
|
+
chunks.push(d);
|
|
68
|
+
have += d.length;
|
|
69
|
+
if (have >= frameBytes * 4) proc.stdout.pause();
|
|
70
|
+
flush();
|
|
71
|
+
});
|
|
72
|
+
proc.stdout.on("end", () => { ended = true; flush(); });
|
|
73
|
+
};
|
|
74
|
+
const take = () => {
|
|
75
|
+
const all = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks);
|
|
76
|
+
const frame = Buffer.from(all.subarray(0, frameBytes));
|
|
77
|
+
const rest = all.subarray(frameBytes);
|
|
78
|
+
chunks = rest.length ? [rest] : [];
|
|
79
|
+
have = rest.length;
|
|
80
|
+
if (proc && have < frameBytes * 4) proc.stdout.resume();
|
|
81
|
+
return frame;
|
|
82
|
+
};
|
|
83
|
+
const flush = () => {
|
|
84
|
+
while (waiters.length && (have >= frameBytes || ended)) {
|
|
85
|
+
const w = waiters.shift();
|
|
86
|
+
if (have >= frameBytes) w.resolve(take());
|
|
87
|
+
else w.reject(new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: input.video ${file} ended early (${stderr.trim()})`));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const stop = () => {
|
|
91
|
+
if (proc && proc.exitCode === null) proc.kill("SIGKILL");
|
|
92
|
+
proc = null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
width, height, frames, info, matrix,
|
|
97
|
+
frame(f) {
|
|
98
|
+
if (f === 0 || !proc) start();
|
|
99
|
+
if (f !== next) return Promise.reject(new Error(`footage frames must be read in order (asked ${f}, next ${next})`));
|
|
100
|
+
next += 1;
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
waiters.push({ resolve, reject });
|
|
103
|
+
flush();
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
close: stop,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Extract an audio window to a 48 kHz stereo WAV in a temp dir; null when the file has no audio. */
|
|
111
|
+
export function extractAudio(file, { startSec = 0, durationSec }) {
|
|
112
|
+
const info = probeClip(file);
|
|
113
|
+
if (!info.hasAudio) return null;
|
|
114
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-audio-"));
|
|
115
|
+
const out = path.join(dir, "window.wav");
|
|
116
|
+
const r = spawnSync(findFfmpeg(), ["-v", "error", "-y", "-ss", String(startSec), "-i", file, "-t", String(durationSec), "-vn", "-ac", "2", "-ar", "48000", "-c:a", "pcm_s16le", out], { encoding: "utf8" });
|
|
117
|
+
if (r.status !== 0) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: could not extract audio from ${file}: ${r.stderr.trim()}`);
|
|
118
|
+
return { file: out, dir };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Audio-only file (input.audio) window to WAV. */
|
|
122
|
+
export function extractAudioFile(file, { startSec = 0, durationSec }) {
|
|
123
|
+
if (!existsSync(file)) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: input.audio not found: ${file}`);
|
|
124
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-audio-"));
|
|
125
|
+
const out = path.join(dir, "window.wav");
|
|
126
|
+
const r = spawnSync(findFfmpeg(), ["-v", "error", "-y", "-ss", String(startSec), "-i", file, "-t", String(durationSec), "-vn", "-ac", "2", "-ar", "48000", "-c:a", "pcm_s16le", out], { encoding: "utf8" });
|
|
127
|
+
if (r.status !== 0) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: could not decode input.audio ${file}: ${r.stderr.trim()}`);
|
|
128
|
+
return { file: out, dir };
|
|
129
|
+
}
|