11import time
22
3- from app .models .pipeline import PipelineRequest , PipelineResponse , PipelineTimings , StepTiming
3+ import cv2
4+ import numpy as np
5+
6+ from app .models .pipeline import (
7+ DebugStepState ,
8+ PipelineRequest ,
9+ PipelineResponse ,
10+ PipelineTimings ,
11+ StepTiming ,
12+ )
413from app .operators .registry import get_operator
514from app .utils .image import decode_base64_image , encode_image_base64
615
716NOOP_TYPES = {"basic_readimage" , "basic_writeimage" , "border_for_all" , "border_each_side" }
817
18+ DEBUG_ENCODE_FORMAT = "jpeg"
19+ DEBUG_JPEG_QUALITY = 70
20+ MAX_DEBUG_DIM = 512
21+ MAX_DEBUG_STEPS = 25
22+
23+
24+ def _thumbnail_for_debug (image : np .ndarray ) -> np .ndarray :
25+ """Resize image to fit within MAX_DEBUG_DIM for compact debug snapshots."""
26+ h , w = image .shape [:2 ]
27+ if max (h , w ) <= MAX_DEBUG_DIM :
28+ return image
29+ scale = MAX_DEBUG_DIM / max (h , w )
30+ new_w , new_h = int (w * scale ), int (h * scale )
31+ return cv2 .resize (image , (new_w , new_h ), interpolation = cv2 .INTER_AREA )
32+
33+
34+ def _encode_debug_image (image : np .ndarray ) -> str :
35+ """Encode an intermediate image as a compressed JPEG thumbnail."""
36+ thumb = _thumbnail_for_debug (image )
37+ # Ensure the image has 3 channels for JPEG encoding
38+ if len (thumb .shape ) == 2 :
39+ thumb = cv2 .cvtColor (thumb , cv2 .COLOR_GRAY2BGR )
40+ elif thumb .shape [2 ] == 4 :
41+ thumb = cv2 .cvtColor (thumb , cv2 .COLOR_BGRA2BGR )
42+ return encode_image_base64 (thumb , DEBUG_ENCODE_FORMAT , quality = DEBUG_JPEG_QUALITY )
43+
944
1045# Thread-safety: this function is safe to call concurrently from FastAPI's
1146# threadpool. All processing state (image array, operator instances, encoded
@@ -22,6 +57,7 @@ def execute_pipeline(request: PipelineRequest) -> PipelineResponse:
2257 """
2358 t_start_total = time .perf_counter ()
2459 step_timings : list [StepTiming ] = []
60+ debug_states : list [DebugStepState ] = []
2561
2662 try :
2763 image = decode_base64_image (request .image )
@@ -34,6 +70,18 @@ def execute_pipeline(request: PipelineRequest) -> PipelineResponse:
3470 timings = PipelineTimings (total_ms = (t_fail - t_start_total ) * 1000 , steps = step_timings ),
3571 )
3672
73+ # Step 0: capture the original decoded image as the "before anything" state
74+ if request .debug :
75+ debug_states .append (
76+ DebugStepState (
77+ step = 0 ,
78+ block_id = None ,
79+ operator_type = "original" ,
80+ image = _encode_debug_image (image ),
81+ duration_ms = 0.0 ,
82+ )
83+ )
84+
3785 for i , step in enumerate (request .pipeline ):
3886 if step .type in NOOP_TYPES :
3987 continue
@@ -46,23 +94,36 @@ def execute_pipeline(request: PipelineRequest) -> PipelineResponse:
4694 error = f"Unknown operator '{ step .type } ' at step { i + 1 } " ,
4795 step = i + 1 ,
4896 timings = PipelineTimings (total_ms = (t_fail - t_start_total ) * 1000 , steps = step_timings ),
97+ debug_states = debug_states if request .debug else None ,
4998 )
5099
51100 try :
52101 t_step_start = time .perf_counter ()
53102 operator = operator_cls (step .params )
54103 image = operator .compute (image )
55104 t_step_end = time .perf_counter ()
56- step_timings .append (
57- StepTiming (step = i + 1 , operator_type = step .type , duration_ms = (t_step_end - t_step_start ) * 1000 )
58- )
105+ step_ms = (t_step_end - t_step_start ) * 1000
106+ step_timings .append (StepTiming (step = i + 1 , operator_type = step .type , duration_ms = step_ms ))
107+
108+ # Debug injection: capture this step's output
109+ if request .debug and len (debug_states ) < MAX_DEBUG_STEPS :
110+ debug_states .append (
111+ DebugStepState (
112+ step = i + 1 ,
113+ block_id = step .id ,
114+ operator_type = step .type ,
115+ image = _encode_debug_image (image ),
116+ duration_ms = step_ms ,
117+ )
118+ )
59119 except Exception as e :
60120 t_fail = time .perf_counter ()
61121 return PipelineResponse (
62122 success = False ,
63123 error = f"Error in step { i + 1 } ({ step .type } ): { type (e ).__name__ } : { e } " ,
64124 step = i + 1 ,
65125 timings = PipelineTimings (total_ms = (t_fail - t_start_total ) * 1000 , steps = step_timings ),
126+ debug_states = debug_states if request .debug else None ,
66127 )
67128
68129 try :
@@ -75,6 +136,7 @@ def execute_pipeline(request: PipelineRequest) -> PipelineResponse:
75136 error = error_msg ,
76137 step = len (request .pipeline ),
77138 timings = PipelineTimings (total_ms = (t_fail - t_start_total ) * 1000 , steps = step_timings ),
139+ debug_states = debug_states if request .debug else None ,
78140 )
79141
80142 t_end_total = time .perf_counter ()
@@ -84,4 +146,5 @@ def execute_pipeline(request: PipelineRequest) -> PipelineResponse:
84146 image = encoded ,
85147 image_format = request .image_format ,
86148 timings = PipelineTimings (total_ms = (t_end_total - t_start_total ) * 1000 , steps = step_timings ),
149+ debug_states = debug_states if request .debug else None ,
87150 )
0 commit comments