Files
feedmill-recounter/.opencode/plans/truck-count-bug-box-counts-compression.md

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:

  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:

counter.update([d for d in tracked_sacks if d.class_name in ("sack", "box")])
  1. run_merged_pipeline line 519:
countable = [d for d in tracked if d.track_id is not None and d.class_name in ("sack", "box")]
counter.update(countable)
  1. 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:

<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).

  1. /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_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 <job_dir>/<name>_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