Files
test/templates/cycle-crossfade.sexp
gilesb d241e2a663 Add streaming video compositor with sexp interpreter
- New streaming/ module for real-time video processing:
  - compositor.py: Main streaming compositor with cycle-crossfade
  - sexp_executor.py: Executes compiled sexp recipes in real-time
  - sexp_interp.py: Full S-expression interpreter for SLICE_ON Lambda
  - recipe_adapter.py: Bridges recipes to streaming compositor
  - sources.py: Video source with ffmpeg streaming
  - audio.py: Real-time audio analysis (energy, beats)
  - output.py: Preview (mpv) and file output with audio muxing

- New templates/:
  - cycle-crossfade.sexp: Smooth zoom-based video cycling
  - process-pair.sexp: Dual-clip processing with effects

- Key features:
  - Videos cycle in input-videos order (not definition order)
  - Cumulative whole-spin rotation
  - Zero-weight sources skip processing
  - Live audio-reactive effects

- New effects: blend_multi for weighted layer compositing
- Updated primitives and interpreter for streaming compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 01:27:39 +00:00

66 lines
2.7 KiB
Common Lisp

;; 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}))))