;; cycle-effects-preset construct ;; Data-driven effect cycling using preset definitions ;; ;; Preset format (flat, no nested :params): ;; {:effect "brightness" :amount {:bind "bass" :range [-80 80]}} ;; {:effect "blur" :radius 5} ;; ;; Binding specs {:bind "analyzer" :range [min max]} are resolved to actual bindings (define-construct cycle-effects-preset :params ( (preset :type any :desc "List of effect preset definitions") (videos :type any :desc "List of video source nodes") (video_infos :type any :desc "List of video info analysis results") (beats :type any :desc "Beat analysis data with :times") (beats_per_segment :type int :default 4 :desc "Number of beats per segment") ) (let [num-effects (len preset) num-videos (len videos) ;; Extract durations from video-info analysis results durations (map (fn [info] (get info :duration)) video_infos) times (get beats :times) grouped (chunk-every times beats_per_segment) ;; Resolve a param value - if it's a binding spec dict, create actual Binding ;; Note: pass the analyzer NAME (string) to bind, not the data - it will be ;; looked up at execution time in analysis_data resolve-param (fn [param-value] (if (dict? param-value) (let [bind-name (get param-value :bind)] (if bind-name (let [range-spec (get param-value :range (list 0 1))] (bind bind-name :range range-spec)) param-value)) param-value)) ;; Process effect spec - resolve any binding specs in params ;; Effect spec is flat: {:effect "name" :param1 val1 :param2 {:bind ...}} make-effect (fn [effect-spec] (let [effect-name (get effect-spec :effect)] ;; Build effect dict with resolved params ;; Since we can't iterate dict keys, we check known params ;; Only include params that exist (non-nil) in the spec (let [result {:effect effect-name} ;; Check each known param amount (get effect-spec :amount nil) degrees (get effect-spec :degrees nil) speed (get effect-spec :speed nil) level (get effect-spec :level nil) levels (get effect-spec :levels nil) radius (get effect-spec :radius nil) intensity (get effect-spec :intensity nil) contrast (get effect-spec :contrast nil) brightness (get effect-spec :brightness nil) strength (get effect-spec :strength nil) amplitude (get effect-spec :amplitude nil) wavelength (get effect-spec :wavelength nil) frequency (get effect-spec :frequency nil) segments-p (get effect-spec :segments nil) rotation_speed (get effect-spec :rotation_speed nil) factor (get effect-spec :factor nil) angle (get effect-spec :angle nil) direction (get effect-spec :direction nil) block_size (get effect-spec :block_size nil) char_size (get effect-spec :char_size nil) color_mode (get effect-spec :color_mode nil) low (get effect-spec :low nil) high (get effect-spec :high nil) thickness (get effect-spec :thickness nil) glow_radius (get effect-spec :glow_radius nil) glow_intensity (get effect-spec :glow_intensity nil) line_spacing (get effect-spec :line_spacing nil) vignette_amount (get effect-spec :vignette_amount nil) spacing (get effect-spec :spacing nil) offset_x (get effect-spec :offset_x nil) num_echoes (get effect-spec :num_echoes nil) decay (get effect-spec :decay nil) persistence (get effect-spec :persistence nil) rows (get effect-spec :rows nil) cols (get effect-spec :cols nil) threshold_low (get effect-spec :threshold_low nil) threshold_high (get effect-spec :threshold_high nil) corruption (get effect-spec :corruption nil)] ;; Only add non-nil params to result ;; Use cond to build up the dict (since we can't dynamically add keys) ;; This is ugly but necessary without dict iteration {:effect effect-name :amount (if (nil? amount) nil (resolve-param amount)) :degrees (if (nil? degrees) nil (resolve-param degrees)) :speed speed :level level :levels levels :radius (if (nil? radius) nil (resolve-param radius)) :intensity (if (nil? intensity) nil (resolve-param intensity)) :contrast (if (nil? contrast) nil (resolve-param contrast)) :brightness (if (nil? brightness) nil (resolve-param brightness)) :strength (if (nil? strength) nil (resolve-param strength)) :amplitude (if (nil? amplitude) nil (resolve-param amplitude)) :wavelength wavelength :frequency frequency :segments segments-p :rotation_speed rotation_speed :factor (if (nil? factor) nil (resolve-param factor)) :angle (if (nil? angle) nil (resolve-param angle)) :direction direction :block_size (if (nil? block_size) nil (resolve-param block_size)) :char_size char_size :color_mode color_mode :low low :high high :thickness thickness :glow_radius glow_radius :glow_intensity glow_intensity :line_spacing line_spacing :vignette_amount (if (nil? vignette_amount) nil (resolve-param vignette_amount)) :spacing spacing :offset_x (if (nil? offset_x) nil (resolve-param offset_x)) :num_echoes num_echoes :decay decay :persistence persistence :rows rows :cols cols :threshold_low threshold_low :threshold_high threshold_high :corruption (if (nil? corruption) nil (resolve-param corruption))}))) find-valid-video (fn [preferred-idx seg-duration] (cond (>= (nth durations preferred-idx) seg-duration) preferred-idx (>= (nth durations (mod (+ preferred-idx 1) num-videos)) seg-duration) (mod (+ preferred-idx 1) num-videos) (>= (nth durations (mod (+ preferred-idx 2) num-videos)) seg-duration) (mod (+ preferred-idx 2) num-videos) :else nil))] (nth (reduce (fn [state group] (let [acc (first state) segments (nth state 1) audio-start (first group) audio-end (last group) seg-duration (- audio-end audio-start) vid-idx (find-valid-video (mod acc num-videos) seg-duration)] (if (nil? vid-idx) (list (inc acc) segments) (let [src (nth videos vid-idx) src-duration (nth durations vid-idx) wrapped-start (mod audio-start src-duration) effect-idx (mod acc num-effects) effect-spec (nth preset effect-idx) fx (make-effect effect-spec) segment (dict :source src :start wrapped-start :duration seg-duration :effects (list fx))] (list (inc acc) (append segments segment)))))) (list 0 (list)) grouped) 1)))