Replace batch DAG system with streaming architecture

- Remove legacy_tasks.py, hybrid_state.py, render.py
- Remove old task modules (analyze, execute, execute_sexp, orchestrate)
- Add streaming interpreter from test repo
- Add sexp_effects with primitives and video effects
- Add streaming Celery task with CID-based asset resolution
- Support both CID and friendly name references for assets
- Add .dockerignore to prevent local clones from conflicting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-02 19:10:11 +00:00
parent 270eeb3fcf
commit bb458aa924
107 changed files with 15830 additions and 3211 deletions

View File

@@ -0,0 +1,25 @@
;; Crossfade with Zoom Transition
;;
;; Macro for transitioning between two frames with a zoom effect.
;; Active frame zooms out while next frame zooms in.
;;
;; Required context:
;; - zoom effect must be loaded
;; - blend effect must be loaded
;;
;; Parameters:
;; active-frame: current frame
;; next-frame: frame to transition to
;; fade-amt: transition progress (0 = all active, 1 = all next)
;;
;; Usage:
;; (include :path "../templates/crossfade-zoom.sexp")
;; ...
;; (crossfade-zoom active-frame next-frame 0.5)
(defmacro crossfade-zoom (active-frame next-frame fade-amt)
(let [active-zoom (+ 1.0 fade-amt)
active-zoomed (zoom active-frame :amount active-zoom)
next-zoom (+ 0.1 (* fade-amt 0.9))
next-zoomed (zoom next-frame :amount next-zoom)]
(blend active-zoomed next-zoomed :opacity fade-amt)))

View File

@@ -0,0 +1,65 @@
;; cycle-crossfade template
;;
;; Generalized cycling zoom-crossfade for any number of video layers.
;; Cycles through videos with smooth zoom-based crossfade transitions.
;;
;; Parameters:
;; beat-data - beat analysis node (drives timing)
;; input-videos - list of video nodes to cycle through
;; init-clen - initial cycle length in beats
;;
;; NOTE: The parameter is named "input-videos" (not "videos") because
;; template substitution replaces param names everywhere in the AST.
;; The planner's _expand_slice_on injects env["videos"] at plan time,
;; so (len videos) inside the lambda references that injected value.
(deftemplate cycle-crossfade
(beat-data input-videos init-clen)
(slice-on beat-data
:videos input-videos
:init {:cycle 0 :beat 0 :clen init-clen}
:fn (lambda [acc i start end]
(let [beat (get acc "beat")
clen (get acc "clen")
active (get acc "cycle")
n (len videos)
phase3 (* beat 3)
wt (lambda [p]
(let [prev (mod (+ p (- n 1)) n)]
(if (= active p)
(if (< phase3 clen) 1.0
(if (< phase3 (* clen 2))
(- 1.0 (* (/ (- phase3 clen) clen) 1.0))
0.0))
(if (= active prev)
(if (< phase3 clen) 0.0
(if (< phase3 (* clen 2))
(* (/ (- phase3 clen) clen) 1.0)
1.0))
0.0))))
zm (lambda [p]
(let [prev (mod (+ p (- n 1)) n)]
(if (= active p)
;; Active video: normal -> zoom out during transition -> tiny
(if (< phase3 clen) 1.0
(if (< phase3 (* clen 2))
(+ 1.0 (* (/ (- phase3 clen) clen) 1.0))
0.1))
(if (= active prev)
;; Incoming video: tiny -> zoom in during transition -> normal
(if (< phase3 clen) 0.1
(if (< phase3 (* clen 2))
(+ 0.1 (* (/ (- phase3 clen) clen) 0.9))
1.0))
0.1))))
new-acc (if (< (+ beat 1) clen)
(dict :cycle active :beat (+ beat 1) :clen clen)
(dict :cycle (mod (+ active 1) n) :beat 0
:clen (+ 40 (mod (* i 7) 41))))]
{:layers (map (lambda [p]
{:video p :effects [{:effect zoom :amount (zm p)}]})
(range 0 n))
:compose {:effect blend_multi :mode "alpha"
:weights (map (lambda [p] (wt p)) (range 0 n))}
:acc new-acc}))))

112
templates/process-pair.sexp Normal file
View File

@@ -0,0 +1,112 @@
;; process-pair template
;;
;; Reusable video-pair processor: takes a single video source, creates two
;; clips (A and B) with opposite rotations and sporadic effects, blends them,
;; and applies a per-pair slow rotation driven by a beat scan.
;;
;; All sporadic triggers (invert, hue-shift, ascii) and pair-level controls
;; (blend opacity, rotation) are defined internally using seed offsets.
;;
;; Parameters:
;; video - source video node
;; energy - energy analysis node (drives rotation/zoom amounts)
;; beat-data - beat analysis node (drives sporadic triggers)
;; rng - RNG object from (make-rng seed) for auto-derived seeds
;; rot-dir - initial rotation direction: 1 (clockwise) or -1 (anti-clockwise)
;; rot-a/b - rotation ranges for clip A/B (e.g. [0 45])
;; zoom-a/b - zoom ranges for clip A/B (e.g. [1 1.5])
(deftemplate process-pair
(video energy beat-data rng rot-dir rot-a rot-b zoom-a zoom-b)
;; --- Sporadic triggers for clip A ---
;; Invert: 10% chance per beat, lasts 1-5 beats
(def inv-a (scan beat-data :rng rng :init 0
:step (if (> acc 0) (- acc 1) (if (< (rand) 0.1) (rand-int 1 5) 0))
:emit (if (> acc 0) 1 0)))
;; Hue shift: 10% chance, random hue 30-330 deg, lasts 1-5 beats
(def hue-a (scan beat-data :rng rng :init (dict :rem 0 :hue 0)
:step (if (> rem 0)
(dict :rem (- rem 1) :hue hue)
(if (< (rand) 0.1)
(dict :rem (rand-int 1 5) :hue (rand-range 30 330))
(dict :rem 0 :hue 0)))
:emit (if (> rem 0) hue 0)))
;; ASCII art: 5% chance, lasts 1-3 beats
(def ascii-a (scan beat-data :rng rng :init 0
:step (if (> acc 0) (- acc 1) (if (< (rand) 0.05) (rand-int 1 3) 0))
:emit (if (> acc 0) 1 0)))
;; --- Sporadic triggers for clip B (offset seeds) ---
(def inv-b (scan beat-data :rng rng :init 0
:step (if (> acc 0) (- acc 1) (if (< (rand) 0.1) (rand-int 1 5) 0))
:emit (if (> acc 0) 1 0)))
(def hue-b (scan beat-data :rng rng :init (dict :rem 0 :hue 0)
:step (if (> rem 0)
(dict :rem (- rem 1) :hue hue)
(if (< (rand) 0.1)
(dict :rem (rand-int 1 5) :hue (rand-range 30 330))
(dict :rem 0 :hue 0)))
:emit (if (> rem 0) hue 0)))
(def ascii-b (scan beat-data :rng rng :init 0
:step (if (> acc 0) (- acc 1) (if (< (rand) 0.05) (rand-int 1 3) 0))
:emit (if (> acc 0) 1 0)))
;; --- Pair-level controls ---
;; Internal A/B blend: randomly show A (0), both (0.5), or B (1), every 1-11 beats
(def pair-mix (scan beat-data :rng rng
:init (dict :rem 0 :opacity 0.5)
:step (if (> rem 0)
(dict :rem (- rem 1) :opacity opacity)
(dict :rem (rand-int 1 11) :opacity (* (rand-int 0 2) 0.5)))
:emit opacity))
;; Per-pair rotation: one full rotation every 20-30 beats, alternating direction
(def pair-rot (scan beat-data :rng rng
:init (dict :beat 0 :clen 25 :dir rot-dir :angle 0)
:step (if (< (+ beat 1) clen)
(dict :beat (+ beat 1) :clen clen :dir dir
:angle (+ angle (* dir (/ 360 clen))))
(dict :beat 0 :clen (rand-int 20 30) :dir (* dir -1)
:angle angle))
:emit angle))
;; --- Clip A processing ---
(def clip-a (-> video (segment :start 0 :duration (bind energy duration))))
(def rotated-a (-> clip-a
(effect rotate :angle (bind energy values :range rot-a))
(effect zoom :amount (bind energy values :range zoom-a))
(effect invert :amount (bind inv-a values))
(effect hue_shift :degrees (bind hue-a values))
;; ASCII disabled - too slow without GPU
;; (effect ascii_art
;; :char_size (bind energy values :range [4 32])
;; :mix (bind ascii-a values))
))
;; --- Clip B processing ---
(def clip-b (-> video (segment :start 0 :duration (bind energy duration))))
(def rotated-b (-> clip-b
(effect rotate :angle (bind energy values :range rot-b))
(effect zoom :amount (bind energy values :range zoom-b))
(effect invert :amount (bind inv-b values))
(effect hue_shift :degrees (bind hue-b values))
;; ASCII disabled - too slow without GPU
;; (effect ascii_art
;; :char_size (bind energy values :range [4 32])
;; :mix (bind ascii-b values))
))
;; --- Blend A+B and apply pair rotation ---
(-> rotated-a
(effect blend rotated-b
:mode "alpha" :opacity (bind pair-mix values) :resize_mode "fit")
(effect rotate
:angle (bind pair-rot values))))

View File

@@ -0,0 +1,28 @@
;; Oscillating Spin Scan
;;
;; Accumulates rotation angle on each beat, reversing direction
;; periodically for an oscillating effect.
;;
;; Required context:
;; - music: audio analyzer from (streaming:make-audio-analyzer ...)
;;
;; Provides scan: spin
;; Bind with: (bind spin :angle) ;; cumulative rotation angle
;;
;; Behavior:
;; - Rotates 14.4 degrees per beat (completes 360 in 25 beats)
;; - After 20-30 beats, reverses direction
;; - Creates a swinging/oscillating rotation effect
;;
;; Usage:
;; (include :path "../templates/scan-oscillating-spin.sexp")
;;
;; In frame:
;; (rotate frame :angle (bind spin :angle))
(scan spin (streaming:audio-beat music t)
:init {:angle 0 :dir 1 :left 25}
:step (if (> left 0)
(dict :angle (+ angle (* dir 14.4)) :dir dir :left (- left 1))
(dict :angle angle :dir (* dir -1)
:left (+ 20 (mod (streaming:audio-beat-count music t) 11)))))

View File

@@ -0,0 +1,41 @@
;; Beat-Triggered Ripple Drops Scan
;;
;; Creates random ripple drops triggered by audio beats.
;; Each drop has a random center position and duration.
;;
;; Required context:
;; - music: audio analyzer from (streaming:make-audio-analyzer ...)
;; - core primitives loaded
;;
;; Provides scan: ripple-state
;; Bind with: (bind ripple-state :gate) ;; 0 or 1
;; (bind ripple-state :cx) ;; center x (0-1)
;; (bind ripple-state :cy) ;; center y (0-1)
;;
;; Parameters:
;; trigger-chance: probability per beat (default 0.15)
;; min-duration: minimum beats (default 1)
;; max-duration: maximum beats (default 15)
;;
;; Usage:
;; (include :path "../templates/scan-ripple-drops.sexp")
;; ;; Uses default: 15% chance, 1-15 beat duration
;;
;; In frame:
;; (let [rip-gate (bind ripple-state :gate)
;; rip-amp (* rip-gate (core:map-range e 0 1 5 50))]
;; (ripple frame
;; :amplitude rip-amp
;; :center_x (bind ripple-state :cx)
;; :center_y (bind ripple-state :cy)))
(scan ripple-state (streaming:audio-beat music t)
:init {:gate 0 :cx 0.5 :cy 0.5 :left 0}
:step (if (> left 0)
(dict :gate 1 :cx cx :cy cy :left (- left 1))
(if (< (core:rand) 0.15)
(dict :gate 1
:cx (+ 0.2 (* (core:rand) 0.6))
:cy (+ 0.2 (* (core:rand) 0.6))
:left (+ 1 (mod (streaming:audio-beat-count music t) 15)))
(dict :gate 0 :cx 0.5 :cy 0.5 :left 0))))

View File

@@ -0,0 +1,22 @@
;; Standard Effects Bundle
;;
;; Loads commonly-used video effects.
;; Include after primitives are loaded.
;;
;; Effects provided:
;; - rotate: rotation by angle
;; - zoom: scale in/out
;; - blend: alpha blend two frames
;; - ripple: water ripple distortion
;; - invert: color inversion
;; - hue_shift: hue rotation
;;
;; Usage:
;; (include :path "../templates/standard-effects.sexp")
(effect rotate :path "../sexp_effects/effects/rotate.sexp")
(effect zoom :path "../sexp_effects/effects/zoom.sexp")
(effect blend :path "../sexp_effects/effects/blend.sexp")
(effect ripple :path "../sexp_effects/effects/ripple.sexp")
(effect invert :path "../sexp_effects/effects/invert.sexp")
(effect hue_shift :path "../sexp_effects/effects/hue_shift.sexp")

View File

@@ -0,0 +1,14 @@
;; Standard Primitives Bundle
;;
;; Loads all commonly-used primitive libraries.
;; Include this at the top of streaming recipes.
;;
;; Usage:
;; (include :path "../templates/standard-primitives.sexp")
(require-primitives "geometry")
(require-primitives "core")
(require-primitives "image")
(require-primitives "blending")
(require-primitives "color_ops")
(require-primitives "streaming")

View File

@@ -0,0 +1,72 @@
;; stream-process-pair template (streaming-compatible)
;;
;; Macro for processing a video source pair with full effects.
;; Reads source, applies A/B effects (rotate, zoom, invert, hue), blends,
;; and applies pair-level rotation.
;;
;; Required context (must be defined in calling scope):
;; - sources: array of video sources
;; - pair-configs: array of {:dir :rot-a :rot-b :zoom-a :zoom-b} configs
;; - pair-states: array from (bind pairs :states)
;; - now: current time (t)
;; - e: audio energy (0-1)
;;
;; Required effects (must be loaded):
;; - rotate, zoom, invert, hue_shift, blend
;;
;; Usage:
;; (include :path "../templates/stream-process-pair.sexp")
;; ...in frame pipeline...
;; (let [pair-states (bind pairs :states)
;; now t
;; e (streaming:audio-energy music now)]
;; (process-pair 0)) ;; process source at index 0
(require-primitives "core")
(defmacro process-pair (src-idx)
(let [src (nth sources src-idx)
frame (streaming:source-read src now)
cfg (nth pair-configs src-idx)
state (nth pair-states src-idx)
;; Get state values (invert uses countdown > 0)
inv-a-active (if (> (get state :inv-a) 0) 1 0)
inv-b-active (if (> (get state :inv-b) 0) 1 0)
;; Hue is active only when countdown > 0
hue-a-val (if (> (get state :hue-a) 0) (get state :hue-a-val) 0)
hue-b-val (if (> (get state :hue-b) 0) (get state :hue-b-val) 0)
mix-opacity (get state :mix)
pair-rot-angle (* (get state :angle) (get cfg :dir))
;; Get config values for energy-mapped ranges
rot-a-max (get cfg :rot-a)
rot-b-max (get cfg :rot-b)
zoom-a-max (get cfg :zoom-a)
zoom-b-max (get cfg :zoom-b)
;; Energy-driven rotation and zoom
rot-a (core:map-range e 0 1 0 rot-a-max)
rot-b (core:map-range e 0 1 0 rot-b-max)
zoom-a (core:map-range e 0 1 1 zoom-a-max)
zoom-b (core:map-range e 0 1 1 zoom-b-max)
;; Apply effects to clip A
clip-a (-> frame
(rotate :angle rot-a)
(zoom :amount zoom-a)
(invert :amount inv-a-active)
(hue_shift :degrees hue-a-val))
;; Apply effects to clip B
clip-b (-> frame
(rotate :angle rot-b)
(zoom :amount zoom-b)
(invert :amount inv-b-active)
(hue_shift :degrees hue-b-val))
;; Blend A+B
blended (blend clip-a clip-b :opacity mix-opacity)]
;; Apply pair-level rotation
(rotate blended :angle pair-rot-angle)))