""" Effect metadata types. Defines the core dataclasses for effect metadata: - ParamSpec: Parameter specification with type, range, and default - EffectMeta: Complete effect metadata including params and flags """ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Tuple, Type, Union @dataclass class ParamSpec: """ Specification for an effect parameter. Attributes: name: Parameter name (used in recipes as :name) param_type: Python type (float, int, bool, str) default: Default value if not specified range: Optional (min, max) tuple for numeric types description: Human-readable description choices: Optional list of allowed values (for enums) """ name: str param_type: Type default: Any = None range: Optional[Tuple[float, float]] = None description: str = "" choices: Optional[List[Any]] = None def validate(self, value: Any) -> Any: """ Validate and coerce a parameter value. Args: value: Input value to validate Returns: Validated and coerced value Raises: ValueError: If value is invalid """ if value is None: if self.default is not None: return self.default raise ValueError(f"Parameter '{self.name}' requires a value") # Type coercion try: if self.param_type == bool: if isinstance(value, str): value = value.lower() in ("true", "1", "yes") else: value = bool(value) elif self.param_type == int: value = int(value) elif self.param_type == float: value = float(value) elif self.param_type == str: value = str(value) else: value = self.param_type(value) except (ValueError, TypeError) as e: raise ValueError( f"Parameter '{self.name}' expects {self.param_type.__name__}, " f"got {type(value).__name__}: {e}" ) # Range check for numeric types if self.range is not None and self.param_type in (int, float): min_val, max_val = self.range if value < min_val or value > max_val: raise ValueError( f"Parameter '{self.name}' must be in range " f"[{min_val}, {max_val}], got {value}" ) # Choices check if self.choices is not None and value not in self.choices: raise ValueError( f"Parameter '{self.name}' must be one of {self.choices}, got {value}" ) return value def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" d = { "name": self.name, "type": self.param_type.__name__, "description": self.description, } if self.default is not None: d["default"] = self.default if self.range is not None: d["range"] = list(self.range) if self.choices is not None: d["choices"] = self.choices return d @dataclass class EffectMeta: """ Complete metadata for an effect. Attributes: name: Effect name (used in recipes) version: Semantic version string temporal: If True, effect needs complete input (can't be collapsed) params: List of parameter specifications author: Optional author identifier description: Human-readable description examples: List of example S-expression usages dependencies: List of Python package dependencies requires_python: Minimum Python version api_type: "frame" for frame-by-frame, "video" for whole-video """ name: str version: str = "1.0.0" temporal: bool = False params: List[ParamSpec] = field(default_factory=list) author: str = "" description: str = "" examples: List[str] = field(default_factory=list) dependencies: List[str] = field(default_factory=list) requires_python: str = ">=3.10" api_type: str = "frame" # "frame" or "video" def get_param(self, name: str) -> Optional[ParamSpec]: """Get a parameter spec by name.""" for param in self.params: if param.name == name: return param return None def validate_params(self, params: Dict[str, Any]) -> Dict[str, Any]: """ Validate all parameters. Args: params: Dictionary of parameter values Returns: Dictionary with validated/coerced values including defaults Raises: ValueError: If any parameter is invalid """ result = {} for spec in self.params: value = params.get(spec.name) result[spec.name] = spec.validate(value) return result def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "name": self.name, "version": self.version, "temporal": self.temporal, "params": [p.to_dict() for p in self.params], "author": self.author, "description": self.description, "examples": self.examples, "dependencies": self.dependencies, "requires_python": self.requires_python, "api_type": self.api_type, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "EffectMeta": """Create from dictionary.""" params = [] for p in data.get("params", []): # Map type name back to Python type type_map = {"float": float, "int": int, "bool": bool, "str": str} param_type = type_map.get(p.get("type", "float"), float) params.append( ParamSpec( name=p["name"], param_type=param_type, default=p.get("default"), range=tuple(p["range"]) if p.get("range") else None, description=p.get("description", ""), choices=p.get("choices"), ) ) return cls( name=data["name"], version=data.get("version", "1.0.0"), temporal=data.get("temporal", False), params=params, author=data.get("author", ""), description=data.get("description", ""), examples=data.get("examples", []), dependencies=data.get("dependencies", []), requires_python=data.get("requires_python", ">=3.10"), api_type=data.get("api_type", "frame"), ) @dataclass class ExecutionContext: """ Context passed to effect execution. Provides controlled access to resources within sandbox. """ input_paths: List[str] output_path: str params: Dict[str, Any] seed: int # Deterministic seed for RNG frame_rate: float = 30.0 width: int = 1920 height: int = 1080 # Resolved bindings (frame -> param value lookup) bindings: Dict[str, List[float]] = field(default_factory=dict) def get_param_at_frame(self, param_name: str, frame: int) -> Any: """ Get parameter value at a specific frame. If parameter has a binding, looks up the bound value. Otherwise returns the static parameter value. """ if param_name in self.bindings: binding_values = self.bindings[param_name] if frame < len(binding_values): return binding_values[frame] # Past end of binding data, use last value return binding_values[-1] if binding_values else self.params.get(param_name) return self.params.get(param_name) def get_rng(self) -> "random.Random": """Get a seeded random number generator.""" import random return random.Random(self.seed)