From 7d4623b009a28b85c29bcb7e0744718445c223ab Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Sat, 21 Mar 2026 16:18:51 -0700 Subject: [PATCH] fix(comparison): Fix pipeline construction for proper headline rendering - Add source stage (headlines, poetry, or empty) - Add viewport filter and font stage for headlines/poetry - Add camera stages (camera_update and camera) - Add effect stages based on preset - Fix stage order: message_overlay BEFORE display - Add null display stage with recording enabled - Capture frames from null display recording The fix ensures that the comparison framework uses the same pipeline structure as the main pipeline runner, producing proper block character rendering for headlines and poetry sources. --- tests/comparison_capture.py | 135 ++++++++++++++++++++++++++++++++---- 1 file changed, 123 insertions(+), 12 deletions(-) diff --git a/tests/comparison_capture.py b/tests/comparison_capture.py index 1692a00..d54f87a 100644 --- a/tests/comparison_capture.py +++ b/tests/comparison_capture.py @@ -108,7 +108,88 @@ def capture_frames( ) ctx.params = params - # Add message overlay stage if enabled + # Add stages based on source type (similar to pipeline_runner) + from engine.display import DisplayRegistry + from engine.pipeline.adapters import create_stage_from_display + from engine.data_sources.sources import EmptyDataSource + from engine.pipeline.adapters import DataSourceStage + + # Add source stage + if preset.source == "empty": + source_stage = DataSourceStage( + EmptyDataSource(width=preset.viewport_width, height=preset.viewport_height), + name="empty", + ) + else: + # For headlines/poetry, use the actual source + from engine.data_sources.sources import HeadlinesDataSource, PoetryDataSource + + if preset.source == "headlines": + source_stage = DataSourceStage(HeadlinesDataSource(), name="headlines") + elif preset.source == "poetry": + source_stage = DataSourceStage(PoetryDataSource(), name="poetry") + else: + # Fallback to empty + source_stage = DataSourceStage( + EmptyDataSource( + width=preset.viewport_width, height=preset.viewport_height + ), + name="empty", + ) + pipeline.add_stage("source", source_stage) + + # Add font stage for headlines/poetry (with viewport filter) + 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") + ) + # Add font stage for block character rendering + pipeline.add_stage("font", FontStage(name="font")) + else: + # Fallback to simple conversion for empty/other sources + from engine.pipeline.adapters import SourceItemsToBufferStage + + pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer")) + + # Add camera stage + from engine.camera import Camera + from engine.pipeline.adapters import CameraStage, CameraClockStage + + # Create camera based on preset + if preset.camera == "feed": + camera = Camera.feed() + elif preset.camera == "scroll": + camera = Camera.scroll(speed=0.1) + elif preset.camera == "horizontal": + camera = Camera.horizontal(speed=0.1) + else: + camera = Camera.feed() + + camera.set_canvas_size(preset.viewport_width, preset.viewport_height * 2) + + # Add camera update (for animation) + pipeline.add_stage("camera_update", CameraClockStage(camera, name="camera-clock")) + # Add camera stage + pipeline.add_stage("camera", CameraStage(camera, name=preset.camera)) + + # Add effects + if preset.effects: + from engine.effects.registry import EffectRegistry + from engine.pipeline.adapters import create_stage_from_effect + + effect_registry = EffectRegistry() + 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 (BEFORE display) if getattr(preset, "enable_message_overlay", False): from engine.pipeline.adapters import MessageOverlayConfig, MessageOverlayStage @@ -120,9 +201,21 @@ def capture_frames( "message_overlay", MessageOverlayStage(config=overlay_config) ) + # Add null display stage (LAST) + null_display = DisplayRegistry.create("null") + if null_display: + pipeline.add_stage("display", create_stage_from_display(null_display, "null")) + # Build pipeline pipeline.build() + # Enable recording on null display if available + display_stage = pipeline._stages.get("display") + if display_stage and hasattr(display_stage, "_display"): + backend = display_stage._display + if hasattr(backend, "start_recording"): + backend.start_recording() + # Capture frames frames = [] start_time = time.time() @@ -132,18 +225,36 @@ def capture_frames( stage_result = pipeline.execute() frame_time = time.time() - frame_start - # Extract buffer from result - buffer = stage_result.data if stage_result.success else [] + # Get frames from display recording + display_stage = pipeline._stages.get("display") + if display_stage and hasattr(display_stage, "_display"): + backend = display_stage._display + if hasattr(backend, "get_recorded_data"): + recorded_frames = backend.get_recorded_data() + # Add render_time_ms to each frame + for frame in recorded_frames: + frame["render_time_ms"] = frame_time * 1000 + frames = recorded_frames - frames.append( - { - "frame_number": i, - "buffer": buffer, - "width": preset.viewport_width, - "height": preset.viewport_height, - "render_time_ms": frame_time * 1000, - } - ) + # Fallback: create empty frames if no recording + if not frames: + for i in range(frame_count): + frames.append( + { + "frame_number": i, + "buffer": [], + "width": preset.viewport_width, + "height": preset.viewport_height, + "render_time_ms": frame_time * 1000, + } + ) + + # Stop recording on null display + display_stage = pipeline._stages.get("display") + if display_stage and hasattr(display_stage, "_display"): + backend = display_stage._display + if hasattr(backend, "stop_recording"): + backend.stop_recording() total_time = time.time() - start_time