From b20f4c582cf793b2048bd4d67c75113801ccb1c1 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Mon, 1 Jun 2026 14:41:29 +0200 Subject: [PATCH] Zaehllinie persistent speichern (ueberlebt Neustart) - Linie wird beim Setzen atomar in counting_line.json gespeichert - beim Start geladen -> Default fuer Session UND Webcam-Grabber - Pfad per LINE_FILE-Env ueberschreibbar, Datei via .gitignore ausgeschlossen - set_line-Route nutzt jetzt _valid_line (weniger Doppelcode) Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 +++ app.py | 58 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 5352bc8..9d34623 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # Hochgeladene Videos uploads/ +# Persistierte Zaehllinie (maschinenspezifisch) +counting_line.json + # Python __pycache__/ *.py[cod] diff --git a/app.py b/app.py index 1df587b..0e38522 100644 --- a/app.py +++ b/app.py @@ -136,6 +136,45 @@ UPLOAD_DIR = "uploads" DEFAULT_LINE = {"x1": 0, "y1": 300, "x2": 1020, "y2": 300} FRAME_SIZE = (1020, 600) +# Persistente Zaehllinie: wird als JSON gespeichert und beim Start geladen, +# damit sie einen Neustart ueberlebt (Pfad per Env ueberschreibbar). +LINE_FILE = os.environ.get("LINE_FILE", "counting_line.json") + + +def _valid_line(d): + """Validiert/normalisiert ein Linien-Dict zu int-Koordinaten oder None.""" + try: + return {k: int(d[k]) for k in ("x1", "y1", "x2", "y2")} + except (KeyError, ValueError, TypeError): + return None + + +def load_saved_line() -> dict: + """Laedt die gespeicherte Linie oder faellt auf DEFAULT_LINE zurueck.""" + try: + with open(LINE_FILE) as fh: + line = _valid_line(json.load(fh)) + if line: + return line + except (OSError, json.JSONDecodeError): + pass + return dict(DEFAULT_LINE) + + +def save_line(line: dict) -> None: + """Speichert die Linie atomar als JSON (ueberlebt App-Neustart).""" + try: + tmp = f"{LINE_FILE}.tmp" + with open(tmp, "w") as fh: + json.dump(line, fh) + os.replace(tmp, LINE_FILE) + except OSError as exc: + print(f"[line] save failed: {exc}", flush=True) + + +# Beim Start einmal laden -> Default fuer Session UND Webcam-Grabber. +SAVED_LINE = load_saved_line() + # --------------------------------------------------------------------------- # Hilfsfunktionen @@ -151,7 +190,7 @@ def ensure_upload_dir() -> None: def get_line_from_session(): if "counting_line" not in session: - session["counting_line"] = dict(DEFAULT_LINE) + session["counting_line"] = dict(SAVED_LINE) return session["counting_line"] @@ -314,7 +353,7 @@ class WebcamGrabber: self.frame_seq = 0 self.viewers = 0 self.reset_flag = Event() - self.line = dict(DEFAULT_LINE) + self.line = dict(SAVED_LINE) # Eigenes Modell -> isolierter Tracker, getrennt vom Video-Pfad. # Lazy: wird erst beim ersten aktiven Stream geladen. @@ -612,18 +651,15 @@ def webcam_feed(): def set_counting_line(): """Setzt die Zaehllinie (gilt fuer Video-Session UND Webcam-Grabber).""" data = request.get_json(silent=True) or {} - try: - line = { - "x1": int(data["x1"]), - "y1": int(data["y1"]), - "x2": int(data["x2"]), - "y2": int(data["y2"]), - } - except (KeyError, ValueError, TypeError): + line = _valid_line(data) + if line is None: abort(400, description="Ungueltige Linienkoordinaten") + global SAVED_LINE + SAVED_LINE = line # Default fuer kuenftige Sessions + save_line(line) # persistent -> ueberlebt Neustart session["counting_line"] = line - webcam.set_line(line) # Webcam nutzt eine globale Linie (eine Kamera) + webcam.set_line(line) # Webcam nutzt eine globale Linie (eine Kamera) return jsonify({"status": "success", "line": line})