From 7b6122588ee07eb169a0212db908f475673a0075 Mon Sep 17 00:00:00 2001 From: Suryam Agarwal Date: Thu, 12 Mar 2026 19:42:36 +0530 Subject: [PATCH 1/2] Video recall at 10 FPS --- take-away/docker-compose.yaml | 9 +- take-away/gradio-ui/gradio_app.py | 300 +++++++++++++++++++++++ take-away/src/api/endpoints.py | 181 +++++++++++++- take-away/src/core/pipeline_runner.py | 4 +- take-away/src/core/video_history.py | 74 +++++- take-away/src/parallel/station_worker.py | 2 +- 6 files changed, 562 insertions(+), 8 deletions(-) diff --git a/take-away/docker-compose.yaml b/take-away/docker-compose.yaml index 7cbe972..ed58d35 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..efc5a17 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 @@ -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''' + + {item.get("name","?")} + {item.get("quantity",0)} + ''' + + # ── 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''' + + {name} + {qty} + {s} + ''' + + # ── 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
+ + + + + + {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 # ----------------------------- @@ -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('''