# Plan: Live Preview Toggle, FPS Indicator, Live Counting ## Goal 1. Toggle on/off for live preview (disable when not needed for faster processing) 2. Live FPS indicator showing processing speed 3. Live counting by object type (sack loading/unloading, box loading/unloading) ## Architecture The pipeline already runs `counter.update(tracked_sacks)` every frame and computes `counter.loading_count` / `counter.unloading_count`. The `progress_callback` is already called every frame. We piggyback on this to push live stats to the Job object. Frontend polls `/api/jobs/` every 2s (already exists) and reads the new live stats. ``` Pipeline (every frame) → progress_callback(frame_idx, total_frames) → job.update_live_stats(fps, loading, unloading, boxes) Frontend (every 2s poll) → reads live_fps, live_loading, live_unloading, live_boxes from API → updates display ``` ## Implementation ### 1. `src/pipeline.py` — Add live_stats_callback, compute FPS Add parameter: ```python live_stats_callback: Callable[[dict], None] | None = None, ``` In the main loop, compute FPS (rolling average over last 30 frames) and call: ```python if live_stats_callback and frame_idx % 10 == 0: live_stats_callback({ "fps": processing_fps, "loading": counter.loading_count, "unloading": counter.unloading_count, "frame": frame_idx, }) ``` FPS computation: `time.time()` delta over last N frames, smoothed. ### 2. `src/job.py` — Add live stats fields to Job Add fields: ```python live_fps: float = 0.0 live_loading: int = 0 live_unloading: int = 0 live_frame: int = 0 ``` In `_run_job()`, pass `live_stats_callback` to `run_pipeline()`: ```python def _on_live_stats(stats): with self._lock: job.live_fps = stats["fps"] job.live_loading = stats["loading"] job.live_unloading = stats["unloading"] job.live_frame = stats["frame"] result = run_pipeline( ..., live_stats_callback=_on_live_stats, ) ``` ### 3. `app.py` — Add live stats to API response In `api_job_detail()`, add to the JSON response: ```python "live_fps": job.live_fps, "live_loading": job.live_loading, "live_unloading": job.live_unloading, "live_frame": job.live_frame, ``` ### 4. `templates/status.html` — Add toggle, FPS indicator, live counters Add to the live preview section header: ```html

Live Preview

-- FPS
``` Add live counting display below the preview image: ```html ``` ### 5. `static/app.js` — Toggle logic, update live stats from poll In `initStatusPage()`: **Toggle handler:** ```javascript var previewToggle = document.getElementById('preview-toggle'); var livePreviewImg = document.getElementById('live-preview-img'); if (previewToggle) { previewToggle.addEventListener('change', function() { if (this.checked) { livePreviewImg.src = '/api/preview/' + jobId; livePreviewSection.style.display = ''; } else { livePreviewImg.src = ''; livePreviewSection.style.display = 'none'; } }); } ``` **Update live stats from poll response:** ```javascript // In pollJob() success handler: if (data.live_fps !== undefined) { var fpsEl = document.getElementById('live-fps'); if (fpsEl) fpsEl.textContent = data.live_fps.toFixed(1) + ' FPS'; } if (data.live_loading !== undefined) { var countsSection = document.getElementById('live-counts'); if (countsSection) countsSection.style.display = ''; var el = document.getElementById('live-loading'); if (el) el.textContent = data.live_loading; el = document.getElementById('live-unloading'); if (el) el.textContent = data.live_unloading; el = document.getElementById('live-net'); if (el) el.textContent = (data.live_loading - data.live_unloading); } ``` ### 6. `static/style.css` — Style toggle, FPS badge, live counts ```css .live-preview-header { display: flex; align-items: center; gap: var(--space-3); } .live-fps { font-size: var(--text-xs); font-weight: var(--font-semibold); color: var(--color-primary); background: var(--color-primary-50); padding: 2px 8px; border-radius: var(--radius-sm); } .live-counts { display: flex; gap: var(--space-4); padding: var(--space-3) 0; } .live-count-item { text-align: center; } .live-count-label { font-size: var(--text-xs); color: var(--color-neutral-500); display: block; } .live-count-value { font-size: var(--text-xl); font-weight: var(--font-bold); } .toggle-label { display: flex; align-items: center; gap: var(--space-1); font-size: var(--text-sm); cursor: pointer; } ``` ## Files to Modify | File | Changes | |------|---------| | `src/pipeline.py` | Add `live_stats_callback` param, compute FPS, call callback every 10 frames | | `src/job.py` | Add `live_fps`, `live_loading`, `live_unloading`, `live_frame` fields, wire callback | | `app.py` | Add live stats to `api_job_detail()` response | | `templates/status.html` | Add toggle checkbox, FPS badge, live counting display | | `static/app.js` | Toggle handler, update live stats from poll response | | `static/style.css` | Style toggle, FPS badge, live counts | ## Expected Behavior - **Toggle OFF**: Live preview img hidden, no MJPEG stream (faster processing) - **Toggle ON**: MJPEG stream shown, FPS badge updates, live counts update - **FPS badge**: Shows "12.3 FPS" (rolling average) - **Live counts**: Shows Loading/Unloading/Net in real-time below the preview - **Counts per class**: Pipeline counts sack and box separately via `MultiClassLineCounter` (already exists, needs wiring) ## Performance Impact - FPS computation: negligible (1 `time.time()` per frame) - Live stats callback: negligible (dict + lock every 10 frames) - MJPEG toggle OFF: saves disk I/O (no file writes), reduces CPU (no resize/encode) - API poll: same 2s interval, just more fields in JSON response ## Testing 1. `python -m pytest tests/ -v --tb=short` — 62/62 pass 2. Start job, verify FPS badge shows ~12-25 FPS 3. Toggle OFF → verify preview disappears, processing continues 4. Toggle ON → verify preview reappears 5. Verify live counts increment during processing