6.3 KiB
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:
run_merged_pipelineline 519:counter.update([d for d in tracked if d.track_id is not None])— no class filter, trucks counteddashboard.pyline 155:label = "sack"hardcoded- Both pipelines use
LineCrossCounterwhich counts any class
Changes:
-
src/pipeline.py— importMultiClassLineCounterinstead of / in addition toLineCrossCounter -
run_pipeline(line 163): replaceLineCrossCounter(...)withMultiClassLineCounter(...)(same args) -
run_merged_pipeline(line 426): same replacement -
run_pipelineline 248:counter.update(tracked_sacks)→ filter first:
counter.update([d for d in tracked_sacks if d.class_name in ("sack", "box")])
run_merged_pipelineline 519:
countable = [d for d in tracked if d.track_id is not None and d.class_name in ("sack", "box")]
counter.update(countable)
src/dashboard.py_draw_detectionsline 155:label = "sack"→label = det.class_name
Interfaces:
- Consumes:
MultiClassLineCounterfrom src/counting.py (exists, hasbox_loading_count,box_unloading_countproperties) - Produces: counter objects are now
MultiClassLineCounter— same.loading_count/.unloading_countproperties 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:
-
PipelineResultdataclass: addbox_loading_count: int = 0,box_unloading_count: int = 0, propertybox_net_count -
MergedPipelineResultdataclass: same fields -
run_pipelinereturn (line ~323): addbox_loading_count=counter.box_loading_count, box_unloading_count=counter.box_unloading_count -
run_merged_pipelinereturn (line ~585): same -
JobResultdataclass: addbox_loading_count: int = 0,box_unloading_count: int = 0,box_net_count: int = 0 -
Both
JobResultconstruction sites in_run_job(lines ~220 and ~264): pass box counts from result -
templates/status.htmlresults card: add box stats blocks:
<div class="stat">
<span class="stat-value">{{ r.box_loading_count }}</span>
<span class="stat-label">Box In</span>
</div>
<div class="stat">
<span class="stat-value">{{ r.box_unloading_count }}</span>
<span class="stat-label">Box Out</span>
</div>
Keep existing Loading/Unloading as sack counts (rename labels to "Sack In"/"Sack Out" for clarity).
/api/jobs/<job_id>in app.py: include box counts in results JSON
Interfaces:
- Consumes: box counts from Task 1's MultiClassLineCounter
- Produces:
box_loading_countetc. 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:
-
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
<job_dir>/<name>_compressed.mp4 - If compressed file exists, reuse it
- Return compressed path or None
-
download()route: call helper, send compressed file if produced, else original -
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