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:
217
effects/random.py
Normal file
217
effects/random.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["numpy"]
|
||||
# ///
|
||||
"""
|
||||
@effect random
|
||||
@version 1.0.0
|
||||
@author artdag
|
||||
|
||||
@description
|
||||
Deterministic random number generator for use in recipes and effects.
|
||||
Given the same seed, produces the same sequence of values every time.
|
||||
|
||||
This effect doesn't modify the frame - it provides random values that
|
||||
can be bound to other effect parameters. The random state persists
|
||||
across frames for consistent sequences.
|
||||
|
||||
@param seed int
|
||||
@default 42
|
||||
Random seed for reproducibility. Same seed = same sequence.
|
||||
|
||||
@param min float
|
||||
@default 0
|
||||
Minimum output value.
|
||||
|
||||
@param max float
|
||||
@default 1
|
||||
Maximum output value.
|
||||
|
||||
@param mode string
|
||||
@enum uniform gaussian integer choice
|
||||
@default uniform
|
||||
Distribution type:
|
||||
- uniform: even distribution between min and max
|
||||
- gaussian: normal distribution (min=mean, max=stddev)
|
||||
- integer: random integers between min and max (inclusive)
|
||||
- choice: randomly pick from a list (use choices param)
|
||||
|
||||
@param choices list
|
||||
@default []
|
||||
List of values to choose from (for mode=choice).
|
||||
|
||||
@param step_every int
|
||||
@default 1
|
||||
Only generate new value every N frames (1 = every frame).
|
||||
|
||||
@state rng RandomState
|
||||
Numpy random state for deterministic sequence.
|
||||
|
||||
@state frame_count int
|
||||
Tracks frames for step_every.
|
||||
|
||||
@state current_value float
|
||||
Current random value (persists between steps).
|
||||
|
||||
@example
|
||||
;; Random value 0-1 each frame
|
||||
(bind (random :seed 123))
|
||||
|
||||
@example
|
||||
;; Random integer 1-10, changes every 5 frames
|
||||
(random :seed 42 :mode "integer" :min 1 :max 10 :step_every 5)
|
||||
|
||||
@example
|
||||
;; Gaussian noise around 0.5
|
||||
(random :mode "gaussian" :min 0.5 :max 0.1)
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
||||
"""
|
||||
Generate deterministic random values.
|
||||
|
||||
This effect passes through the frame unchanged but updates state
|
||||
with random values that can be used by the recipe/executor.
|
||||
|
||||
Args:
|
||||
frame: Input frame (passed through unchanged)
|
||||
params: Effect parameters
|
||||
- seed: random seed (default 42)
|
||||
- min: minimum value (default 0)
|
||||
- max: maximum value (default 1)
|
||||
- mode: uniform/gaussian/integer/choice (default uniform)
|
||||
- choices: list for choice mode
|
||||
- step_every: frames between new values (default 1)
|
||||
state: Persistent state dict
|
||||
- rng: numpy RandomState
|
||||
- frame_count: frame counter
|
||||
- current_value: last generated value
|
||||
|
||||
Returns:
|
||||
Tuple of (frame, state_with_random_value)
|
||||
"""
|
||||
seed = int(params.get("seed", 42))
|
||||
min_val = params.get("min", 0)
|
||||
max_val = params.get("max", 1)
|
||||
mode = params.get("mode", "uniform")
|
||||
choices = params.get("choices", [])
|
||||
step_every = max(1, int(params.get("step_every", 1)))
|
||||
|
||||
if state is None:
|
||||
state = {}
|
||||
|
||||
# Initialize RNG on first call
|
||||
if "rng" not in state:
|
||||
state["rng"] = np.random.RandomState(seed)
|
||||
state["frame_count"] = 0
|
||||
state["current_value"] = None
|
||||
|
||||
rng = state["rng"]
|
||||
frame_count = state["frame_count"]
|
||||
|
||||
# Generate new value if needed
|
||||
if frame_count % step_every == 0 or state["current_value"] is None:
|
||||
if mode == "uniform":
|
||||
value = rng.uniform(min_val, max_val)
|
||||
elif mode == "gaussian":
|
||||
# min = mean, max = stddev
|
||||
value = rng.normal(min_val, max_val)
|
||||
elif mode == "integer":
|
||||
value = rng.randint(int(min_val), int(max_val) + 1)
|
||||
elif mode == "choice" and choices:
|
||||
value = choices[rng.randint(0, len(choices))]
|
||||
else:
|
||||
value = rng.uniform(min_val, max_val)
|
||||
|
||||
state["current_value"] = value
|
||||
|
||||
state["frame_count"] = frame_count + 1
|
||||
|
||||
# Store value in state for recipe access
|
||||
state["value"] = state["current_value"]
|
||||
|
||||
return frame, state
|
||||
|
||||
|
||||
# Standalone RNG class for use in other effects
|
||||
class DeterministicRNG:
|
||||
"""
|
||||
Deterministic random number generator for use in effects.
|
||||
|
||||
Usage in effects:
|
||||
from effects.random import DeterministicRNG
|
||||
|
||||
def process_frame(frame, params, state):
|
||||
if "rng" not in state:
|
||||
state["rng"] = DeterministicRNG(params.get("seed", 42))
|
||||
rng = state["rng"]
|
||||
|
||||
value = rng.uniform(0, 1)
|
||||
integer = rng.randint(0, 10)
|
||||
choice = rng.choice(["a", "b", "c"])
|
||||
"""
|
||||
|
||||
def __init__(self, seed: int = 42):
|
||||
"""Initialize with seed for reproducibility."""
|
||||
self._rng = np.random.RandomState(seed)
|
||||
self._seed = seed
|
||||
|
||||
def seed(self, seed: int):
|
||||
"""Reset with new seed."""
|
||||
self._rng = np.random.RandomState(seed)
|
||||
self._seed = seed
|
||||
|
||||
def uniform(self, low: float = 0, high: float = 1) -> float:
|
||||
"""Random float in [low, high)."""
|
||||
return self._rng.uniform(low, high)
|
||||
|
||||
def randint(self, low: int, high: int) -> int:
|
||||
"""Random integer in [low, high]."""
|
||||
return self._rng.randint(low, high + 1)
|
||||
|
||||
def gaussian(self, mean: float = 0, stddev: float = 1) -> float:
|
||||
"""Random float from normal distribution."""
|
||||
return self._rng.normal(mean, stddev)
|
||||
|
||||
def choice(self, items: list):
|
||||
"""Random choice from list."""
|
||||
if not items:
|
||||
return None
|
||||
return items[self._rng.randint(0, len(items))]
|
||||
|
||||
def shuffle(self, items: list) -> list:
|
||||
"""Return shuffled copy of list."""
|
||||
result = list(items)
|
||||
self._rng.shuffle(result)
|
||||
return result
|
||||
|
||||
def sample(self, items: list, n: int) -> list:
|
||||
"""Random sample of n items without replacement."""
|
||||
if n >= len(items):
|
||||
return self.shuffle(items)
|
||||
indices = self._rng.choice(len(items), n, replace=False)
|
||||
return [items[i] for i in indices]
|
||||
|
||||
def weighted_choice(self, items: list, weights: list):
|
||||
"""Random choice with weights."""
|
||||
if not items or not weights:
|
||||
return None
|
||||
weights = np.array(weights, dtype=float)
|
||||
weights /= weights.sum()
|
||||
idx = self._rng.choice(len(items), p=weights)
|
||||
return items[idx]
|
||||
|
||||
@property
|
||||
def state(self) -> dict:
|
||||
"""Get RNG state for serialization."""
|
||||
return {"seed": self._seed, "state": self._rng.get_state()}
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state: dict) -> 'DeterministicRNG':
|
||||
"""Restore RNG from serialized state."""
|
||||
rng = cls(state["seed"])
|
||||
rng._rng.set_state(state["state"])
|
||||
return rng
|
||||
Reference in New Issue
Block a user