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>
91 lines
2.3 KiB
Python
91 lines
2.3 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy"]
|
|
# ///
|
|
"""
|
|
@effect strobe
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Strobe / Posterize Time effect. Locks video to a reduced frame rate,
|
|
creating a choppy, stop-motion look. Also known as frame hold.
|
|
|
|
@param frame_rate float
|
|
@range 1 60
|
|
@default 12
|
|
Target frame rate in fps. Lower = choppier.
|
|
|
|
@param sync_to_beat bool
|
|
@default false
|
|
If true, hold frames until next beat (overrides frame_rate).
|
|
|
|
@param beat_divisor int
|
|
@range 1 8
|
|
@default 1
|
|
Hold for 1/N beats when sync_to_beat is true.
|
|
|
|
@state held_frame ndarray
|
|
Currently held frame.
|
|
|
|
@state held_until float
|
|
Time until which to hold the frame.
|
|
|
|
@example
|
|
(effect strobe :frame_rate 8)
|
|
|
|
@example
|
|
;; Very choppy at 4 fps
|
|
(effect strobe :frame_rate 4)
|
|
|
|
@example
|
|
;; Beat-synced frame hold
|
|
(effect strobe :sync_to_beat true :beat_divisor 2)
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply strobe/posterize time effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- frame_rate: target fps 1-60 (default 12)
|
|
- sync_to_beat: use beat timing (default False)
|
|
- beat_divisor: beat fraction (default 1)
|
|
state: Persistent state dict
|
|
- held_frame: currently held frame
|
|
- held_until: hold expiry time
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
target_fps = max(1, min(params.get("frame_rate", 12), 60))
|
|
sync_to_beat = params.get("sync_to_beat", False)
|
|
beat_divisor = max(1, int(params.get("beat_divisor", 1)))
|
|
|
|
# Get current time from params (executor should provide this)
|
|
t = params.get("_time", 0)
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
# Initialize state
|
|
if "held_frame" not in state:
|
|
state["held_frame"] = None
|
|
state["held_until"] = 0.0
|
|
state["last_beat"] = -1
|
|
|
|
# Frame rate based hold
|
|
frame_duration = 1.0 / target_fps
|
|
|
|
if t >= state["held_until"]:
|
|
# Time for new frame
|
|
state["held_frame"] = frame.copy()
|
|
state["held_until"] = t + frame_duration
|
|
|
|
return state["held_frame"] if state["held_frame"] is not None else frame, state
|