Files
feedmill-recounter/.opencode/plans/live-preview-toggle-fps-counts.md
T
jetson 09a8829575 feat: live preview toggle, FPS indicator, live counting by object
- Toggle ON/OFF switch to disable preview (faster processing)
- FPS badge showing rolling average processing speed
- Live Loading/Unloading/Net counts updated in real-time
- Pipeline callback pushes stats every 10 frames
- Frontend polls and updates display every 2s
- 62/62 tests pass
2026-09-22 10:36:11 +07:00

6.7 KiB

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/<job_id> 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:

live_stats_callback: Callable[[dict], None] | None = None,

In the main loop, compute FPS (rolling average over last 30 frames) and call:

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:

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():

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:

"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:

<div class="live-preview-header">
  <h3 class="section-title">Live Preview</h3>
  <label class="toggle-label">
    <input type="checkbox" id="preview-toggle" checked>
    <span>ON</span>
  </label>
  <span class="live-fps" id="live-fps">-- FPS</span>
</div>

Add live counting display below the preview image:

<div class="live-counts" id="live-counts" style="display:none">
  <div class="live-count-item">
    <span class="live-count-label">Loading</span>
    <span class="live-count-value" id="live-loading">0</span>
  </div>
  <div class="live-count-item">
    <span class="live-count-label">Unloading</span>
    <span class="live-count-value" id="live-unloading">0</span>
  </div>
  <div class="live-count-item">
    <span class="live-count-label">Net</span>
    <span class="live-count-value" id="live-net">0</span>
  </div>
</div>

5. static/app.js — Toggle logic, update live stats from poll

In initStatusPage():

Toggle handler:

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:

// 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

.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