# /// 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