Initial commit: video effects processing system
Add S-expression based video effects pipeline with modular effect definitions, constructs, and recipe files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
21
constructs/beat-alternate.sexp
Normal file
21
constructs/beat-alternate.sexp
Normal file
@@ -0,0 +1,21 @@
|
||||
;; beat-alternate construct
|
||||
;; Alternates between sources on each beat
|
||||
;;
|
||||
;; Usage in recipe:
|
||||
;; (construct beat-alternate :path "constructs/beat-alternate.sexp")
|
||||
;; (def segments (beat-alternate beats-data (list video-a video-b)))
|
||||
|
||||
(define-construct beat-alternate
|
||||
"Alternate between sources on each beat"
|
||||
(analysis sources)
|
||||
;; Body: map over time pairs, return segment descriptors
|
||||
(let [times (get analysis :times)
|
||||
pairs (zip-pairs (cons 0 times))
|
||||
n-sources (len sources)]
|
||||
(map-indexed
|
||||
(fn [i pair]
|
||||
(dict :source (nth sources (mod i n-sources))
|
||||
:start (first pair)
|
||||
:end (nth pair 1)
|
||||
:effects (list)))
|
||||
pairs)))
|
||||
153
constructs/cycle-effects-preset.sexp
Normal file
153
constructs/cycle-effects-preset.sexp
Normal file
@@ -0,0 +1,153 @@
|
||||
;; 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
|
||||
"Cycle through effects from a data preset, with automatic binding resolution"
|
||||
()
|
||||
(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)))
|
||||
53
constructs/slice-every-n.sexp
Normal file
53
constructs/slice-every-n.sexp
Normal file
@@ -0,0 +1,53 @@
|
||||
;; slice-every-n construct - group every N beats into one segment
|
||||
;;
|
||||
;; Usage:
|
||||
;; (construct slice-every-n :path "constructs/slice-every-n.sexp")
|
||||
;; (def segments (slice-every-n beats-data 4
|
||||
;; :init 0
|
||||
;; :reducer (fn [acc i start end]
|
||||
;; {:source video-a
|
||||
;; :effects (list {:effect invert})
|
||||
;; :acc (inc acc)})))
|
||||
;;
|
||||
;; Groups every N analysis times into one segment, calling reducer once per group
|
||||
|
||||
(define-construct slice-every-n
|
||||
"Group every N analysis beats into segments"
|
||||
(analysis n)
|
||||
;; 'init' and 'reducer' come from keyword args
|
||||
;; Reducer receives: (acc, i, start, end) where start/end are audio beat times
|
||||
;; Reducer returns: {:source src :effects fx :acc new-acc}
|
||||
;; Optionally include :start/:end to override (e.g., for wrapping/randomizing)
|
||||
;; :duration is calculated from start/end (use :duration to override)
|
||||
;; Return :skip true to skip this segment
|
||||
(let [times (get analysis :times)
|
||||
;; Group times into chunks of n
|
||||
grouped (chunk-every times n)]
|
||||
(nth
|
||||
(reduce
|
||||
(fn [state group]
|
||||
(let [acc (first state)
|
||||
segments (nth state 1)
|
||||
i (len segments)
|
||||
audio-start (first group)
|
||||
audio-end (last group)
|
||||
audio-duration (- audio-end audio-start)
|
||||
;; Call user's reducer with audio beat times
|
||||
result (reducer acc i audio-start audio-end)
|
||||
new-acc (get result :acc)]
|
||||
;; Skip if reducer returns :skip true
|
||||
(if (get result :skip false)
|
||||
(list new-acc segments)
|
||||
(let [;; Use reducer's start/end/duration if provided, else use audio times
|
||||
seg-start (get result :start audio-start)
|
||||
seg-end (get result :end audio-end)
|
||||
seg-duration (get result :duration (- seg-end seg-start))
|
||||
segment (dict :source (get result :source)
|
||||
:start seg-start
|
||||
:end seg-end
|
||||
:duration seg-duration
|
||||
:effects (get result :effects))]
|
||||
(list new-acc (append segments segment))))))
|
||||
(list init (list))
|
||||
grouped)
|
||||
1)))
|
||||
45
constructs/slice-on.sexp
Normal file
45
constructs/slice-on.sexp
Normal file
@@ -0,0 +1,45 @@
|
||||
;; slice-on construct - iterate over analysis times with user function
|
||||
;;
|
||||
;; Usage:
|
||||
;; (construct slice-on :path "constructs/slice-on.sexp")
|
||||
;; (def segments (slice-on beats-data
|
||||
;; :init 0
|
||||
;; :reducer (fn [acc i start end]
|
||||
;; {:source (nth (list video-a video-b) (mod acc 2))
|
||||
;; :effects (list)
|
||||
;; :acc (inc acc)})))
|
||||
;;
|
||||
;; The construct receives:
|
||||
;; - First positional arg as 'analysis' (the analysis data with :times)
|
||||
;; - :init as 'init' (initial accumulator value)
|
||||
;; - :reducer as 'reducer' (the reducer lambda)
|
||||
|
||||
(define-construct slice-on
|
||||
"Iterate over analysis times, calling reducer for each slice"
|
||||
(analysis)
|
||||
;; 'init' and 'reducer' come from keyword args
|
||||
;; Get times from analysis data
|
||||
(let [times (get analysis :times)
|
||||
pairs (zip-pairs (cons 0 times))]
|
||||
;; Use nth to get second element of reduce result (the segments list)
|
||||
(nth
|
||||
(reduce
|
||||
(fn [state pair]
|
||||
(let [acc (first state)
|
||||
segments (nth state 1)
|
||||
i (len segments)
|
||||
start (first pair)
|
||||
end (nth pair 1)
|
||||
;; Call user's reducer function
|
||||
result (reducer acc i start end)
|
||||
;; Extract new acc and build segment
|
||||
new-acc (get result :acc)
|
||||
segment (dict :source (get result :source)
|
||||
:start start
|
||||
:end end
|
||||
:duration (- end start)
|
||||
:effects (get result :effects))]
|
||||
(list new-acc (append segments segment))))
|
||||
(list init (list))
|
||||
pairs)
|
||||
1)))
|
||||
Reference in New Issue
Block a user