# Truck Counting Bug Fix + Box Counts + Download Compression Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Fix truck-classed-as-sack counting bug, propagate sack vs box counts to results UI, compress downloads to max 250MB. **Architecture:** (1) Filter truck class before counting and switch both pipelines to `MultiClassLineCounter` (defense in depth + box counts); fix dashboard hardcoded "sack" label. (2) Add box count fields to result dataclasses and UI. (3) Compress download on-the-fly via ffmpeg targeting 250MB. **Tech Stack:** Python 3.10, Flask, OpenCV, ffmpeg subprocess, existing `MultiClassLineCounter` **Spec:** Original repo reference: `/tmp/karung-counting-feedmill-semarang/predict.py` lines 1497-1500 (class filter), 1409 (`MultiClassLineCounter`) ## Global Constraints - Python >= 3.10, NumPy < 2.0, Jetson Orin Nano - All existing tests (71) must pass after each task - Follow existing Flat Design system (teal/orange) - Git remote: `git.proit.id/andrew/feedmill-recounter.git` ## File Structure | Action | File | Responsibility | |--------|------|----------------| | Modify | `src/pipeline.py` | Class filter before counter, switch to MultiClassLineCounter, box counts in results | | Modify | `src/job.py` | Box count fields in JobResult, pass from pipeline results | | Modify | `src/dashboard.py` | Label from det.class_name not hardcoded "sack" | | Modify | `templates/status.html` | Show sack vs box counts separately | | Modify | `app.py` | Download route compresses via ffmpeg to 250MB | | Modify | `tests/` | New tests for filter, box counts, compression | --- ### Task 1: Fix truck counting bug (pipeline + dashboard) **Files:** - Modify: `src/pipeline.py` - Modify: `src/dashboard.py` - Create/Modify: `tests/test_pipeline.py` (add filter tests) **Root cause:** 1. `run_merged_pipeline` line 519: `counter.update([d for d in tracked if d.track_id is not None])` — no class filter, trucks counted 2. `dashboard.py` line 155: `label = "sack"` hardcoded 3. Both pipelines use `LineCrossCounter` which counts any class **Changes:** 1. `src/pipeline.py` — import `MultiClassLineCounter` instead of / in addition to `LineCrossCounter` 2. `run_pipeline` (line 163): replace `LineCrossCounter(...)` with `MultiClassLineCounter(...)` (same args) 3. `run_merged_pipeline` (line 426): same replacement 4. `run_pipeline` line 248: `counter.update(tracked_sacks)` → filter first: ```python counter.update([d for d in tracked_sacks if d.class_name in ("sack", "box")]) ``` 5. `run_merged_pipeline` line 519: ```python countable = [d for d in tracked if d.track_id is not None and d.class_name in ("sack", "box")] counter.update(countable) ``` 6. `src/dashboard.py` `_draw_detections` line 155: `label = "sack"` → `label = det.class_name` **Interfaces:** - Consumes: `MultiClassLineCounter` from src/counting.py (exists, has `box_loading_count`, `box_unloading_count` properties) - Produces: counter objects are now `MultiClassLineCounter` — same `.loading_count`/`.unloading_count` properties plus new `.box_loading_count`/`.box_unloading_count` **Steps:** - [ ] Write failing test: truck class detections not counted - [ ] Run test to verify fail - [ ] Apply 6 changes above - [ ] Run full suite: `python -m pytest tests/ --tb=short` - [ ] Commit: `fix: exclude truck class from counting, use MultiClassLineCounter, real class labels` --- ### Task 2: Box count propagation to results UI **Files:** - Modify: `src/pipeline.py` (PipelineResult, MergedPipelineResult, return statements) - Modify: `src/job.py` (JobResult, both branches of _run_job) - Modify: `templates/status.html` - Modify: `static/app.js` (live stats if applicable) **Changes:** 1. `PipelineResult` dataclass: add `box_loading_count: int = 0`, `box_unloading_count: int = 0`, property `box_net_count` 2. `MergedPipelineResult` dataclass: same fields 3. `run_pipeline` return (line ~323): add `box_loading_count=counter.box_loading_count, box_unloading_count=counter.box_unloading_count` 4. `run_merged_pipeline` return (line ~585): same 5. `JobResult` dataclass: add `box_loading_count: int = 0`, `box_unloading_count: int = 0`, `box_net_count: int = 0` 6. Both `JobResult` construction sites in `_run_job` (lines ~220 and ~264): pass box counts from result 7. `templates/status.html` results card: add box stats blocks: ```html
{{ r.box_loading_count }} Box In
{{ r.box_unloading_count }} Box Out
``` Keep existing Loading/Unloading as sack counts (rename labels to "Sack In"/"Sack Out" for clarity). 8. `/api/jobs/` in app.py: include box counts in results JSON **Interfaces:** - Consumes: box counts from Task 1's MultiClassLineCounter - Produces: `box_loading_count` etc. on JobResult — status.html and API consumers **Steps:** - [ ] Write failing test for box count propagation - [ ] Run to verify fail - [ ] Apply changes - [ ] Run full suite - [ ] Commit: `feat: propagate sack vs box counts to results and UI` --- ### Task 3: Download compression to max 250MB **Files:** - Modify: `app.py` (download route) - Create: `tests/test_download_compress.py` **Changes:** 1. Add helper `_compress_for_download(file_path, max_bytes=250*1024*1024) -> str | None`: - If file size <= max_bytes, return None (no compression) - Probe duration via ffprobe - Calculate target bitrate: `int((max_bytes * 8 * 0.92) / duration)` (8% safety margin, avoid container overhead overshoot) - ffmpeg: `ffmpeg -y -i input -c:v libx264 -b:v {bitrate} -c:a aac -b:a 64k output.mp4` - Output to `/_compressed.mp4` - If compressed file exists, reuse it - Return compressed path or None 2. `download()` route: call helper, send compressed file if produced, else original 3. Handle ffmpeg missing: fall back to original file (no error) **Steps:** - [ ] Write failing tests (small file not compressed; helper signature) - [ ] Run to verify fail - [ ] Implement helper + route change - [ ] Run full suite - [ ] Commit: `feat: compress downloads to max 250MB via ffmpeg`