Files
celery/recipes/woods-recipe-optimized.sexp
giles ed617fcdd6
Some checks are pending
GPU Worker CI/CD / test (push) Waiting to run
GPU Worker CI/CD / deploy (push) Blocked by required conditions
Fix lazy audio path resolution for GPU streaming
Audio playback path was being resolved during parsing when database
may not be ready, causing fallback to non-existent path. Now resolves
lazily when stream starts, matching how audio analyzer works.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:32:04 +00:00

202 lines
8.5 KiB
Common Lisp

;; Woods Recipe - OPTIMIZED VERSION
;;
;; Uses fused-pipeline for GPU acceleration when available,
;; falls back to individual primitives on CPU.
;;
;; Key optimizations:
;; 1. Uses streaming_gpu primitives with fast CUDA kernels
;; 2. Uses fused-pipeline to batch effects into single kernel passes
;; 3. GPU persistence - frames stay on GPU throughout pipeline
(stream "woods-recipe-optimized"
:fps 30
:width 1920
:height 1080
:seed 42
;; Load standard primitives (includes proper asset resolution)
(include :name "tpl-standard-primitives")
;; Load optimized GPU primitives (falls back to CPU automatically)
(require-primitives "streaming_gpu") ;; Includes fused-pipeline
(require-primitives "geometry_gpu") ;; Fast CUDA rotate/ripple
(require-primitives "color_ops_gpu") ;; Fast CUDA hue-shift/invert
(require-primitives "blending_gpu") ;; Fast CUDA blend
;; === SOURCES (using streaming: which has proper asset resolution) ===
(def sources [
(streaming:make-video-source "woods-1" 30)
(streaming:make-video-source "woods-2" 30)
(streaming:make-video-source "woods-3" 30)
(streaming:make-video-source "woods-4" 30)
(streaming:make-video-source "woods-5" 30)
(streaming:make-video-source "woods-6" 30)
(streaming:make-video-source "woods-7" 30)
(streaming:make-video-source "woods-8" 30)
])
;; Per-pair config
(def pair-configs [
{:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
{:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
{:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
{:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5}
{:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
{:dir 1 :rot-a 30 :rot-b -30 :zoom-a 1.3 :zoom-b 0.7}
{:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5}
{:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
])
;; Audio
(def music (streaming:make-audio-analyzer "woods-audio"))
(audio-playback "woods-audio")
;; === SCANS ===
;; Cycle state
(scan cycle (streaming:audio-beat music t)
:init {:active 0 :beat 0 :clen 16}
:step (if (< (+ beat 1) clen)
(dict :active active :beat (+ beat 1) :clen clen)
(dict :active (mod (+ active 1) (len sources)) :beat 0
:clen (+ 8 (mod (* (streaming:audio-beat-count music t) 7) 17)))))
;; Spin scan
(scan spin (streaming:audio-beat music t)
:init {:angle 0 :dir 1 :speed 2}
:step (let [new-dir (if (< (core:rand) 0.05) (* dir -1) dir)
new-speed (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) speed)]
(dict :angle (+ angle (* new-dir new-speed))
:dir new-dir
:speed new-speed)))
;; Ripple scan
(scan ripple-state (streaming:audio-beat music t)
:init {:gate 0 :cx 960 :cy 540}
:step (let [new-gate (if (< (core:rand) 0.15) (+ 3 (core:rand-int 0 5)) (core:max 0 (- gate 1)))
new-cx (if (> new-gate gate) (+ 200 (core:rand-int 0 1520)) cx)
new-cy (if (> new-gate gate) (+ 200 (core:rand-int 0 680)) cy)]
(dict :gate new-gate :cx new-cx :cy new-cy)))
;; Pair states
(scan pairs (streaming:audio-beat music t)
:init {:states (map (core:range (len sources)) (lambda (_)
{:inv-a 0 :inv-b 0 :hue-a 0 :hue-b 0 :hue-a-val 0 :hue-b-val 0 :mix 0.5 :mix-rem 5 :angle 0 :rot-beat 0 :rot-clen 25}))}
:step (dict :states (map states (lambda (p)
(let [new-inv-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-a) 1)))
new-inv-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-b) 1)))
old-hue-a (get p :hue-a)
old-hue-b (get p :hue-b)
new-hue-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-a 1)))
new-hue-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-b 1)))
new-hue-a-val (if (> new-hue-a old-hue-a) (+ 30 (* (core:rand) 300)) (get p :hue-a-val))
new-hue-b-val (if (> new-hue-b old-hue-b) (+ 30 (* (core:rand) 300)) (get p :hue-b-val))
mix-rem (get p :mix-rem)
old-mix (get p :mix)
new-mix-rem (if (> mix-rem 0) (- mix-rem 1) (+ 1 (core:rand-int 1 10)))
new-mix (if (> mix-rem 0) old-mix (* (core:rand-int 0 2) 0.5))
rot-beat (get p :rot-beat)
rot-clen (get p :rot-clen)
old-angle (get p :angle)
new-rot-beat (if (< (+ rot-beat 1) rot-clen) (+ rot-beat 1) 0)
new-rot-clen (if (< (+ rot-beat 1) rot-clen) rot-clen (+ 20 (core:rand-int 0 10)))
new-angle (+ old-angle (/ 360 rot-clen))]
(dict :inv-a new-inv-a :inv-b new-inv-b
:hue-a new-hue-a :hue-b new-hue-b
:hue-a-val new-hue-a-val :hue-b-val new-hue-b-val
:mix new-mix :mix-rem new-mix-rem
:angle new-angle :rot-beat new-rot-beat :rot-clen new-rot-clen))))))
;; === OPTIMIZED PROCESS-PAIR MACRO ===
;; Uses fused-pipeline to batch rotate+hue+invert into single kernel
(defmacro process-pair-fast (idx)
(let [;; Get sources for this pair (with safe modulo indexing)
num-sources (len sources)
src-a (nth sources (mod (* idx 2) num-sources))
src-b (nth sources (mod (+ (* idx 2) 1) num-sources))
cfg (nth pair-configs idx)
pstate (nth (bind pairs :states) idx)
;; Read frames (GPU decode, stays on GPU)
frame-a (streaming:source-read src-a t)
frame-b (streaming:source-read src-b t)
;; Get state values
dir (get cfg :dir)
rot-max-a (get cfg :rot-a)
rot-max-b (get cfg :rot-b)
zoom-a (get cfg :zoom-a)
zoom-b (get cfg :zoom-b)
pair-angle (get pstate :angle)
inv-a-on (> (get pstate :inv-a) 0)
inv-b-on (> (get pstate :inv-b) 0)
hue-a-on (> (get pstate :hue-a) 0)
hue-b-on (> (get pstate :hue-b) 0)
hue-a-val (get pstate :hue-a-val)
hue-b-val (get pstate :hue-b-val)
mix-ratio (get pstate :mix)
;; Calculate rotation angles
angle-a (* dir pair-angle rot-max-a 0.01)
angle-b (* dir pair-angle rot-max-b 0.01)
;; Define effect pipelines for each source
;; These get compiled to single CUDA kernels!
effects-a [{:op "zoom" :amount zoom-a}
{:op "rotate" :angle angle-a}
{:op "hue_shift" :degrees (if hue-a-on hue-a-val 0)}
{:op "invert" :amount (if inv-a-on 1 0)}]
effects-b [{:op "zoom" :amount zoom-b}
{:op "rotate" :angle angle-b}
{:op "hue_shift" :degrees (if hue-b-on hue-b-val 0)}
{:op "invert" :amount (if inv-b-on 1 0)}]
;; Apply fused pipelines (single kernel per source!)
processed-a (streaming_gpu:fused-pipeline frame-a effects-a)
processed-b (streaming_gpu:fused-pipeline frame-b effects-b)]
;; Blend the two processed frames
(blending:blend-images processed-a processed-b mix-ratio)))
;; === FRAME PIPELINE ===
(frame
(let [now t
e (streaming:audio-energy music now)
;; Get cycle state
active (bind cycle :active)
beat-pos (bind cycle :beat)
clen (bind cycle :clen)
;; Transition logic
phase3 (* beat-pos 3)
fading (and (>= phase3 (* clen 2)) (< phase3 (* clen 3)))
fade-amt (if fading (/ (- phase3 (* clen 2)) clen) 0)
next-idx (mod (+ active 1) (len sources))
;; Process active pair with fused pipeline
active-frame (process-pair-fast active)
;; Crossfade during transition
result (if fading
(let [next-frame (process-pair-fast next-idx)]
(blending:blend-images active-frame next-frame fade-amt))
active-frame)
;; Final effects pipeline (fused!)
spin-angle (bind spin :angle)
rip-gate (bind ripple-state :gate)
rip-amp (* rip-gate (core:map-range e 0 1 5 50))
rip-cx (bind ripple-state :cx)
rip-cy (bind ripple-state :cy)
;; Fused final effects
final-effects [{:op "rotate" :angle spin-angle}
{:op "ripple" :amplitude rip-amp :frequency 8 :decay 2
:phase (* now 5) :center_x rip-cx :center_y rip-cy}]]
;; Apply final fused pipeline
(streaming_gpu:fused-pipeline result final-effects
:rotate_angle spin-angle
:ripple_phase (* now 5)))))