From e3c6163e2e903c2a06502c9c8491b878573f5d26 Mon Sep 17 00:00:00 2001 From: gilesb Date: Tue, 20 Jan 2026 09:19:40 +0000 Subject: [PATCH] Add ascii_dual_blend recipe and fix blend/layer effects - Add ascii_dual_blend.sexp: blends two ASCII-processed videos synced to audio - Fix blend.sexp: add require-primitives, fix params syntax - Fix layer.sexp: add require-primitives - Use consistent (effect blend ...) syntax instead of special form Co-Authored-By: Claude Opus 4.5 --- effects/ascii_dual_blend.sexp | 101 ++++++++++++++++++++++++++++++++ sexp_effects/effects/blend.sexp | 26 ++++---- sexp_effects/effects/layer.sexp | 2 + 3 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 effects/ascii_dual_blend.sexp diff --git a/effects/ascii_dual_blend.sexp b/effects/ascii_dual_blend.sexp new file mode 100644 index 0000000..cd36b5b --- /dev/null +++ b/effects/ascii_dual_blend.sexp @@ -0,0 +1,101 @@ +;; ASCII Dual Blend +;; +;; Applies ASCII alternating rotation effect to two video sources, +;; blends them together, and muxes with audio. +;; All synced to the same audio analysis. + +(recipe "ascii_dual_blend" + :version "1.0" + :description "Blend two ASCII-processed videos synced to audio" + :minimal-primitives true + :encoding (:codec "libx264" :crf 20 :preset "medium" :audio-codec "aac" :fps 30) + + :params ( + (cols :type int :default 50 :range [20 100] + :desc "Number of character columns") + (rotation_scale :type float :default 60 :range [0 180] + :desc "Max rotation in degrees") + (blend_opacity :type float :default 0.5 :range [0 1] + :desc "Blend opacity (0=video-a only, 1=video-b only)") + (blend_mode :type string :default "overlay" + :desc "Blend mode: alpha, add, multiply, screen, overlay, difference") + (audio_start :type float :default 60 + :desc "Start time in audio (seconds)") + (duration :type float :default 10 + :desc "Duration (seconds)") + ) + + ;; Registry - effects and analyzers + (effect ascii_fx_zone :path "../sexp_effects/effects/ascii_fx_zone.sexp") + (effect rotate :path "../sexp_effects/effects/rotate.sexp") + (effect blend :path "../sexp_effects/effects/blend.sexp") + (analyzer energy :path "../../artdag-analyzers/energy/analyzer.py") + + ;; Source files + (def video-a (source :path "../monday.webm")) + (def video-b (source :path "../new.webm")) + (def audio (source :path "../dizzy.mp3")) + + ;; Stage 1: Analysis + (stage :analyze + :outputs [energy-data] + (def audio-clip (-> audio (segment :start audio_start :duration duration))) + (def energy-data (-> audio-clip (analyze energy)))) + + ;; Stage 2: Process both videos + (stage :process + :requires [:analyze] + :inputs [energy-data] + :outputs [blended audio-clip] + + ;; Get audio clip for final mux + (def audio-clip (-> audio (segment :start audio_start :duration duration))) + + ;; Process video A with ASCII effect + (def clip-a (-> video-a (segment :start 0 :duration duration))) + (def ascii-a (-> clip-a + (effect ascii_fx_zone + :cols cols + :char_size (bind energy-data values :range [10 20]) + :color_mode "color" + :background "black" + :energy (bind energy-data values :range [0 1]) + :rotation_scale rotation_scale + :cell_effect (lambda [cell zone] + (rotate cell + (* (if (= (mod (+ (get zone "row") (get zone "col")) 2) 0) 1 -1) + (* (get zone "energy") + (get zone "rotation_scale") + (* 1.5 (+ (get zone "col-norm") + (- 1 (get zone "row-norm"))))))))))) + + ;; Process video B with ASCII effect + (def clip-b (-> video-b (segment :start 0 :duration duration))) + (def ascii-b (-> clip-b + (effect ascii_fx_zone + :cols cols + :char_size (bind energy-data values :range [10 20]) + :color_mode "color" + :background "black" + :energy (bind energy-data values :range [0 1]) + :rotation_scale rotation_scale + :cell_effect (lambda [cell zone] + (rotate cell + (* (if (= (mod (+ (get zone "row") (get zone "col")) 2) 0) 1 -1) + (* (get zone "energy") + (get zone "rotation_scale") + (* 1.5 (+ (get zone "col-norm") + (- 1 (get zone "row-norm"))))))))))) + + ;; Blend the two ASCII videos using consistent effect syntax + (def blended (-> ascii-a + (effect blend ascii-b + :mode blend_mode + :opacity blend_opacity + :resize_mode "fit")))) + + ;; Stage 3: Output + (stage :output + :requires [:process] + :inputs [blended audio-clip] + (mux blended audio-clip))) diff --git a/sexp_effects/effects/blend.sexp b/sexp_effects/effects/blend.sexp index b8f6850..893c49e 100644 --- a/sexp_effects/effects/blend.sexp +++ b/sexp_effects/effects/blend.sexp @@ -3,17 +3,19 @@ ;; Params: ;; mode - blend mode (add, multiply, screen, overlay, difference, lighten, darken, alpha) ;; opacity - blend amount (0-1) -;; resize-mode - how to resize frame-b to match frame-a (fit, crop, stretch) +;; resize_mode - how to resize frame-b to match frame-a (fit, crop, stretch) ;; priority - which dimension takes priority (width, height) -;; pad-color - color for padding in fit mode [r g b] +;; pad_color - color for padding in fit mode [r g b] + +(require-primitives "image" "blending") (define-effect blend :params ( (mode :type string :default "overlay") (opacity :type float :default 0.5) + (resize_mode :type string :default "fit") (priority :type string :default "width") - (list :type string :default 0 0 0) - ) + (pad_color :type list :default [0 0 0]) ) (let [a frame-a a-w (width a) @@ -24,31 +26,31 @@ ;; Calculate scale based on resize mode and priority scale-w (/ a-w b-w) scale-h (/ a-h b-h) - scale (if (= resize-mode "stretch") + scale (if (= resize_mode "stretch") 1 ;; Will use explicit dimensions - (if (= resize-mode "crop") + (if (= resize_mode "crop") (max scale-w scale-h) ;; Scale to cover, then crop (if (= priority "width") scale-w scale-h))) ;; For stretch, use target dimensions directly - new-w (if (= resize-mode "stretch") a-w (round (* b-w scale))) - new-h (if (= resize-mode "stretch") a-h (round (* b-h scale))) + new-w (if (= resize_mode "stretch") a-w (round (* b-w scale))) + new-h (if (= resize_mode "stretch") a-h (round (* b-h scale))) ;; Resize b b-resized (resize b-raw new-w new-h "linear") ;; Handle fit (pad) or crop to exact size - b (if (= resize-mode "crop") + b (if (= resize_mode "crop") ;; Crop to center (let [cx (/ (- new-w a-w) 2) cy (/ (- new-h a-h) 2)] (crop b-resized cx cy a-w a-h)) - (if (and (= resize-mode "fit") (or (!= new-w a-w) (!= new-h a-h))) + (if (and (= resize_mode "fit") (or (!= new-w a-w) (!= new-h a-h))) ;; Pad to center (let [pad-x (/ (- a-w new-w) 2) pad-y (/ (- a-h new-h) 2) - canvas (make-image a-w a-h pad-color)] + canvas (make-image a-w a-h pad_color)] (paste canvas b-resized pad-x pad-y)) b-resized))] (if (= mode "alpha") (blend-images a b opacity) - (blend-images a (blend-mode a b mode) opacity))) + (blend-images a (blend-mode a b mode) opacity)))) diff --git a/sexp_effects/effects/layer.sexp b/sexp_effects/effects/layer.sexp index 9915407..30ba927 100644 --- a/sexp_effects/effects/layer.sexp +++ b/sexp_effects/effects/layer.sexp @@ -2,6 +2,8 @@ ;; Multi-input effect: uses frame-a (background) and frame-b (overlay) ;; Params: x, y (position), opacity (0-1), mode (blend mode) +(require-primitives "image" "blending") + (define-effect layer :params ( (x :type int :default 0)