feat(pipeline): add unified pipeline architecture with Stage abstraction
- Add engine/pipeline/ module with Stage ABC, PipelineContext, PipelineParams - Stage provides unified interface for sources, effects, displays, cameras - Pipeline class handles DAG-based execution with dependency resolution - PipelinePreset for pre-configured pipelines (demo, poetry, pipeline, etc.) - Add PipelineParams as params layer for animation-driven config - Add StageRegistry for unified stage registration - Add sources_v2.py with DataSource.is_dynamic property - Add animation.py with Preset and AnimationController - Skip ntfy integration tests by default (require -m integration) - Skip e2e tests by default (require -m e2e) - Update pipeline.py with comprehensive introspection methods
This commit is contained in:
107
engine/pipeline/__init__.py
Normal file
107
engine/pipeline/__init__.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Unified Pipeline Architecture.
|
||||
|
||||
This module provides a clean, dependency-managed pipeline system:
|
||||
- Stage: Base class for all pipeline components
|
||||
- Pipeline: DAG-based execution orchestrator
|
||||
- PipelineParams: Runtime configuration for animation
|
||||
- PipelinePreset: Pre-configured pipeline configurations
|
||||
- StageRegistry: Unified registration for all stage types
|
||||
|
||||
The pipeline architecture supports:
|
||||
- Sources: Data providers (headlines, poetry, pipeline viz)
|
||||
- Effects: Post-processors (noise, fade, glitch, hud)
|
||||
- Displays: Output backends (terminal, pygame, websocket)
|
||||
- Cameras: Viewport controllers (vertical, horizontal, omni)
|
||||
|
||||
Example:
|
||||
from engine.pipeline import Pipeline, PipelineConfig, StageRegistry
|
||||
|
||||
pipeline = Pipeline(PipelineConfig(source="headlines", display="terminal"))
|
||||
pipeline.add_stage("source", StageRegistry.create("source", "headlines"))
|
||||
pipeline.add_stage("display", StageRegistry.create("display", "terminal"))
|
||||
pipeline.build().initialize()
|
||||
|
||||
result = pipeline.execute(initial_data)
|
||||
"""
|
||||
|
||||
from engine.pipeline.controller import (
|
||||
Pipeline,
|
||||
PipelineConfig,
|
||||
PipelineRunner,
|
||||
create_default_pipeline,
|
||||
create_pipeline_from_params,
|
||||
)
|
||||
from engine.pipeline.core import (
|
||||
PipelineContext,
|
||||
Stage,
|
||||
StageConfig,
|
||||
StageError,
|
||||
StageResult,
|
||||
)
|
||||
from engine.pipeline.params import (
|
||||
DEFAULT_HEADLINE_PARAMS,
|
||||
DEFAULT_PIPELINE_PARAMS,
|
||||
DEFAULT_PYGAME_PARAMS,
|
||||
PipelineParams,
|
||||
)
|
||||
from engine.pipeline.presets import (
|
||||
DEMO_PRESET,
|
||||
FIREHOSE_PRESET,
|
||||
PIPELINE_VIZ_PRESET,
|
||||
POETRY_PRESET,
|
||||
PRESETS,
|
||||
SIXEL_PRESET,
|
||||
WEBSOCKET_PRESET,
|
||||
PipelinePreset,
|
||||
create_preset_from_params,
|
||||
get_preset,
|
||||
list_presets,
|
||||
)
|
||||
from engine.pipeline.registry import (
|
||||
StageRegistry,
|
||||
discover_stages,
|
||||
register_camera,
|
||||
register_display,
|
||||
register_effect,
|
||||
register_source,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Core
|
||||
"Stage",
|
||||
"StageConfig",
|
||||
"StageError",
|
||||
"StageResult",
|
||||
"PipelineContext",
|
||||
# Controller
|
||||
"Pipeline",
|
||||
"PipelineConfig",
|
||||
"PipelineRunner",
|
||||
"create_default_pipeline",
|
||||
"create_pipeline_from_params",
|
||||
# Params
|
||||
"PipelineParams",
|
||||
"DEFAULT_HEADLINE_PARAMS",
|
||||
"DEFAULT_PIPELINE_PARAMS",
|
||||
"DEFAULT_PYGAME_PARAMS",
|
||||
# Presets
|
||||
"PipelinePreset",
|
||||
"PRESETS",
|
||||
"DEMO_PRESET",
|
||||
"POETRY_PRESET",
|
||||
"PIPELINE_VIZ_PRESET",
|
||||
"WEBSOCKET_PRESET",
|
||||
"SIXEL_PRESET",
|
||||
"FIREHOSE_PRESET",
|
||||
"get_preset",
|
||||
"list_presets",
|
||||
"create_preset_from_params",
|
||||
# Registry
|
||||
"StageRegistry",
|
||||
"discover_stages",
|
||||
"register_source",
|
||||
"register_effect",
|
||||
"register_display",
|
||||
"register_camera",
|
||||
]
|
||||
229
engine/pipeline/controller.py
Normal file
229
engine/pipeline/controller.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Pipeline controller - DAG-based pipeline execution.
|
||||
|
||||
The Pipeline class orchestrates stages in dependency order, handling
|
||||
the complete render cycle from source to display.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from engine.pipeline.core import PipelineContext, Stage, StageError, StageResult
|
||||
from engine.pipeline.params import PipelineParams
|
||||
from engine.pipeline.registry import StageRegistry
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineConfig:
|
||||
"""Configuration for a pipeline instance."""
|
||||
|
||||
source: str = "headlines"
|
||||
display: str = "terminal"
|
||||
camera: str = "vertical"
|
||||
effects: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class Pipeline:
|
||||
"""Main pipeline orchestrator.
|
||||
|
||||
Manages the execution of all stages in dependency order,
|
||||
handling initialization, processing, and cleanup.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: PipelineConfig | None = None,
|
||||
context: PipelineContext | None = None,
|
||||
):
|
||||
self.config = config or PipelineConfig()
|
||||
self.context = context or PipelineContext()
|
||||
self._stages: dict[str, Stage] = {}
|
||||
self._execution_order: list[str] = []
|
||||
self._initialized = False
|
||||
|
||||
def add_stage(self, name: str, stage: Stage) -> "Pipeline":
|
||||
"""Add a stage to the pipeline."""
|
||||
self._stages[name] = stage
|
||||
return self
|
||||
|
||||
def remove_stage(self, name: str) -> None:
|
||||
"""Remove a stage from the pipeline."""
|
||||
if name in self._stages:
|
||||
del self._stages[name]
|
||||
|
||||
def get_stage(self, name: str) -> Stage | None:
|
||||
"""Get a stage by name."""
|
||||
return self._stages.get(name)
|
||||
|
||||
def build(self) -> "Pipeline":
|
||||
"""Build execution order based on dependencies."""
|
||||
self._execution_order = self._resolve_dependencies()
|
||||
self._initialized = True
|
||||
return self
|
||||
|
||||
def _resolve_dependencies(self) -> list[str]:
|
||||
"""Resolve stage execution order using topological sort."""
|
||||
ordered = []
|
||||
visited = set()
|
||||
temp_mark = set()
|
||||
|
||||
def visit(name: str) -> None:
|
||||
if name in temp_mark:
|
||||
raise StageError(name, "Circular dependency detected")
|
||||
if name in visited:
|
||||
return
|
||||
|
||||
temp_mark.add(name)
|
||||
stage = self._stages.get(name)
|
||||
if stage:
|
||||
for dep in stage.dependencies:
|
||||
dep_stage = self._stages.get(dep)
|
||||
if dep_stage:
|
||||
visit(dep)
|
||||
|
||||
temp_mark.remove(name)
|
||||
visited.add(name)
|
||||
ordered.append(name)
|
||||
|
||||
for name in self._stages:
|
||||
if name not in visited:
|
||||
visit(name)
|
||||
|
||||
return ordered
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""Initialize all stages in execution order."""
|
||||
for name in self._execution_order:
|
||||
stage = self._stages.get(name)
|
||||
if stage and not stage.init(self.context) and not stage.optional:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, data: Any | None = None) -> StageResult:
|
||||
"""Execute the pipeline with the given input data."""
|
||||
if not self._initialized:
|
||||
self.build()
|
||||
|
||||
if not self._initialized:
|
||||
return StageResult(
|
||||
success=False,
|
||||
data=None,
|
||||
error="Pipeline not initialized",
|
||||
)
|
||||
|
||||
current_data = data
|
||||
|
||||
for name in self._execution_order:
|
||||
stage = self._stages.get(name)
|
||||
if not stage or not stage.is_enabled():
|
||||
continue
|
||||
|
||||
try:
|
||||
current_data = stage.process(current_data, self.context)
|
||||
except Exception as e:
|
||||
if not stage.optional:
|
||||
return StageResult(
|
||||
success=False,
|
||||
data=current_data,
|
||||
error=str(e),
|
||||
stage_name=name,
|
||||
)
|
||||
# Skip optional stage on error
|
||||
continue
|
||||
|
||||
return StageResult(success=True, data=current_data)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up all stages in reverse order."""
|
||||
for name in reversed(self._execution_order):
|
||||
stage = self._stages.get(name)
|
||||
if stage:
|
||||
try:
|
||||
stage.cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
self._stages.clear()
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
def stages(self) -> dict[str, Stage]:
|
||||
"""Get all stages."""
|
||||
return self._stages.copy()
|
||||
|
||||
@property
|
||||
def execution_order(self) -> list[str]:
|
||||
"""Get execution order."""
|
||||
return self._execution_order.copy()
|
||||
|
||||
def get_stage_names(self) -> list[str]:
|
||||
"""Get list of stage names."""
|
||||
return list(self._stages.keys())
|
||||
|
||||
|
||||
class PipelineRunner:
|
||||
"""High-level pipeline runner with animation support."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pipeline: Pipeline,
|
||||
params: PipelineParams | None = None,
|
||||
):
|
||||
self.pipeline = pipeline
|
||||
self.params = params or PipelineParams()
|
||||
self._running = False
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start the pipeline."""
|
||||
self._running = True
|
||||
return self.pipeline.initialize()
|
||||
|
||||
def step(self, input_data: Any | None = None) -> Any:
|
||||
"""Execute one pipeline step."""
|
||||
self.params.frame_number += 1
|
||||
self.context.params = self.params
|
||||
result = self.pipeline.execute(input_data)
|
||||
return result.data if result.success else None
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop and clean up the pipeline."""
|
||||
self._running = False
|
||||
self.pipeline.cleanup()
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Check if runner is active."""
|
||||
return self._running
|
||||
|
||||
|
||||
def create_pipeline_from_params(params: PipelineParams) -> Pipeline:
|
||||
"""Create a pipeline from PipelineParams."""
|
||||
config = PipelineConfig(
|
||||
source=params.source,
|
||||
display=params.display,
|
||||
camera=params.camera_mode,
|
||||
effects=params.effect_order,
|
||||
)
|
||||
return Pipeline(config=config)
|
||||
|
||||
|
||||
def create_default_pipeline() -> Pipeline:
|
||||
"""Create a default pipeline with all standard components."""
|
||||
pipeline = Pipeline()
|
||||
|
||||
# Add source stage
|
||||
source = StageRegistry.create("source", "headlines")
|
||||
if source:
|
||||
pipeline.add_stage("source", source)
|
||||
|
||||
# Add effect stages
|
||||
for effect_name in ["noise", "fade", "glitch", "firehose", "hud"]:
|
||||
effect = StageRegistry.create("effect", effect_name)
|
||||
if effect:
|
||||
pipeline.add_stage(f"effect_{effect_name}", effect)
|
||||
|
||||
# Add display stage
|
||||
display = StageRegistry.create("display", "terminal")
|
||||
if display:
|
||||
pipeline.add_stage("display", display)
|
||||
|
||||
return pipeline.build()
|
||||
221
engine/pipeline/core.py
Normal file
221
engine/pipeline/core.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Pipeline core - Unified Stage abstraction and PipelineContext.
|
||||
|
||||
This module provides the foundation for a clean, dependency-managed pipeline:
|
||||
- Stage: Base class for all pipeline components (sources, effects, displays, cameras)
|
||||
- PipelineContext: Dependency injection context for runtime data exchange
|
||||
- Capability system: Explicit capability declarations with duck-typing support
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from engine.pipeline.params import PipelineParams
|
||||
|
||||
|
||||
@dataclass
|
||||
class StageConfig:
|
||||
"""Configuration for a single stage."""
|
||||
|
||||
name: str
|
||||
category: str
|
||||
enabled: bool = True
|
||||
optional: bool = False
|
||||
params: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class Stage(ABC):
|
||||
"""Abstract base class for all pipeline stages.
|
||||
|
||||
A Stage is a single component in the rendering pipeline. Stages can be:
|
||||
- Sources: Data providers (headlines, poetry, pipeline viz)
|
||||
- Effects: Post-processors (noise, fade, glitch, hud)
|
||||
- Displays: Output backends (terminal, pygame, websocket)
|
||||
- Cameras: Viewport controllers (vertical, horizontal, omni)
|
||||
|
||||
Stages declare:
|
||||
- capabilities: What they provide to other stages
|
||||
- dependencies: What they need from other stages
|
||||
|
||||
Duck-typing is supported: any class with the required methods can act as a Stage.
|
||||
"""
|
||||
|
||||
name: str
|
||||
category: str # "source", "effect", "display", "camera"
|
||||
optional: bool = False # If True, pipeline continues even if stage fails
|
||||
|
||||
@property
|
||||
def capabilities(self) -> set[str]:
|
||||
"""Return set of capabilities this stage provides.
|
||||
|
||||
Examples:
|
||||
- "source.headlines"
|
||||
- "effect.noise"
|
||||
- "display.output"
|
||||
- "camera"
|
||||
"""
|
||||
return {f"{self.category}.{self.name}"}
|
||||
|
||||
@property
|
||||
def dependencies(self) -> set[str]:
|
||||
"""Return set of capability names this stage needs.
|
||||
|
||||
Examples:
|
||||
- {"display.output"}
|
||||
- {"source.headlines"}
|
||||
- {"camera"}
|
||||
"""
|
||||
return set()
|
||||
|
||||
def init(self, ctx: "PipelineContext") -> bool:
|
||||
"""Initialize stage with pipeline context.
|
||||
|
||||
Args:
|
||||
ctx: PipelineContext for accessing services
|
||||
|
||||
Returns:
|
||||
True if initialization succeeded, False otherwise
|
||||
"""
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def process(self, data: Any, ctx: "PipelineContext") -> Any:
|
||||
"""Process input data and return output.
|
||||
|
||||
Args:
|
||||
data: Input data from previous stage (or initial data for first stage)
|
||||
ctx: PipelineContext for accessing services and state
|
||||
|
||||
Returns:
|
||||
Processed data for next stage
|
||||
"""
|
||||
...
|
||||
|
||||
def cleanup(self) -> None: # noqa: B027
|
||||
"""Clean up resources when pipeline shuts down."""
|
||||
pass
|
||||
|
||||
def get_config(self) -> StageConfig:
|
||||
"""Return current configuration of this stage."""
|
||||
return StageConfig(
|
||||
name=self.name,
|
||||
category=self.category,
|
||||
optional=self.optional,
|
||||
)
|
||||
|
||||
def set_enabled(self, enabled: bool) -> None:
|
||||
"""Enable or disable this stage."""
|
||||
self._enabled = enabled # type: ignore[attr-defined]
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""Check if stage is enabled."""
|
||||
return getattr(self, "_enabled", True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StageResult:
|
||||
"""Result of stage processing, including success/failure info."""
|
||||
|
||||
success: bool
|
||||
data: Any
|
||||
error: str | None = None
|
||||
stage_name: str = ""
|
||||
|
||||
|
||||
class PipelineContext:
|
||||
"""Dependency injection context passed through the pipeline.
|
||||
|
||||
Provides:
|
||||
- services: Named services (display, config, event_bus, etc.)
|
||||
- state: Runtime state shared between stages
|
||||
- params: PipelineParams for animation-driven config
|
||||
|
||||
Services can be injected at construction time or lazily resolved.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
services: dict[str, Any] | None = None,
|
||||
initial_state: dict[str, Any] | None = None,
|
||||
):
|
||||
self.services: dict[str, Any] = services or {}
|
||||
self.state: dict[str, Any] = initial_state or {}
|
||||
self._params: PipelineParams | None = None
|
||||
|
||||
# Lazy resolvers for common services
|
||||
self._lazy_resolvers: dict[str, Callable[[], Any]] = {
|
||||
"config": self._resolve_config,
|
||||
"event_bus": self._resolve_event_bus,
|
||||
}
|
||||
|
||||
def _resolve_config(self) -> Any:
|
||||
from engine.config import get_config
|
||||
|
||||
return get_config()
|
||||
|
||||
def _resolve_event_bus(self) -> Any:
|
||||
from engine.eventbus import get_event_bus
|
||||
|
||||
return get_event_bus()
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Get a service or state value by key.
|
||||
|
||||
First checks services, then state, then lazy resolution.
|
||||
"""
|
||||
if key in self.services:
|
||||
return self.services[key]
|
||||
if key in self.state:
|
||||
return self.state[key]
|
||||
if key in self._lazy_resolvers:
|
||||
try:
|
||||
return self._lazy_resolvers[key]()
|
||||
except Exception:
|
||||
return default
|
||||
return default
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""Set a service or state value."""
|
||||
self.services[key] = value
|
||||
|
||||
def set_state(self, key: str, value: Any) -> None:
|
||||
"""Set a runtime state value."""
|
||||
self.state[key] = value
|
||||
|
||||
def get_state(self, key: str, default: Any = None) -> Any:
|
||||
"""Get a runtime state value."""
|
||||
return self.state.get(key, default)
|
||||
|
||||
@property
|
||||
def params(self) -> "PipelineParams | None":
|
||||
"""Get current pipeline params (for animation)."""
|
||||
return self._params
|
||||
|
||||
@params.setter
|
||||
def params(self, value: "PipelineParams") -> None:
|
||||
"""Set pipeline params (from animation controller)."""
|
||||
self._params = value
|
||||
|
||||
def has_capability(self, capability: str) -> bool:
|
||||
"""Check if a capability is available."""
|
||||
return capability in self.services or capability in self._lazy_resolvers
|
||||
|
||||
|
||||
class StageError(Exception):
|
||||
"""Raised when a stage fails to process."""
|
||||
|
||||
def __init__(self, stage_name: str, message: str, is_optional: bool = False):
|
||||
self.stage_name = stage_name
|
||||
self.message = message
|
||||
self.is_optional = is_optional
|
||||
super().__init__(f"Stage '{stage_name}' failed: {message}")
|
||||
|
||||
|
||||
def create_stage_error(
|
||||
stage_name: str, error: Exception, is_optional: bool = False
|
||||
) -> StageError:
|
||||
"""Helper to create a StageError from an exception."""
|
||||
return StageError(stage_name, str(error), is_optional)
|
||||
144
engine/pipeline/params.py
Normal file
144
engine/pipeline/params.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Pipeline parameters - Runtime configuration layer for animation control.
|
||||
|
||||
PipelineParams is the target for AnimationController - animation events
|
||||
modify these params, which the pipeline then applies to its stages.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineParams:
|
||||
"""Runtime configuration for the pipeline.
|
||||
|
||||
This is the canonical config object that AnimationController modifies.
|
||||
Stages read from these params to adjust their behavior.
|
||||
"""
|
||||
|
||||
# Source config
|
||||
source: str = "headlines"
|
||||
source_refresh_interval: float = 60.0
|
||||
|
||||
# Display config
|
||||
display: str = "terminal"
|
||||
|
||||
# Camera config
|
||||
camera_mode: str = "vertical"
|
||||
camera_speed: float = 1.0
|
||||
camera_x: int = 0 # For horizontal scrolling
|
||||
|
||||
# Effect config
|
||||
effect_order: list[str] = field(
|
||||
default_factory=lambda: ["noise", "fade", "glitch", "firehose", "hud"]
|
||||
)
|
||||
effect_enabled: dict[str, bool] = field(default_factory=dict)
|
||||
effect_intensity: dict[str, float] = field(default_factory=dict)
|
||||
|
||||
# Animation-driven state (set by AnimationController)
|
||||
pulse: float = 0.0
|
||||
current_effect: str | None = None
|
||||
path_progress: float = 0.0
|
||||
|
||||
# Viewport
|
||||
viewport_width: int = 80
|
||||
viewport_height: int = 24
|
||||
|
||||
# Firehose
|
||||
firehose_enabled: bool = False
|
||||
|
||||
# Runtime state
|
||||
frame_number: int = 0
|
||||
fps: float = 60.0
|
||||
|
||||
def get_effect_config(self, name: str) -> tuple[bool, float]:
|
||||
"""Get (enabled, intensity) for an effect."""
|
||||
enabled = self.effect_enabled.get(name, True)
|
||||
intensity = self.effect_intensity.get(name, 1.0)
|
||||
return enabled, intensity
|
||||
|
||||
def set_effect_config(self, name: str, enabled: bool, intensity: float) -> None:
|
||||
"""Set effect configuration."""
|
||||
self.effect_enabled[name] = enabled
|
||||
self.effect_intensity[name] = intensity
|
||||
|
||||
def is_effect_enabled(self, name: str) -> bool:
|
||||
"""Check if an effect is enabled."""
|
||||
if name not in self.effect_enabled:
|
||||
return True # Default to enabled
|
||||
return self.effect_enabled.get(name, True)
|
||||
|
||||
def get_effect_intensity(self, name: str) -> float:
|
||||
"""Get effect intensity (0.0 to 1.0)."""
|
||||
return self.effect_intensity.get(name, 1.0)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"source": self.source,
|
||||
"display": self.display,
|
||||
"camera_mode": self.camera_mode,
|
||||
"camera_speed": self.camera_speed,
|
||||
"effect_order": self.effect_order,
|
||||
"effect_enabled": self.effect_enabled.copy(),
|
||||
"effect_intensity": self.effect_intensity.copy(),
|
||||
"pulse": self.pulse,
|
||||
"current_effect": self.current_effect,
|
||||
"viewport_width": self.viewport_width,
|
||||
"viewport_height": self.viewport_height,
|
||||
"firehose_enabled": self.firehose_enabled,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "PipelineParams":
|
||||
"""Create from dictionary."""
|
||||
params = cls()
|
||||
for key, value in data.items():
|
||||
if hasattr(params, key):
|
||||
setattr(params, key, value)
|
||||
return params
|
||||
|
||||
def copy(self) -> "PipelineParams":
|
||||
"""Create a copy of this params object."""
|
||||
params = PipelineParams()
|
||||
params.source = self.source
|
||||
params.display = self.display
|
||||
params.camera_mode = self.camera_mode
|
||||
params.camera_speed = self.camera_speed
|
||||
params.camera_x = self.camera_x
|
||||
params.effect_order = self.effect_order.copy()
|
||||
params.effect_enabled = self.effect_enabled.copy()
|
||||
params.effect_intensity = self.effect_intensity.copy()
|
||||
params.pulse = self.pulse
|
||||
params.current_effect = self.current_effect
|
||||
params.path_progress = self.path_progress
|
||||
params.viewport_width = self.viewport_width
|
||||
params.viewport_height = self.viewport_height
|
||||
params.firehose_enabled = self.firehose_enabled
|
||||
params.frame_number = self.frame_number
|
||||
params.fps = self.fps
|
||||
return params
|
||||
|
||||
|
||||
# Default params for different modes
|
||||
DEFAULT_HEADLINE_PARAMS = PipelineParams(
|
||||
source="headlines",
|
||||
display="terminal",
|
||||
camera_mode="vertical",
|
||||
effect_order=["noise", "fade", "glitch", "firehose", "hud"],
|
||||
)
|
||||
|
||||
DEFAULT_PYGAME_PARAMS = PipelineParams(
|
||||
source="headlines",
|
||||
display="pygame",
|
||||
camera_mode="vertical",
|
||||
effect_order=["noise", "fade", "glitch", "firehose", "hud"],
|
||||
)
|
||||
|
||||
DEFAULT_PIPELINE_PARAMS = PipelineParams(
|
||||
source="pipeline",
|
||||
display="pygame",
|
||||
camera_mode="trace",
|
||||
effect_order=["hud"], # Just HUD for pipeline viz
|
||||
)
|
||||
155
engine/pipeline/presets.py
Normal file
155
engine/pipeline/presets.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
Pipeline presets - Pre-configured pipeline configurations.
|
||||
|
||||
Provides PipelinePreset as a unified preset system that wraps
|
||||
the existing Preset class from animation.py for backwards compatibility.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from engine.animation import Preset as AnimationPreset
|
||||
from engine.pipeline.params import PipelineParams
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelinePreset:
|
||||
"""Pre-configured pipeline with stages and animation.
|
||||
|
||||
A PipelinePreset packages:
|
||||
- Initial params: Starting configuration
|
||||
- Stages: List of stage configurations to create
|
||||
- Animation: Optional animation controller
|
||||
|
||||
This is the new unified preset that works with the Pipeline class.
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str = ""
|
||||
source: str = "headlines"
|
||||
display: str = "terminal"
|
||||
camera: str = "vertical"
|
||||
effects: list[str] = field(default_factory=list)
|
||||
initial_params: PipelineParams | None = None
|
||||
animation_preset: AnimationPreset | None = None
|
||||
|
||||
def to_params(self) -> PipelineParams:
|
||||
"""Convert to PipelineParams."""
|
||||
if self.initial_params:
|
||||
return self.initial_params.copy()
|
||||
params = PipelineParams()
|
||||
params.source = self.source
|
||||
params.display = self.display
|
||||
params.camera_mode = self.camera
|
||||
params.effect_order = self.effects.copy()
|
||||
return params
|
||||
|
||||
@classmethod
|
||||
def from_animation_preset(cls, preset: AnimationPreset) -> "PipelinePreset":
|
||||
"""Create a PipelinePreset from an existing animation Preset."""
|
||||
params = preset.initial_params
|
||||
return cls(
|
||||
name=preset.name,
|
||||
description=preset.description,
|
||||
source=params.source,
|
||||
display=params.display,
|
||||
camera=params.camera_mode,
|
||||
effects=params.effect_order.copy(),
|
||||
initial_params=params,
|
||||
animation_preset=preset,
|
||||
)
|
||||
|
||||
def create_animation_controller(self):
|
||||
"""Create an AnimationController from this preset."""
|
||||
if self.animation_preset:
|
||||
return self.animation_preset.create_controller()
|
||||
return None
|
||||
|
||||
|
||||
# Built-in presets
|
||||
DEMO_PRESET = PipelinePreset(
|
||||
name="demo",
|
||||
description="Demo mode with effect cycling and camera modes",
|
||||
source="headlines",
|
||||
display="terminal",
|
||||
camera="vertical",
|
||||
effects=["noise", "fade", "glitch", "firehose", "hud"],
|
||||
)
|
||||
|
||||
POETRY_PRESET = PipelinePreset(
|
||||
name="poetry",
|
||||
description="Poetry feed with subtle effects",
|
||||
source="poetry",
|
||||
display="terminal",
|
||||
camera="vertical",
|
||||
effects=["fade", "hud"],
|
||||
)
|
||||
|
||||
PIPELINE_VIZ_PRESET = PipelinePreset(
|
||||
name="pipeline",
|
||||
description="Pipeline visualization mode",
|
||||
source="pipeline",
|
||||
display="terminal",
|
||||
camera="trace",
|
||||
effects=["hud"],
|
||||
)
|
||||
|
||||
WEBSOCKET_PRESET = PipelinePreset(
|
||||
name="websocket",
|
||||
description="WebSocket display mode",
|
||||
source="headlines",
|
||||
display="websocket",
|
||||
camera="vertical",
|
||||
effects=["noise", "fade", "glitch", "hud"],
|
||||
)
|
||||
|
||||
SIXEL_PRESET = PipelinePreset(
|
||||
name="sixel",
|
||||
description="Sixel graphics display mode",
|
||||
source="headlines",
|
||||
display="sixel",
|
||||
camera="vertical",
|
||||
effects=["noise", "fade", "glitch", "hud"],
|
||||
)
|
||||
|
||||
FIREHOSE_PRESET = PipelinePreset(
|
||||
name="firehose",
|
||||
description="High-speed firehose mode",
|
||||
source="headlines",
|
||||
display="terminal",
|
||||
camera="vertical",
|
||||
effects=["noise", "fade", "glitch", "firehose", "hud"],
|
||||
)
|
||||
|
||||
|
||||
PRESETS: dict[str, PipelinePreset] = {
|
||||
"demo": DEMO_PRESET,
|
||||
"poetry": POETRY_PRESET,
|
||||
"pipeline": PIPELINE_VIZ_PRESET,
|
||||
"websocket": WEBSOCKET_PRESET,
|
||||
"sixel": SIXEL_PRESET,
|
||||
"firehose": FIREHOSE_PRESET,
|
||||
}
|
||||
|
||||
|
||||
def get_preset(name: str) -> PipelinePreset | None:
|
||||
"""Get a preset by name."""
|
||||
return PRESETS.get(name)
|
||||
|
||||
|
||||
def list_presets() -> list[str]:
|
||||
"""List all available preset names."""
|
||||
return list(PRESETS.keys())
|
||||
|
||||
|
||||
def create_preset_from_params(
|
||||
params: PipelineParams, name: str = "custom"
|
||||
) -> PipelinePreset:
|
||||
"""Create a preset from PipelineParams."""
|
||||
return PipelinePreset(
|
||||
name=name,
|
||||
source=params.source,
|
||||
display=params.display,
|
||||
camera=params.camera_mode,
|
||||
effects=params.effect_order.copy(),
|
||||
initial_params=params,
|
||||
)
|
||||
127
engine/pipeline/registry.py
Normal file
127
engine/pipeline/registry.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Stage registry - Unified registration for all pipeline stages.
|
||||
|
||||
Provides a single registry for sources, effects, displays, and cameras.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
|
||||
class StageRegistry:
|
||||
"""Unified registry for all pipeline stage types."""
|
||||
|
||||
_categories: dict[str, dict[str, type[Stage]]] = {}
|
||||
_discovered: bool = False
|
||||
_instances: dict[str, Stage] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, category: str, stage_class: type[Stage]) -> None:
|
||||
"""Register a stage class in a category.
|
||||
|
||||
Args:
|
||||
category: Category name (source, effect, display, camera)
|
||||
stage_class: Stage subclass to register
|
||||
"""
|
||||
if category not in cls._categories:
|
||||
cls._categories[category] = {}
|
||||
|
||||
# Use class name as key
|
||||
key = stage_class.__name__
|
||||
cls._categories[category][key] = stage_class
|
||||
|
||||
@classmethod
|
||||
def get(cls, category: str, name: str) -> type[Stage] | None:
|
||||
"""Get a stage class by category and name."""
|
||||
return cls._categories.get(category, {}).get(name)
|
||||
|
||||
@classmethod
|
||||
def list(cls, category: str) -> list[str]:
|
||||
"""List all stage names in a category."""
|
||||
return list(cls._categories.get(category, {}).keys())
|
||||
|
||||
@classmethod
|
||||
def list_categories(cls) -> list[str]:
|
||||
"""List all registered categories."""
|
||||
return list(cls._categories.keys())
|
||||
|
||||
@classmethod
|
||||
def create(cls, category: str, name: str, **kwargs) -> Stage | None:
|
||||
"""Create a stage instance by category and name."""
|
||||
stage_class = cls.get(category, name)
|
||||
if stage_class:
|
||||
return stage_class(**kwargs)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def create_instance(cls, stage: Stage | type[Stage], **kwargs) -> Stage:
|
||||
"""Create an instance from a stage class or return as-is."""
|
||||
if isinstance(stage, Stage):
|
||||
return stage
|
||||
if isinstance(stage, type) and issubclass(stage, Stage):
|
||||
return stage(**kwargs)
|
||||
raise TypeError(f"Expected Stage class or instance, got {type(stage)}")
|
||||
|
||||
@classmethod
|
||||
def register_instance(cls, name: str, stage: Stage) -> None:
|
||||
"""Register a stage instance by name."""
|
||||
cls._instances[name] = stage
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, name: str) -> Stage | None:
|
||||
"""Get a registered stage instance by name."""
|
||||
return cls._instances.get(name)
|
||||
|
||||
|
||||
def discover_stages() -> None:
|
||||
"""Auto-discover and register all stage implementations."""
|
||||
if StageRegistry._discovered:
|
||||
return
|
||||
|
||||
# Import and register all stage implementations
|
||||
try:
|
||||
from engine.sources_v2 import (
|
||||
HeadlinesDataSource,
|
||||
PipelineDataSource,
|
||||
PoetryDataSource,
|
||||
)
|
||||
|
||||
StageRegistry.register("source", HeadlinesDataSource)
|
||||
StageRegistry.register("source", PoetryDataSource)
|
||||
StageRegistry.register("source", PipelineDataSource)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from engine.effects.types import EffectPlugin # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from engine.display import Display # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
StageRegistry._discovered = True
|
||||
|
||||
|
||||
# Convenience functions
|
||||
def register_source(stage_class: type[Stage]) -> None:
|
||||
"""Register a source stage."""
|
||||
StageRegistry.register("source", stage_class)
|
||||
|
||||
|
||||
def register_effect(stage_class: type[Stage]) -> None:
|
||||
"""Register an effect stage."""
|
||||
StageRegistry.register("effect", stage_class)
|
||||
|
||||
|
||||
def register_display(stage_class: type[Stage]) -> None:
|
||||
"""Register a display stage."""
|
||||
StageRegistry.register("display", stage_class)
|
||||
|
||||
|
||||
def register_camera(stage_class: type[Stage]) -> None:
|
||||
"""Register a camera stage."""
|
||||
StageRegistry.register("camera", stage_class)
|
||||
Reference in New Issue
Block a user