Files
test/effects/strobe.py
gilesb 406cc7c0c7 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>
2026-01-19 12:34:45 +00:00

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