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:
2026-03-16 03:11:24 -07:00
parent 996ba14b1d
commit bcb4ef0cfe
17 changed files with 2356 additions and 48 deletions

View File

@@ -572,7 +572,7 @@ def run_pipeline_demo():
get_registry,
set_monitor,
)
from engine.pipeline_viz import generate_network_pipeline
from engine.pipeline_viz import generate_large_network_viewport
print(" \033[1;38;5;46mMAINLINE PIPELINE DEMO\033[0m")
print(" \033[38;5;245mInitializing...\033[0m")
@@ -667,7 +667,7 @@ def run_pipeline_demo():
camera.update(config.FRAME_DT)
buf = generate_network_pipeline(w, h, frame_number)
buf = generate_large_network_viewport(w, h, frame_number)
ctx = EffectContext(
terminal_width=w,
@@ -699,6 +699,144 @@ def run_pipeline_demo():
print("\n \033[38;5;245mPipeline demo ended\033[0m")
def run_preset_mode(preset_name: str):
"""Run mode using animation presets."""
from engine import config
from engine.animation import (
create_demo_preset,
create_pipeline_preset,
)
from engine.camera import Camera
from engine.display import DisplayRegistry
from engine.effects import (
EffectContext,
PerformanceMonitor,
get_effect_chain,
get_registry,
set_monitor,
)
from engine.sources_v2 import (
PipelineDataSource,
get_source_registry,
init_default_sources,
)
w, h = 80, 24
if preset_name == "demo":
preset = create_demo_preset()
init_default_sources()
source = get_source_registry().default()
elif preset_name == "pipeline":
preset = create_pipeline_preset()
source = PipelineDataSource(w, h)
else:
print(f" \033[38;5;196mUnknown preset: {preset_name}\033[0m")
print(" Available: demo, pipeline")
sys.exit(1)
print(f" \033[1;38;5;46mMAINLINE PRESET: {preset.name}\033[0m")
print(f" \033[38;5;245m{preset.description}\033[0m")
print(" \033[38;5;245mInitializing...\033[0m")
import effects_plugins
effects_plugins.discover_plugins()
registry = get_registry()
chain = get_effect_chain()
chain.set_order(["noise", "fade", "glitch", "firehose", "hud"])
monitor = PerformanceMonitor()
set_monitor(monitor)
chain._monitor = monitor
display = DisplayRegistry.create(preset.initial_params.display_backend)
if not display:
print(
f" \033[38;5;196mFailed to create {preset.initial_params.display_backend} display\033[0m"
)
sys.exit(1)
display.init(w, h)
display.clear()
camera = Camera.vertical()
print(" \033[38;5;82mStarting preset animation...\033[0m")
print(" \033[38;5;245mPress Ctrl+C to exit\033[0m\n")
controller = preset.create_controller()
frame_number = 0
try:
while True:
params = controller.update()
effect_name = params.get("current_effect", "none")
intensity = params.get("effect_intensity", 0.0)
camera_mode = params.get("camera_mode", "vertical")
if camera_mode == "vertical":
camera = Camera.vertical(speed=params.get("camera_speed", 1.0))
elif camera_mode == "horizontal":
camera = Camera.horizontal(speed=params.get("camera_speed", 1.0))
elif camera_mode == "omni":
camera = Camera.omni(speed=params.get("camera_speed", 1.0))
elif camera_mode == "floating":
camera = Camera.floating(speed=params.get("camera_speed", 1.0))
camera.update(config.FRAME_DT)
for eff in registry.list_all().values():
if eff.name == effect_name:
eff.config.enabled = True
eff.config.intensity = intensity
elif eff.name not in ("hud",):
eff.config.enabled = False
hud_effect = registry.get("hud")
if hud_effect:
hud_effect.config.params["display_effect"] = (
f"{effect_name} / {camera_mode}"
)
hud_effect.config.params["display_intensity"] = intensity
source.viewport_width = w
source.viewport_height = h
items = source.get_items()
buffer = items[0].content.split("\n") if items else [""] * h
ctx = EffectContext(
terminal_width=w,
terminal_height=h,
scroll_cam=camera.y,
ticker_height=h,
camera_x=camera.x,
mic_excess=0.0,
grad_offset=0.0,
frame_number=frame_number,
has_message=False,
items=[],
)
result = chain.process(buffer, ctx)
display.show(result)
new_w, new_h = display.get_dimensions()
if new_w != w or new_h != h:
w, h = new_w, new_h
frame_number += 1
time.sleep(1 / 60)
except KeyboardInterrupt:
pass
finally:
display.cleanup()
print("\n \033[38;5;245mPreset ended\033[0m")
def main():
from engine import config
from engine.pipeline import generate_pipeline_diagram
@@ -711,6 +849,10 @@ def main():
run_pipeline_demo()
return
if config.PRESET:
run_preset_mode(config.PRESET)
return
if config.DEMO:
run_demo_mode()
return