- Created analysis/visual_output_comparison.md with detailed architectural comparison - Added capture utilities for output comparison (capture_output.py, capture_upstream.py, compare_outputs.py) - Captured and compared output from upstream/main vs sideline branch - Documented fundamental architectural differences in rendering approaches - Updated Gitea issue #50 with findings
202 lines
5.4 KiB
Python
202 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Capture output utility for Mainline.
|
|
|
|
This script captures the output of a Mainline pipeline using NullDisplay
|
|
and saves it to a JSON file for comparison with other branches.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from engine.display import DisplayRegistry
|
|
from engine.pipeline import Pipeline, PipelineConfig, PipelineContext
|
|
from engine.pipeline.adapters import create_stage_from_display
|
|
from engine.pipeline.presets import get_preset
|
|
|
|
|
|
def capture_pipeline_output(
|
|
preset_name: str,
|
|
output_file: str,
|
|
frames: int = 60,
|
|
width: int = 80,
|
|
height: int = 24,
|
|
):
|
|
"""Capture pipeline output for a given preset.
|
|
|
|
Args:
|
|
preset_name: Name of preset to use
|
|
output_file: Path to save captured output
|
|
frames: Number of frames to capture
|
|
width: Terminal width
|
|
height: Terminal height
|
|
"""
|
|
print(f"Capturing output for preset '{preset_name}'...")
|
|
|
|
# Get preset
|
|
preset = get_preset(preset_name)
|
|
if not preset:
|
|
print(f"Error: Preset '{preset_name}' not found")
|
|
return False
|
|
|
|
# Create NullDisplay with recording
|
|
display = DisplayRegistry.create("null")
|
|
display.init(width, height)
|
|
display.start_recording()
|
|
|
|
# Build pipeline
|
|
config = PipelineConfig(
|
|
source=preset.source,
|
|
display="null", # Use null display
|
|
camera=preset.camera,
|
|
effects=preset.effects,
|
|
enable_metrics=False,
|
|
)
|
|
|
|
# Create pipeline context with params
|
|
from engine.pipeline.params import PipelineParams
|
|
|
|
params = PipelineParams(
|
|
source=preset.source,
|
|
display="null",
|
|
camera_mode=preset.camera,
|
|
effect_order=preset.effects,
|
|
viewport_width=preset.viewport_width,
|
|
viewport_height=preset.viewport_height,
|
|
camera_speed=preset.camera_speed,
|
|
)
|
|
|
|
ctx = PipelineContext()
|
|
ctx.params = params
|
|
|
|
pipeline = Pipeline(config=config, context=ctx)
|
|
|
|
# Add stages based on preset
|
|
from engine.data_sources.sources import HeadlinesDataSource
|
|
from engine.pipeline.adapters import DataSourceStage
|
|
|
|
# Add source stage
|
|
source = HeadlinesDataSource()
|
|
pipeline.add_stage("source", DataSourceStage(source, name="headlines"))
|
|
|
|
# Add message overlay if enabled
|
|
if getattr(preset, "enable_message_overlay", False):
|
|
from engine import config as engine_config
|
|
from engine.pipeline.adapters import MessageOverlayConfig, MessageOverlayStage
|
|
|
|
overlay_config = MessageOverlayConfig(
|
|
enabled=True,
|
|
display_secs=getattr(engine_config, "MESSAGE_DISPLAY_SECS", 30),
|
|
topic_url=getattr(engine_config, "NTFY_TOPIC", None),
|
|
)
|
|
pipeline.add_stage(
|
|
"message_overlay", MessageOverlayStage(config=overlay_config)
|
|
)
|
|
|
|
# Add display stage
|
|
pipeline.add_stage("display", create_stage_from_display(display, "null"))
|
|
|
|
# Build and initialize
|
|
pipeline.build()
|
|
if not pipeline.initialize():
|
|
print("Error: Failed to initialize pipeline")
|
|
return False
|
|
|
|
# Capture frames
|
|
print(f"Capturing {frames} frames...")
|
|
start_time = time.time()
|
|
|
|
for frame in range(frames):
|
|
try:
|
|
pipeline.execute([])
|
|
if frame % 10 == 0:
|
|
print(f" Frame {frame}/{frames}")
|
|
except Exception as e:
|
|
print(f"Error on frame {frame}: {e}")
|
|
break
|
|
|
|
elapsed = time.time() - start_time
|
|
print(f"Captured {frame + 1} frames in {elapsed:.2f}s")
|
|
|
|
# Get captured frames
|
|
captured_frames = display.get_frames()
|
|
print(f"Retrieved {len(captured_frames)} frames from display")
|
|
|
|
# Save to JSON
|
|
output_path = Path(output_file)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
recording_data = {
|
|
"version": 1,
|
|
"preset": preset_name,
|
|
"display": "null",
|
|
"width": width,
|
|
"height": height,
|
|
"frame_count": len(captured_frames),
|
|
"frames": [
|
|
{
|
|
"frame_number": i,
|
|
"buffer": frame,
|
|
"width": width,
|
|
"height": height,
|
|
}
|
|
for i, frame in enumerate(captured_frames)
|
|
],
|
|
}
|
|
|
|
with open(output_path, "w") as f:
|
|
json.dump(recording_data, f, indent=2)
|
|
|
|
print(f"Saved recording to {output_path}")
|
|
return True
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Capture Mainline pipeline output")
|
|
parser.add_argument(
|
|
"--preset",
|
|
default="demo",
|
|
help="Preset name to use (default: demo)",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
default="output/capture.json",
|
|
help="Output file path (default: output/capture.json)",
|
|
)
|
|
parser.add_argument(
|
|
"--frames",
|
|
type=int,
|
|
default=60,
|
|
help="Number of frames to capture (default: 60)",
|
|
)
|
|
parser.add_argument(
|
|
"--width",
|
|
type=int,
|
|
default=80,
|
|
help="Terminal width (default: 80)",
|
|
)
|
|
parser.add_argument(
|
|
"--height",
|
|
type=int,
|
|
default=24,
|
|
help="Terminal height (default: 24)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
success = capture_pipeline_output(
|
|
preset_name=args.preset,
|
|
output_file=args.output,
|
|
frames=args.frames,
|
|
width=args.width,
|
|
height=args.height,
|
|
)
|
|
|
|
return 0 if success else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|