forked from genewildish/Mainline
feat(graph): Integrate graph system with pipeline runner
Add support for loading pipelines from TOML graph configs in the pipeline runner, maintaining full backward compatibility with presets. - Add graph_config parameter to run_pipeline_mode() function - Support both preset mode and graph mode with conditional logic - Graph mode: loads from TOML file, uses graph-defined stages - Preset mode: maintains existing behavior with manual stage building - Handle items/context appropriately for each mode (graph uses own data sources) - CLI display flag works in both modes Backward compatible: graph_config defaults to None, so existing calls to run_pipeline_mode(preset_name) continue to work unchanged.
This commit is contained in:
@@ -104,8 +104,13 @@ def _handle_pipeline_mutation(pipeline: Pipeline, command: dict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def run_pipeline_mode(preset_name: str = "demo"):
|
||||
"""Run using the new unified pipeline architecture."""
|
||||
def run_pipeline_mode(preset_name: str = "demo", graph_config: str | None = None):
|
||||
"""Run using the new unified pipeline architecture.
|
||||
|
||||
Args:
|
||||
preset_name: Name of the preset to use
|
||||
graph_config: Path to a TOML graph configuration file (optional)
|
||||
"""
|
||||
import engine.effects.plugins as effects_plugins
|
||||
from engine.effects import PerformanceMonitor, set_monitor
|
||||
|
||||
@@ -117,17 +122,64 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
monitor = PerformanceMonitor()
|
||||
set_monitor(monitor)
|
||||
|
||||
preset = get_preset(preset_name)
|
||||
if not preset:
|
||||
print(f" \033[38;5;196mUnknown preset: {preset_name}\033[0m")
|
||||
sys.exit(1)
|
||||
# Check if graph config is provided
|
||||
using_graph_config = graph_config is not None
|
||||
|
||||
print(f" \033[38;5;245mPreset: {preset.name} - {preset.description}\033[0m")
|
||||
if using_graph_config:
|
||||
from engine.pipeline.graph_toml import load_pipeline_from_toml
|
||||
|
||||
params = preset.to_params()
|
||||
# Use preset viewport if available, else default to 80x24
|
||||
params.viewport_width = getattr(preset, "viewport_width", 80)
|
||||
params.viewport_height = getattr(preset, "viewport_height", 24)
|
||||
print(f" \033[38;5;245mLoading graph from: {graph_config}\033[0m")
|
||||
|
||||
# Determine viewport size
|
||||
viewport_width = 80
|
||||
viewport_height = 24
|
||||
if "--viewport" in sys.argv:
|
||||
idx = sys.argv.index("--viewport")
|
||||
if idx + 1 < len(sys.argv):
|
||||
vp = sys.argv[idx + 1]
|
||||
try:
|
||||
viewport_width, viewport_height = map(int, vp.split("x"))
|
||||
except ValueError:
|
||||
print("Error: Invalid viewport format. Use WxH (e.g., 40x15)")
|
||||
sys.exit(1)
|
||||
|
||||
# Load pipeline from graph config
|
||||
try:
|
||||
pipeline = load_pipeline_from_toml(
|
||||
graph_config,
|
||||
viewport_width=viewport_width,
|
||||
viewport_height=viewport_height,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f" \033[38;5;196mError loading graph config: {e}\033[0m")
|
||||
sys.exit(1)
|
||||
|
||||
# Set params for display
|
||||
from engine.pipeline.params import PipelineParams
|
||||
|
||||
params = PipelineParams(
|
||||
viewport_width=viewport_width, viewport_height=viewport_height
|
||||
)
|
||||
|
||||
# Set display name from graph or CLI
|
||||
display_name = "terminal" # Default for graph mode
|
||||
if "--display" in sys.argv:
|
||||
idx = sys.argv.index("--display")
|
||||
if idx + 1 < len(sys.argv):
|
||||
display_name = sys.argv[idx + 1]
|
||||
else:
|
||||
# Use preset-based pipeline
|
||||
preset = get_preset(preset_name)
|
||||
if not preset:
|
||||
print(f" \033[38;5;196mUnknown preset: {preset_name}\033[0m")
|
||||
sys.exit(1)
|
||||
|
||||
print(f" \033[38;5;245mPreset: {preset.name} - {preset.description}\033[0m")
|
||||
|
||||
params = preset.to_params()
|
||||
# Use preset viewport if available, else default to 80x24
|
||||
params.viewport_width = getattr(preset, "viewport_width", 80)
|
||||
params.viewport_height = getattr(preset, "viewport_height", 24)
|
||||
|
||||
if "--viewport" in sys.argv:
|
||||
idx = sys.argv.index("--viewport")
|
||||
@@ -196,22 +248,28 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
|
||||
print(f" \033[38;5;82mLoaded {len(items)} items\033[0m")
|
||||
|
||||
# CLI --display flag takes priority over preset
|
||||
# CLI --display flag takes priority
|
||||
# Check if --display was explicitly provided
|
||||
display_name = preset.display
|
||||
display_explicitly_specified = "--display" in sys.argv
|
||||
if display_explicitly_specified:
|
||||
idx = sys.argv.index("--display")
|
||||
if idx + 1 < len(sys.argv):
|
||||
display_name = sys.argv[idx + 1]
|
||||
if not using_graph_config:
|
||||
# Preset mode: use preset display as default
|
||||
display_name = preset.display
|
||||
if display_explicitly_specified:
|
||||
idx = sys.argv.index("--display")
|
||||
if idx + 1 < len(sys.argv):
|
||||
display_name = sys.argv[idx + 1]
|
||||
else:
|
||||
# Warn user that display is falling back to preset default
|
||||
print(
|
||||
f" \033[38;5;226mWarning: No --display specified, using preset default: {display_name}\033[0m"
|
||||
)
|
||||
print(
|
||||
" \033[38;5;245mTip: Use --display null for headless mode (useful for testing/capture)\033[0m"
|
||||
)
|
||||
else:
|
||||
# Warn user that display is falling back to preset default
|
||||
print(
|
||||
f" \033[38;5;226mWarning: No --display specified, using preset default: {display_name}\033[0m"
|
||||
)
|
||||
print(
|
||||
" \033[38;5;245mTip: Use --display null for headless mode (useful for testing/capture)\033[0m"
|
||||
)
|
||||
# Graph mode: display_name already set above
|
||||
if not display_explicitly_specified:
|
||||
print(f" \033[38;5;245mUsing default display: {display_name}\033[0m")
|
||||
|
||||
display = DisplayRegistry.create(display_name)
|
||||
if not display and not display_name.startswith("multi"):
|
||||
@@ -245,113 +303,123 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
|
||||
effect_registry = get_registry()
|
||||
|
||||
# Create source stage based on preset source type
|
||||
if preset.source == "pipeline-inspect":
|
||||
from engine.data_sources.pipeline_introspection import (
|
||||
PipelineIntrospectionSource,
|
||||
)
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
# Only build stages from preset if not using graph config
|
||||
# (graph config already has all stages defined)
|
||||
if not using_graph_config:
|
||||
# Create source stage based on preset source type
|
||||
if preset.source == "pipeline-inspect":
|
||||
from engine.data_sources.pipeline_introspection import (
|
||||
PipelineIntrospectionSource,
|
||||
)
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
|
||||
introspection_source = PipelineIntrospectionSource(
|
||||
pipeline=None, # Will be set after pipeline.build()
|
||||
viewport_width=80,
|
||||
viewport_height=24,
|
||||
)
|
||||
pipeline.add_stage(
|
||||
"source", DataSourceStage(introspection_source, name="pipeline-inspect")
|
||||
)
|
||||
elif preset.source == "empty":
|
||||
from engine.data_sources.sources import EmptyDataSource
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
introspection_source = PipelineIntrospectionSource(
|
||||
pipeline=None, # Will be set after pipeline.build()
|
||||
viewport_width=80,
|
||||
viewport_height=24,
|
||||
)
|
||||
pipeline.add_stage(
|
||||
"source", DataSourceStage(introspection_source, name="pipeline-inspect")
|
||||
)
|
||||
elif preset.source == "empty":
|
||||
from engine.data_sources.sources import EmptyDataSource
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
|
||||
empty_source = EmptyDataSource(width=80, height=24)
|
||||
pipeline.add_stage("source", DataSourceStage(empty_source, name="empty"))
|
||||
else:
|
||||
from engine.data_sources.sources import ListDataSource
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
empty_source = EmptyDataSource(width=80, height=24)
|
||||
pipeline.add_stage("source", DataSourceStage(empty_source, name="empty"))
|
||||
else:
|
||||
from engine.data_sources.sources import ListDataSource
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
|
||||
list_source = ListDataSource(items, name=preset.source)
|
||||
pipeline.add_stage("source", DataSourceStage(list_source, name=preset.source))
|
||||
list_source = ListDataSource(items, name=preset.source)
|
||||
pipeline.add_stage(
|
||||
"source", DataSourceStage(list_source, name=preset.source)
|
||||
)
|
||||
|
||||
# Add camera state update stage if specified in preset (must run before viewport filter)
|
||||
camera = None
|
||||
if preset.camera:
|
||||
from engine.camera import Camera
|
||||
from engine.pipeline.adapters import CameraClockStage, CameraStage
|
||||
# Add camera state update stage if specified in preset (must run before viewport filter)
|
||||
camera = None
|
||||
if preset.camera:
|
||||
from engine.camera import Camera
|
||||
from engine.pipeline.adapters import CameraClockStage, CameraStage
|
||||
|
||||
speed = getattr(preset, "camera_speed", 1.0)
|
||||
if preset.camera == "feed":
|
||||
camera = Camera.feed(speed=speed)
|
||||
elif preset.camera == "scroll":
|
||||
camera = Camera.scroll(speed=speed)
|
||||
elif preset.camera == "vertical":
|
||||
camera = Camera.scroll(speed=speed) # Backwards compat
|
||||
elif preset.camera == "horizontal":
|
||||
camera = Camera.horizontal(speed=speed)
|
||||
elif preset.camera == "omni":
|
||||
camera = Camera.omni(speed=speed)
|
||||
elif preset.camera == "floating":
|
||||
camera = Camera.floating(speed=speed)
|
||||
elif preset.camera == "bounce":
|
||||
camera = Camera.bounce(speed=speed)
|
||||
elif preset.camera == "radial":
|
||||
camera = Camera.radial(speed=speed)
|
||||
elif preset.camera == "static" or preset.camera == "":
|
||||
# Static camera: no movement, but provides camera_y=0 for viewport filter
|
||||
camera = Camera.scroll(speed=0.0) # Speed 0 = no movement
|
||||
camera.set_canvas_size(200, 200)
|
||||
speed = getattr(preset, "camera_speed", 1.0)
|
||||
if preset.camera == "feed":
|
||||
camera = Camera.feed(speed=speed)
|
||||
elif preset.camera == "scroll":
|
||||
camera = Camera.scroll(speed=speed)
|
||||
elif preset.camera == "vertical":
|
||||
camera = Camera.scroll(speed=speed) # Backwards compat
|
||||
elif preset.camera == "horizontal":
|
||||
camera = Camera.horizontal(speed=speed)
|
||||
elif preset.camera == "omni":
|
||||
camera = Camera.omni(speed=speed)
|
||||
elif preset.camera == "floating":
|
||||
camera = Camera.floating(speed=speed)
|
||||
elif preset.camera == "bounce":
|
||||
camera = Camera.bounce(speed=speed)
|
||||
elif preset.camera == "radial":
|
||||
camera = Camera.radial(speed=speed)
|
||||
elif preset.camera == "static" or preset.camera == "":
|
||||
# Static camera: no movement, but provides camera_y=0 for viewport filter
|
||||
camera = Camera.scroll(speed=0.0) # Speed 0 = no movement
|
||||
camera.set_canvas_size(200, 200)
|
||||
|
||||
if camera:
|
||||
# Add camera update stage to ensure camera_y is available for viewport filter
|
||||
pipeline.add_stage(
|
||||
"camera_update", CameraClockStage(camera, name="camera-clock")
|
||||
)
|
||||
|
||||
# Only build stages from preset if not using graph config
|
||||
if not using_graph_config:
|
||||
# Add FontStage for headlines/poetry (default for demo)
|
||||
if preset.source in ["headlines", "poetry"]:
|
||||
from engine.pipeline.adapters import FontStage, ViewportFilterStage
|
||||
|
||||
# Add viewport filter to prevent rendering all items
|
||||
pipeline.add_stage(
|
||||
"viewport_filter", ViewportFilterStage(name="viewport-filter")
|
||||
)
|
||||
pipeline.add_stage("font", FontStage(name="font"))
|
||||
else:
|
||||
# Fallback to simple conversion for other sources
|
||||
pipeline.add_stage(
|
||||
"render", SourceItemsToBufferStage(name="items-to-buffer")
|
||||
)
|
||||
|
||||
# Add camera stage if specified in preset (after font/render stage)
|
||||
if camera:
|
||||
# Add camera update stage to ensure camera_y is available for viewport filter
|
||||
pipeline.add_stage("camera", CameraStage(camera, name=preset.camera))
|
||||
|
||||
for effect_name in preset.effects:
|
||||
effect = effect_registry.get(effect_name)
|
||||
if effect:
|
||||
pipeline.add_stage(
|
||||
f"effect_{effect_name}",
|
||||
create_stage_from_effect(effect, effect_name),
|
||||
)
|
||||
|
||||
# Add message overlay stage if enabled
|
||||
if getattr(preset, "enable_message_overlay", False):
|
||||
from engine import config as engine_config
|
||||
from engine.pipeline.adapters import MessageOverlayConfig
|
||||
|
||||
overlay_config = MessageOverlayConfig(
|
||||
enabled=True,
|
||||
display_secs=engine_config.MESSAGE_DISPLAY_SECS
|
||||
if hasattr(engine_config, "MESSAGE_DISPLAY_SECS")
|
||||
else 30,
|
||||
topic_url=engine_config.NTFY_TOPIC
|
||||
if hasattr(engine_config, "NTFY_TOPIC")
|
||||
else None,
|
||||
)
|
||||
pipeline.add_stage(
|
||||
"camera_update", CameraClockStage(camera, name="camera-clock")
|
||||
"message_overlay", MessageOverlayStage(config=overlay_config)
|
||||
)
|
||||
|
||||
# Add FontStage for headlines/poetry (default for demo)
|
||||
if preset.source in ["headlines", "poetry"]:
|
||||
from engine.pipeline.adapters import FontStage, ViewportFilterStage
|
||||
pipeline.add_stage("display", create_stage_from_display(display, display_name))
|
||||
|
||||
# Add viewport filter to prevent rendering all items
|
||||
pipeline.add_stage(
|
||||
"viewport_filter", ViewportFilterStage(name="viewport-filter")
|
||||
)
|
||||
pipeline.add_stage("font", FontStage(name="font"))
|
||||
else:
|
||||
# Fallback to simple conversion for other sources
|
||||
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
||||
|
||||
# Add camera stage if specified in preset (after font/render stage)
|
||||
if camera:
|
||||
pipeline.add_stage("camera", CameraStage(camera, name=preset.camera))
|
||||
|
||||
for effect_name in preset.effects:
|
||||
effect = effect_registry.get(effect_name)
|
||||
if effect:
|
||||
pipeline.add_stage(
|
||||
f"effect_{effect_name}", create_stage_from_effect(effect, effect_name)
|
||||
)
|
||||
|
||||
# Add message overlay stage if enabled
|
||||
if getattr(preset, "enable_message_overlay", False):
|
||||
from engine import config as engine_config
|
||||
from engine.pipeline.adapters import MessageOverlayConfig
|
||||
|
||||
overlay_config = MessageOverlayConfig(
|
||||
enabled=True,
|
||||
display_secs=engine_config.MESSAGE_DISPLAY_SECS
|
||||
if hasattr(engine_config, "MESSAGE_DISPLAY_SECS")
|
||||
else 30,
|
||||
topic_url=engine_config.NTFY_TOPIC
|
||||
if hasattr(engine_config, "NTFY_TOPIC")
|
||||
else None,
|
||||
)
|
||||
pipeline.add_stage(
|
||||
"message_overlay", MessageOverlayStage(config=overlay_config)
|
||||
)
|
||||
|
||||
pipeline.add_stage("display", create_stage_from_display(display, display_name))
|
||||
|
||||
pipeline.build()
|
||||
pipeline.build()
|
||||
|
||||
# For pipeline-inspect, set the pipeline after build to avoid circular dependency
|
||||
if introspection_source is not None:
|
||||
@@ -831,7 +899,13 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
||||
ctx = pipeline.context
|
||||
ctx.params = params
|
||||
ctx.set("display", display)
|
||||
ctx.set("items", items)
|
||||
# For graph mode, items might not be defined - use empty list if needed
|
||||
if not using_graph_config:
|
||||
ctx.set("items", items)
|
||||
else:
|
||||
# Graph-based pipelines typically use their own data sources
|
||||
# But we can set an empty list for compatibility
|
||||
ctx.set("items", [])
|
||||
ctx.set("pipeline", pipeline)
|
||||
ctx.set("pipeline_order", pipeline.execution_order)
|
||||
ctx.set("camera_y", 0)
|
||||
|
||||
Reference in New Issue
Block a user