From f01601d1d7776bf3095ca0aea09834242abcb79d Mon Sep 17 00:00:00 2001 From: dsutanto Date: Wed, 5 Aug 2026 00:49:39 +0700 Subject: [PATCH] Parse batch number and date from upload filename for live API lookup --- AGENTS.md | 6 ++++++ SPEC.md | 30 +++++++++++++++++++++++++-- recounting_dashboard_upload.py | 37 ++++++++++++++++++---------------- 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 76ee61f..1b2b5ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ - `batch_store.py` — shared SQLite persistence + batch state machine. - `counter_dashboard.py` — Flask dashboard on port 5000, same DB. - `recounting_dashboard.py` — Flask recounting dashboard on port 5002, consumes live + recount APIs. +- `recounting_dashboard_upload.py` — Flask upload+recount dashboard on port 5003; uploads MP4 first, then launches `RECOUNT_CMD --source `; START RECOUNT stays disabled until an upload completes. ## No build / test / lint @@ -30,6 +31,9 @@ PYTHONNOUSERSITE=1 venv/bin/python counter_live_rknn_bytetrack.py # Run dashboard PYTHONNOUSERSITE=1 venv/bin/python counter_dashboard.py + +# Run upload recounting dashboard +PYTHONNOUSERSITE=1 venv/bin/python recounting_dashboard_upload.py ``` ## Key environment & install quirks @@ -42,6 +46,8 @@ PYTHONNOUSERSITE=1 venv/bin/python counter_dashboard.py - Two systemd units: `bytetrack-counter.service` + `bytetrack-counter-dashboard.service`. - `counter_live.py` (TensorRT) is Jetson-only and won't work on RK3588. - **Reset flow**: `POST /api/reset` → deletes state JSON + touches `{SHM_DIR}/.reset`. `batch_store._check_reset_signal()` watches this marker and clears in-memory state on next crossing. +- **Upload filename contract**: uploads are expected as `batch_XX_YYYYMMDD_HHmmSS.mp4` (XX = batch number, timestamp = date). `recounting_dashboard_upload.py` parses both to query the live API's `/api/day-detail/` for the recorded count. +- **`RECOUNT_CMD`** in the upload dashboard is a template with a `{path}` placeholder, e.g. `bytetrack-counter config.env --source {path}`; the uploaded file path is `shlex.quote()`d before substitution. ## Code conventions diff --git a/SPEC.md b/SPEC.md index c39bb0b..8ea2bef 100644 --- a/SPEC.md +++ b/SPEC.md @@ -13,10 +13,10 @@ Daily cutoff @ HH:MM resets batch numbering. RK3588 hardware. - `numpy<2` (rknn-toolkit-lite2 incompatible with numpy≥2) - `PYTHONNOUSERSITE=1` ! set or venv breaks - SQLite for persistence, JSON file for active-batch state -- Flask on port 5000 (dashboard) + 5002 (recounting), systemd supervision +- Flask on port 5000 (dashboard) + 5002 (recounting) + 5003 (upload recounting), systemd supervision - `counter_live.py` ⊥ run on RK3588 — Jetson TensorRT artifact, ! port target - `counter_live_rknn_bytetrack.py` — reference for C++ port, this is what systemd runs -- Single deployment: `counter_live_rknn_bytetrack.py` + `counter_dashboard.py` + `recounting_dashboard.py` +- Single deployment: `counter_live_rknn_bytetrack.py` + `counter_dashboard.py` + `recounting_dashboard.py` + `recounting_dashboard_upload.py` - `go2rtc` ! running for recounting MP4 streaming (port 1984) ## §I — Interfaces @@ -63,6 +63,14 @@ env: RECOUNT_API_URL → base URL of recounting counter API (second node) env: GO2RTC_API_URL → go2rtc REST API (default http://localhost:1984) env: GO2RTC_STREAM_NAME → go2rtc stream name for recount preview (default "recount") ``` + +### Upload recounting dashboard config +``` +env: RECOUNTING_UPLOAD_PORT → port for upload UI (default 5003) +env: UPLOAD_DIR → dir for uploaded MP4s (default /uploads) +env: RECOUNT_CMD → replay command template with {path} placeholder (default "bytetrack-counter config.env --source {path}") +env: RECOUNTING_UPLOAD_TEMPLATE → template (default recounting_upload.html) +``` ``` ### SQLite @@ -107,6 +115,22 @@ api: GET /api/download-mp4?file= → download selected MP4 api: POST /api/start-recount {path} → reset recount counter + launch ffmpeg RTSP, return {stream_url} api: POST /api/stop-recount → kill ffmpeg + clear current file ``` + +### Upload recounting dashboard +``` +api: GET / → upload recounting HTML (template via RECOUNTING_UPLOAD_TEMPLATE env var) +api: POST /api/upload → multipart .mp4 (≤512MB), saved as original filename in UPLOAD_DIR; returns {name, path, size} +api: GET /api/uploads → list uploaded MP4s sorted by mtime desc → [ {name, path, size, mtime} ] +api: POST /api/delete-upload {path} → delete uploaded MP4 (path restricted to UPLOAD_DIR) +api: POST /api/start-recount {path} → reset recount API then launch RECOUNT_CMD (path shlex-quoted); START RECOUNT disabled until upload completes +api: POST /api/stop-recount → kill recount process + clear current file +api: GET /api/state → {streaming, file, file_name, stream_url, batch_number, batch_date} +api: GET /api/proxy-stream → proxy MJPEG from RECOUNT_API_URL:/api/live-video (same-origin) +api: GET /api/live-progress → proxy to LIVE_API_URL:/api/current-batch +api: GET /api/recount-progress → proxy to RECOUNT_API_URL:/api/current-batch +api: GET /api/batch-result?file= → parse batch num + date (YYYY-MM-DD) from filename batch_XX_YYYYMMDD_HHmmSS.mp4, query live API /api/day-detail/, fallback recent-batches +api: GET /api/download-mp4?file= → download selected MP4 +``` ``` ## §V — Invariants @@ -132,6 +156,7 @@ V17: tracks pruned after TRACKED_PRUNE_SEC (default 300s) without update V18: .env missing → scripts fail at import (os.getenv falls back to defaults, may mismatch) V19: stream reconnect → reset motion detection state (prev_gray = None) V20: reset → delete STATE_FILE + touch {SHM_DIR}/.reset; batch_store._check_reset_signal() watches marker & clears in-memory state before next crossing +V21: upload filename batch_XX_YYYYMMDD_HHmmSS.mp4 → batch# + date parsed; batch-result queries /api/day-detail/ on live API ``` ## §T — Tasks @@ -152,6 +177,7 @@ T11|x|reduce live-counter poll interval 2000→200ms for real-time feel| T12|x|add recounting dashboard — dual-API counter panels, MP4 browser, go2rtc streaming| T13|x|add template system — DASHBOARD_TEMPLATE env var selects dashboard.html|lamborghini|tesla| T14|x|harden reset API — separate SHM marker creation from state file cleanup; return 500 on SHM failure|V20 +T15|x|add upload recounting dashboard — MP4 upload, progress bar, RECOUNT_CMD replay|V21 ``` ## §B — Bugs diff --git a/recounting_dashboard_upload.py b/recounting_dashboard_upload.py index 155cdd7..e30fdc6 100644 --- a/recounting_dashboard_upload.py +++ b/recounting_dashboard_upload.py @@ -46,7 +46,7 @@ RECOUNT_CMD = os.getenv("RECOUNT_CMD", "bytetrack-counter config.env --source {p _http_session = requests.Session() _http_session.timeout = 3 -_FILENAME_RE = re.compile(r"batch[_-]?(\d+)", re.IGNORECASE) +_FILENAME_RE = re.compile(r"batch[_-]?(\d+)[_-](\d{8})[_-]\d{6}", re.IGNORECASE) _recount_proc = None _recount_lock = threading.Lock() @@ -56,7 +56,8 @@ _current_file = None def _parse_filename(filename): m = _FILENAME_RE.search(filename) if m: - return int(m.group(1)), None + raw = m.group(2) + return int(m.group(1)), f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}" return None, None @@ -254,9 +255,13 @@ def api_proxy_stream(): def api_batch_result(): fpath = request.args.get("file", "") filename = Path(fpath).name - batch_num, _ = _parse_filename(filename) + batch_num, date_str = _parse_filename(filename) if batch_num is None: return jsonify({"success": False, "error": "Cannot parse batch number from filename"}), 200 + if not date_str: + date_str = _folder_date(fpath, Path(UPLOAD_DIR)) + if not date_str: + return jsonify({"success": False, "error": "Cannot determine date from filename"}), 200 def _match_in_batches(batches): for b in batches: @@ -264,20 +269,18 @@ def api_batch_result(): return b return None - date_str = _folder_date(fpath, Path(UPLOAD_DIR)) - if date_str: - data = _api_get(LIVE_API_URL, f"/api/day-detail/{date_str}") - if data: - b = _match_in_batches(data.get("batches", [])) - if b: - return jsonify({ - "success": True, - "batch_number": batch_num, - "date": date_str, - "count": b["count"], - "start_time": b.get("start_time"), - "end_time": b.get("end_time"), - }) + data = _api_get(LIVE_API_URL, f"/api/day-detail/{date_str}") + if data: + b = _match_in_batches(data.get("batches", [])) + if b: + return jsonify({ + "success": True, + "batch_number": batch_num, + "date": date_str, + "count": b["count"], + "start_time": b.get("start_time"), + "end_time": b.get("end_time"), + }) recent = _api_get(LIVE_API_URL, "/api/recent-batches?limit=100") if recent: