diff --git a/take-away/docker-compose.yaml b/take-away/docker-compose.yaml index d53a6c3..70b0416 100755 --- a/take-away/docker-compose.yaml +++ b/take-away/docker-compose.yaml @@ -125,11 +125,11 @@ services: # GStreamer capture settings - CAPTURE_FPS: ${CAPTURE_FPS:-1} # Frames per second for pipeline (1fps for multi-station) + CAPTURE_FPS: ${CAPTURE_FPS:-10} # Frames per second for pipeline (10fps) # Frame pipeline tuning (order 384 fix: OCR models take ~30s to load) # Pre-buffer must hold frames until OCR is ready to detect orders - PRE_BUFFER_FRAMES: ${PRE_BUFFER_FRAMES:-100} # Frames to hold in buffer (100 @ 1fps = 100s) + PRE_BUFFER_FRAMES: ${PRE_BUFFER_FRAMES:-1000} # Frames to hold in buffer (1000 @ 10fps = 100s) OCR_WARMUP_FRAMES: ${OCR_WARMUP_FRAMES:-2} # Frames to wait before OCR ready signal (reduced for faster startup) # Performance tuning @@ -140,6 +140,11 @@ services: # Metrics logging CONTAINER_RESULTS_PATH: ${CONTAINER_RESULTS_PATH:-/results} USECASE_1: ${USECASE_1:-take-away-order-accuracy} + + # Order Recall - path where frame-selector writes debug frames + FRAME_SELECTOR_DEBUG_DIR: ${FRAME_SELECTOR_DEBUG_DIR:-/results/frame-selector-in} + # Playback FPS for stitched recall replay video + RECALL_REPLAY_FPS: ${RECALL_REPLAY_FPS:-10} # RabbitMQ disabled for simplicity (direct HTTP calls used) USE_RABBITMQ: ${USE_RABBITMQ:-false} diff --git a/take-away/gradio-ui/gradio_app.py b/take-away/gradio-ui/gradio_app.py index ff7f3fa..c7dc584 100755 --- a/take-away/gradio-ui/gradio_app.py +++ b/take-away/gradio-ui/gradio_app.py @@ -4,6 +4,8 @@ import threading import time import math +import tempfile +import os import numpy as np from typing import Optional, List, Dict from PIL import Image @@ -26,7 +28,7 @@ # Global processing state is_processing = False -APP_TITLE = "๐Ÿ“ฆ Take-Away Order Accuracy" +APP_TITLE = "Take-Away Order Accuracy" APP_DESCRIPTION = "AI-powered order validation for take-away operations" # Intel Brand CSS (matching dine-in styling) @@ -35,7 +37,7 @@ :root { --primary-500: #0071C5 !important; --primary-600: #005A9E !important; - --primary-700: #00285A !important; + --primary-700: #0258b5 !important; --neutral-50: #f8fafc; --neutral-100: #f1f5f9; --neutral-200: #e2e8f0; @@ -75,7 +77,7 @@ /* Header Banner - Intel Blue */ .header-banner { - background: linear-gradient(135deg, #0071C5 0%, #00285A 100%); + background: linear-gradient(135deg, #0071C5 0%, #0258b5 100%); color: white; padding: 28px 40px; border-radius: 12px; @@ -123,7 +125,7 @@ /* Primary Button - Intel Blue */ .primary-btn { - background: linear-gradient(135deg, #0071C5 0%, #00285A 100%) !important; + background: linear-gradient(135deg, #0071C5 0%, #0258b5 100%) !important; border: none !important; border-radius: 10px !important; font-weight: 600 !important; @@ -156,7 +158,7 @@ } .video-history-header { - background: linear-gradient(135deg, #0071C5 0%, #00285A 100%); + background: linear-gradient(135deg, #0071C5 0%, #0258b5 100%); color: white; padding: 14px 20px; cursor: pointer; @@ -181,7 +183,7 @@ padding: 12px 15px; text-align: left; font-weight: 600; - color: #00285A; + color: #0258b5; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; @@ -231,7 +233,7 @@ /* Section Headers */ .section-header { font-weight: 600; - color: #00285A; + color: #0258b5; font-size: 14px; margin-bottom: 10px; } @@ -239,7 +241,7 @@ /* Footer */ .footer-info { text-align: center; - color: #00285A; + color: #0258b5; font-size: 13px; padding: 20px; border-top: 2px solid #E6F3FB; @@ -391,11 +393,11 @@ def start_smooth_stream(rtsp_url): current_time = time.time() if current_time - last_update >= 1.0: fps = fps_counter / (current_time - last_update) - status = f"โœ… Frame {frame_count} - Smooth stream active ({fps:.1f} FPS)" + status = f"Frame {frame_count} - Smooth stream active ({fps:.1f} FPS)" fps_counter = 0 last_update = current_time else: - status = f"โœ… Frame {frame_count} - Smooth stream active" + status = f"Frame {frame_count} - Smooth stream active" yield frame, status @@ -461,7 +463,7 @@ def upload_video_with_progress(file, progress=gr.Progress()): initial_stats = fetch_statistics() initial_processed = initial_stats.get("total_processed", 0) if initial_stats else 0 - progress(0.1, desc="๐Ÿ“ค Uploading video...") + progress(0.1, desc="Uploading video...") with open(file.name, "rb") as f: resp = _api.post( @@ -472,7 +474,7 @@ def upload_video_with_progress(file, progress=gr.Progress()): if resp.status_code != 200: is_processing = False - return f"โŒ Upload failed: {resp.text}", gr.update(interactive=True, value="๐Ÿš€ Upload & Start Processing") + return f"โŒ Upload failed: {resp.text}", gr.update(interactive=True, value="Upload & Start Processing") data = resp.json() video_id = data.get('video_id', 'unknown') @@ -503,11 +505,11 @@ def upload_video_with_progress(file, progress=gr.Progress()): if orders_completed > 0: # Bump progress when orders complete order_progress = min(0.9, time_progress + 0.1 * orders_completed) - progress(order_progress, desc=f"๐Ÿ”„ {orders_completed} order(s) detected") + progress(order_progress, desc=f"{orders_completed} order(s) detected") else: - progress(time_progress, desc=f"๐Ÿ” Analyzing video...") + progress(time_progress, desc=f"Analyzing video...") else: - progress(time_progress, desc=f"๐Ÿ”„ Processing...") + progress(time_progress, desc=f"Processing...") # Check if results are available results = fetch_results() @@ -520,7 +522,7 @@ def upload_video_with_progress(file, progress=gr.Progress()): results = fetch_results() break - progress(1.0, desc="โœ… Processing complete!") + progress(1.0, desc="Processing complete!") # Mark video as completed via API try: @@ -541,12 +543,12 @@ def upload_video_with_progress(file, progress=gr.Progress()): mismatch = stats.get("total_mismatch", 0) if stats else 0 return ( - f"โœ… Video processed successfully\n" + f"Video processed successfully\n" f"Video: {video_name}\n" f"Time: {upload_time}\n" f"Orders detected: {len(session_results)}\n" f"Validated: {validated} | Mismatch: {mismatch}", - gr.update(interactive=True, value="๐Ÿš€ Upload & Start Processing") + gr.update(interactive=True, value="Upload & Start Processing") ) except Exception as e: @@ -557,7 +559,7 @@ def upload_video_with_progress(file, progress=gr.Progress()): _api.post(f"{API_BASE}/videos/{video_id}/fail?error={str(e)}", timeout=5) except: pass - return f"โŒ Upload error: {e}", gr.update(interactive=True, value="๐Ÿš€ Upload & Start Processing") + return f"Upload error: {e}", gr.update(interactive=True, value="Upload & Start Processing") def generate_validation_summary(results): """Generate validation summary from results""" @@ -621,10 +623,10 @@ def start_rtsp_processing(rtsp_url): data = resp.json() video_id = data.get("video_id", "") return ( - f"โœ… RTSP pipeline started\n" + f"RTSP pipeline started\n" f"Source: {rtsp_url}\n" f"Video ID: {video_id}\n" - f"Results will appear in the '๐Ÿ“Š Detected Orders' tab." + f"Results will appear in the 'Detected Orders' tab." ) except Exception as e: @@ -661,24 +663,24 @@ def format_validation_summary_table(results): - - - + + + - - - + + + - + - + @@ -724,8 +726,8 @@ def format_order_card(result): items_rows += f''' - - + + ''' @@ -734,13 +736,13 @@ def format_order_card(result): validation_details = "" if missing: missing_items = ", ".join([f"{m.get('name', 'Unknown')} (ร—{m.get('quantity', 1)})" for m in missing]) - validation_details += f'
โš ๏ธ Missing: {missing_items}
' + validation_details += f'
Missing: {missing_items}
' if extra: extra_items = ", ".join([f"{e.get('name', 'Unknown')} (ร—{e.get('quantity', 1)})" for e in extra]) - validation_details += f'
โž• Extra: {extra_items}
' + validation_details += f'
Extra: {extra_items}
' if qty_mismatch: qty_items = ", ".join([f"{q.get('name', 'Unknown')}" for q in qty_mismatch]) - validation_details += f'
๐Ÿ“Š Qty Mismatch: {qty_items}
' + validation_details += f'
Qty Mismatch: {qty_items}
' return f'''
@@ -816,7 +818,7 @@ def format_history_html(): # Status badge for video if status == "processing": - video_status = 'โณ Processing' + video_status = 'Processing' elif status == "completed": video_status = 'โœ“ Completed' elif status == "failed": @@ -846,9 +848,9 @@ def format_history_html(): # Use Gradio accordion-compatible structure html_parts.append(f'''
- +
-
๐Ÿ“น {video_name}
+
{video_name}
{timestamp}
@@ -857,10 +859,10 @@ def format_history_html():
-
๐Ÿ“Š Validation Summary
+
Validation Summary
{summary_table if summary_table else '
No results available
'} -
๐Ÿงพ Order Details
+
Order Details
{order_cards if order_cards else '
No orders detected
'}
@@ -908,12 +910,12 @@ def format_detected_orders(): rows.append([ order_id, "\n".join(item_lines) if item_lines else "No items", - "โœ… VALIDATED" if status == "validated" else "โŒ MISMATCH" + "โœ… VALIDATED" if status == "validated" else "MISMATCH" ]) summaries.append( f"### Order {order_id}\n" - f"- Status: {'โœ… VALIDATED' if status == 'validated' else 'โŒ MISMATCH'}\n" + f"- Status: {'โœ… VALIDATED' if status == 'validated' else 'MISMATCH'}\n" f"- Missing: {missing or 'None'}\n" f"- Extra: {extra or 'None'}\n" f"- Quantity Mismatch: {qty_mismatch or 'None'}" @@ -925,6 +927,236 @@ def refresh_history(): """Refresh history display""" return format_history_html() + +# ----------------------------- +# ORDER RECALL HELPERS +# ----------------------------- + +def format_recall_card(order_id, result, upload_time, completed_time, source, frames_available): + """Format the order details card for the Recall tab.""" + if not result: + return f''' +
+
โš ๏ธ Order {order_id} found in history but result details are unavailable.
+
Processed: {upload_time}
+
+ ''' + + status = result.get('status', 'unknown') + detected_items = result.get('detected_items', []) + expected_items = result.get('expected_items', []) + validation = result.get('validation', {}) + missing = validation.get('missing', []) + extra = validation.get('extra', []) + qty_mismatch = validation.get('quantity_mismatch', []) + inference_time = result.get('inference_time_sec') + num_frames = result.get('num_frames', frames_available) + + status_icon = 'โœ…' if status == 'validated' else 'โŒ' + status_text = 'VALIDATED' if status == 'validated' else 'MISMATCH' + status_bg = '#d1fae5' if status == 'validated' else '#fee2e2' + status_color = '#10b981' if status == 'validated' else '#ef4444' + border_color = status_color + + # โ”€โ”€ Expected items table โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + exp_rows = '' + for item in expected_items: + exp_rows += f''' +
+ + + ''' + + # โ”€โ”€ Detected items table with per-item status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + det_rows = '' + for item in detected_items: + name = item.get('name', '?') + qty = item.get('quantity', 0) + s, c = 'OK', '#10b981' + if any(m.get('name') == name for m in missing): + s, c = 'Missing', '#ef4444' + elif any(e.get('name') == name for e in extra): + s, c = 'Extra', '#f59e0b' + elif any(q.get('name') == name for q in qty_mismatch): + s, c = 'Qty Mismatch', '#f59e0b' + det_rows += f''' + + + + + ''' + + # โ”€โ”€ Mismatch summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + mismatch_detail = '' + if missing: + items_str = ', '.join(f"{m.get('name','?')} ร—{m.get('quantity',1)}" for m in missing) + mismatch_detail += f'
Missing: {items_str}
' + if extra: + items_str = ', '.join(f"{e.get('name','?')} ร—{e.get('quantity',1)}" for e in extra) + mismatch_detail += f'
Extra: {items_str}
' + if qty_mismatch: + items_str = ', '.join(q.get('name', '?') for q in qty_mismatch) + mismatch_detail += f'
Qty Mismatch: {items_str}
' + if not mismatch_detail: + mismatch_detail = '
No mismatches
' + + # โ”€โ”€ Meta row (source, timing, frames) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + meta_items = [] + if upload_time: + meta_items.append(f'Processed: {upload_time}') + if source: + meta_items.append(f'Source: {source}') + if inference_time: + meta_items.append(f'Inference: {inference_time:.1f}s') + if num_frames: + meta_items.append(f'Frames: {num_frames}') + meta_html = '  |  '.join(meta_items) + + return f''' +
+ + +
+
Order #{order_id}
+ + {status_icon} {status_text} + +
+ + +
+ {meta_html} +
+ +
+ + +
Expected Items
+
MetricCountPercentageMetricCountPercentage
Total Orders{total}100%Total Orders{total}100%
โœ… ValidatedValidated {validated} {validated/total*100:.1f}%
โŒ MismatchMismatch {mismatch} {mismatch/total*100:.1f}%
{name}{qty}{name}{qty} {item_status}
{item.get("name","?")}{item.get("quantity",0)}
{name}{qty}{s}
+ + + + + {exp_rows if exp_rows else ""} +
ItemQty
No expected items
+ + +
Detected Items
+ + + + + + + {det_rows if det_rows else ""} +
ItemQtyStatus
No items detected
+ + +
Validation Summary
+
+ {mismatch_detail} +
+ + + + ''' + + +def recall_order_fn(order_id: str): + """Gradio callback for the Order Recall tab. + + Returns: + (details_html, video_path_or_None) + """ + order_id = (order_id or '').strip() + + if not order_id: + return ( + '
' \ + '
๐Ÿ”
' \ + '
Enter an order ID above and click Recall Order.
', + None + ) + + # โ”€โ”€ Call backend recall endpoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + try: + resp = _api.get(f"{API_BASE}/orders/{order_id}/recall", timeout=10) + data = resp.json() + except Exception as e: + return ( + f'
' + f'
โŒ Error contacting backend
' + f'
{e}
', + None + ) + + status = data.get('status') + + if status == 'not_found': + return ( + f'
' + f'
๐Ÿšซ
' + f'
Order {order_id} not found
' + f'
' + f'This order ID was never processed, or its history has been cleared.
', + None + ) + + if status == 'expired': + upload_time = data.get('upload_time', 'unknown') + max_age = data.get('max_age_hours', 24) + return ( + f'
' + f'
โฐ
' + f'
Recall window expired for order {order_id}
' + f'
' + f'This order was processed on {upload_time} โ€” ' + f'orders can only be recalled within the last {max_age} hours.
', + None + ) + + # โ”€โ”€ status == 'found' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + result = data.get('result') or {} + upload_time = data.get('upload_time', '') + completed_time = data.get('completed_time', '') + source = data.get('source', '') + has_replay = data.get('has_replay', False) + frames_available = data.get('frames_available', 0) + + details_html = format_recall_card( + order_id, result, upload_time, completed_time, source, frames_available + ) + + # โ”€โ”€ Download replay MP4 from backend โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + video_path = None + if has_replay: + try: + video_resp = _api.get( + f"{API_BASE}/orders/{order_id}/replay", + timeout=60, + stream=True + ) + if video_resp.status_code == 200: + tmp = tempfile.NamedTemporaryFile( + delete=False, + suffix=f"_order_{order_id}_replay.mp4", + dir="/tmp" + ) + for chunk in video_resp.iter_content(chunk_size=8192): + if chunk: + tmp.write(chunk) + tmp.close() + video_path = tmp.name + else: + print(f"[Recall] Replay endpoint returned {video_resp.status_code} for order {order_id}") + except Exception as e: + print(f"[Recall] Failed to download replay video for order {order_id}: {e}") + + return details_html, video_path + + # ----------------------------- # UI # ----------------------------- @@ -962,21 +1194,21 @@ def refresh_history(): # ====================== # FILE UPLOAD TAB # ====================== - with gr.TabItem("๐Ÿ“ Upload Video"): + with gr.TabItem("Upload Video"): gr.HTML('
') with gr.Row(): with gr.Column(scale=2): upload_file = gr.File( - label="๐Ÿ“น Select Video File", + label="Select Video File", file_types=[".mp4", ".avi", ".mkv", ".mov"], elem_classes=["card-panel"] ) with gr.Column(scale=1): - gr.HTML('
๐ŸŽฌ Upload Controls
') + gr.HTML('
Upload Controls
') upload_btn = gr.Button( - "๐Ÿš€ Upload & Start Processing", + "Upload & Start Processing", variant="primary", elem_classes=["primary-btn"], size="lg", @@ -997,7 +1229,7 @@ def refresh_history(): # Connect upload function with button state management upload_btn.click( - fn=lambda: gr.update(interactive=False, value="โณ Processing..."), + fn=lambda: gr.update(interactive=False, value="Processing..."), outputs=upload_btn ).then( fn=upload_video_with_progress, @@ -1009,7 +1241,7 @@ def refresh_history(): # ====================== # RTSP STREAM TAB # ====================== - with gr.TabItem("๐Ÿ“ก RTSP Stream"): + with gr.TabItem("RTSP Stream"): gr.HTML('
') # โ”€โ”€ Row 1: Mode badge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -1025,7 +1257,7 @@ def refresh_history(): with gr.Row(): # Left column: Controls with gr.Column(scale=1): - gr.HTML('
๐Ÿ”— Stream Configuration
') + gr.HTML('
Stream Configuration
') rtsp_url = gr.Textbox( label="RTSP URL", placeholder="rtsp://rtsp-streamer:8554/station_1", @@ -1033,21 +1265,21 @@ def refresh_history(): ) gr.HTML('
') - gr.HTML('
๐ŸŽฅ Stream Controls
') + gr.HTML('
Stream Controls
') with gr.Row(): - stream_start_btn = gr.Button("โ–ถ๏ธ Start Preview", variant="primary", elem_classes=["primary-btn"]) - stream_stop_btn = gr.Button("โน๏ธ Stop", variant="secondary") + stream_start_btn = gr.Button("Start Preview", variant="primary", elem_classes=["primary-btn"]) + stream_stop_btn = gr.Button("Stop", variant="secondary") gr.HTML('
') - gr.HTML('
๐Ÿ”„ Processing Pipeline
') - process_btn = gr.Button("๐Ÿš€ Start Processing", variant="primary", elem_classes=["primary-btn"]) + gr.HTML('
Processing Pipeline
') + process_btn = gr.Button("Start Processing", variant="primary", elem_classes=["primary-btn"]) processing_status = gr.Textbox(label="Pipeline Status", lines=2, interactive=False) # Right column: Stream display with gr.Column(scale=2): - gr.HTML('
๐Ÿ“บ Live Stream Preview
') + gr.HTML('
Live Stream Preview
') stream_image = gr.Image( label="", width=None, @@ -1065,10 +1297,10 @@ def _load_mode_status(): mode = info.get('service_mode', 'unknown') workers = info.get('workers', 1) if mode == 'parallel': - return f"โšก Parallel mode โ€” {workers} station(s) running. 'Start Processing' will show station status." + return f"Parallel mode โ€” {workers} station(s) running. 'Start Processing' will show station status." elif mode == 'single': - return "๐Ÿ”ต Single mode โ€” 'Start Processing' will launch a new GStreamer pipeline for this URL." - return f"โš ๏ธ Mode unknown ({mode})" + return "Single mode โ€” 'Start Processing' will launch a new GStreamer pipeline for this URL." + return f"Mode unknown ({mode})" demo.load(fn=_load_mode_status, inputs=None, outputs=[mode_status]) @@ -1093,10 +1325,10 @@ def _load_mode_status(): # ====================== # RESULTS TAB - History View # ====================== - with gr.TabItem("๐Ÿ“Š Detected Orders"): + with gr.TabItem("Detected Orders"): gr.HTML('
') - gr.HTML('
๐Ÿ“œ Video Processing History
') + gr.HTML('
Video Processing History
') gr.HTML('

Click on any video entry to expand and view detailed validation results.

') results_history_display = gr.HTML( @@ -1105,8 +1337,8 @@ def _load_mode_status(): ) with gr.Row(): - refresh_btn = gr.Button("๐Ÿ”„ Refresh Results", variant="secondary") - clear_history_btn = gr.Button("๐Ÿ—‘๏ธ Clear History", variant="secondary") + refresh_btn = gr.Button("Refresh Results", variant="secondary") + clear_history_btn = gr.Button("Clear History", variant="secondary") refresh_btn.click( fn=refresh_history, @@ -1118,6 +1350,74 @@ def _load_mode_status(): outputs=results_history_display ) + # ====================== + # ORDER RECALL TAB + # ====================== + with gr.TabItem("Order Recall"): + gr.HTML('
') + gr.HTML( + '
' + 'Order Recall
' + '

' + 'Enter an order ID to view its validation result and replay the footage. ' + 'Orders can be recalled within 24 hours of processing.

' + ) + + # โ”€โ”€ Search row โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + with gr.Row(): + recall_input = gr.Textbox( + label="Order ID", + placeholder="e.g. 384", + scale=4, + container=True + ) + recall_btn = gr.Button( + "Recall Order", + variant="primary", + elem_classes=["primary-btn"], + scale=1, + size="lg" + ) + + # โ”€โ”€ Results row: details card (left) | video player (right) โ”€โ”€โ”€โ”€โ”€โ”€ + with gr.Row(): + with gr.Column(scale=1): + recall_details = gr.HTML( + value='
' + '
๐Ÿ”
' + '
' + 'Enter an order ID above and click Recall Order.
', + label="Order Details" + ) + + with gr.Column(scale=2): + gr.HTML('
' + 'Order Replay
') + recall_video = gr.Video( + label="", + interactive=False, + show_download_button=True, + show_label=False, + height=420 + ) + gr.HTML( + '

' + 'Replay is built from all frames captured during the order window ' + '(1 fps capture โ†’ played back at 2 fps).

' + ) + + # โ”€โ”€ Wire up button and Enter key โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + recall_btn.click( + fn=recall_order_fn, + inputs=[recall_input], + outputs=[recall_details, recall_video] + ) + recall_input.submit( + fn=recall_order_fn, + inputs=[recall_input], + outputs=[recall_details, recall_video] + ) + # Footer with Intel Branding gr.HTML('''