forked from zakaria/chicken-counting-sukawarna-det
- batch_runner: _store_to_db after each camera (not just at end) - dashboard: Flask app with templates/index.html - Single-camera full-screen live stream with auto-switch - DB history panel in sidebar - store_results.py: standalone script for existing reports
132 lines
4.2 KiB
Python
132 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Store batch run results into a SQLite database.
|
|
|
|
Reads the aggregate JSON report written by the batch runner and inserts
|
|
all camera-level + summary data. Safe to run multiple times — uses
|
|
(date, location, camera_id) as the unique key, so re-runs update
|
|
existing rows instead of duplicating.
|
|
|
|
Usage:
|
|
python3 store_results.py /path/to/output/counts_2026-06-10.json --location kandang-atas
|
|
python3 store_results.py /path/to/output/counts_2026-06-10.json --db /var/lib/chickens.db
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
CREATE_TABLE = """
|
|
CREATE TABLE IF NOT EXISTS batch_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
date TEXT NOT NULL,
|
|
location TEXT NOT NULL,
|
|
camera_id TEXT NOT NULL,
|
|
total_entered INTEGER NOT NULL DEFAULT 0,
|
|
frames_processed INTEGER NOT NULL DEFAULT 0,
|
|
elapsed_seconds REAL NOT NULL DEFAULT 0.0,
|
|
stopped_reason TEXT NOT NULL DEFAULT '',
|
|
source_video TEXT NOT NULL DEFAULT '',
|
|
generated_at TEXT NOT NULL DEFAULT '',
|
|
UNIQUE(date, location, camera_id)
|
|
)
|
|
"""
|
|
|
|
INSERT_SQL = """
|
|
INSERT INTO batch_runs
|
|
(date, location, camera_id, total_entered, frames_processed,
|
|
elapsed_seconds, stopped_reason, source_video, generated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(date, location, camera_id) DO UPDATE SET
|
|
total_entered = excluded.total_entered,
|
|
frames_processed = excluded.frames_processed,
|
|
elapsed_seconds = excluded.elapsed_seconds,
|
|
stopped_reason = excluded.stopped_reason,
|
|
source_video = excluded.source_video,
|
|
generated_at = excluded.generated_at
|
|
"""
|
|
|
|
SUMMARY_QUERY = """
|
|
SELECT
|
|
date,
|
|
location,
|
|
COUNT(*) AS camera_count,
|
|
SUM(total_entered) AS total_chickens,
|
|
SUM(elapsed_seconds) AS total_seconds,
|
|
ROUND(SUM(elapsed_seconds) / 60.0, 1) AS total_minutes
|
|
FROM batch_runs
|
|
WHERE date = ? AND location = ?
|
|
GROUP BY date, location
|
|
"""
|
|
|
|
|
|
def store_report(report_path: str, location: str, db_path: str) -> None:
|
|
with open(report_path) as f:
|
|
report = json.load(f)
|
|
|
|
date = report["date"]
|
|
cameras = report["cameras"]
|
|
generated_at = report.get("generated_at", "")
|
|
|
|
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(db_path)
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute(CREATE_TABLE)
|
|
|
|
rows = 0
|
|
for camera_id, entry in cameras.items():
|
|
if entry.get("skipped"):
|
|
continue
|
|
conn.execute(INSERT_SQL, (
|
|
date, location, camera_id,
|
|
entry.get("total_entered", 0),
|
|
entry.get("frames_processed", 0),
|
|
entry.get("elapsed_seconds", 0),
|
|
entry.get("stopped_reason", ""),
|
|
entry.get("source_video", ""),
|
|
generated_at,
|
|
))
|
|
rows += 1
|
|
|
|
conn.commit()
|
|
|
|
# print summary
|
|
row = conn.execute(SUMMARY_QUERY, (date, location)).fetchone()
|
|
if row:
|
|
print(f"\n[db] {row[0]} | {row[1]} | {row[2]} cameras | "
|
|
f"{row[3]} chickens | {row[4]:.0f}s ({row[5]} min)")
|
|
|
|
# also print per-camera breakdown
|
|
cur = conn.execute(
|
|
"SELECT camera_id, total_entered, elapsed_seconds "
|
|
"FROM batch_runs WHERE date=? AND location=? ORDER BY camera_id",
|
|
(date, location))
|
|
for cam_id, count, secs in cur:
|
|
print(f" {cam_id}: {count} chickens, {secs:.0f}s")
|
|
|
|
conn.close()
|
|
print(f"\n[db] wrote {rows} rows to {db_path}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Store batch run results into SQLite")
|
|
parser.add_argument("report", help="Path to counts_YYYY-MM-DD.json")
|
|
parser.add_argument("--location", required=True, help="Location name (e.g. kandang-atas)")
|
|
parser.add_argument("--db", default="chicken_counts.db", help="SQLite database path")
|
|
args = parser.parse_args()
|
|
|
|
if not Path(args.report).exists():
|
|
print(f"error: report not found: {args.report}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
store_report(args.report, args.location, args.db)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|