- 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
196 lines
6.7 KiB
Markdown
196 lines
6.7 KiB
Markdown
# 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:
|
|
```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
|
|
<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:
|
|
```html
|
|
<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:**
|
|
```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
|