diff --git a/src/atm/main.py b/src/atm/main.py index b2cb61d..10f52d2 100644 --- a/src/atm/main.py +++ b/src/atm/main.py @@ -377,6 +377,12 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None: start = time.monotonic() heartbeat_due = start + cfg.heartbeat_min * 60 levels_extractor = None + last_saved_color: str | None = None + samples_dir = Path("samples") + samples_dir.mkdir(exist_ok=True) + fires_dir = Path("logs") / "fires" + fires_dir.mkdir(parents=True, exist_ok=True) + import cv2 # type: ignore[import-untyped] try: while duration_s is None or (time.monotonic() - start) < duration_s: @@ -400,11 +406,31 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None: "ts": now, "event": "tick", "color": res.color, "state": tr.next.value, "reason": tr.reason, }) + # corpus: save full frame on each new distinct color for later labeling + if res.color != last_saved_color: + ts_str = datetime.fromtimestamp(now).strftime("%Y%m%d_%H%M%S") + sample_path = samples_dir / f"{ts_str}_{res.color}.png" + try: + cv2.imwrite(str(sample_path), frame) + except Exception as exc: + audit.log({"ts": now, "event": "sample_save_failed", "error": str(exc)}) + last_saved_color = res.color + # FIRE: annotate frame + save, attach to alert if tr.trigger and not tr.locked: + ts_str = datetime.fromtimestamp(now).strftime("%Y%m%d_%H%M%S") + fire_path = fires_dir / f"{ts_str}_{tr.trigger}.png" + try: + annotated = frame.copy() + x, y, w, h = cfg.dot_roi.x, cfg.dot_roi.y, cfg.dot_roi.w, cfg.dot_roi.h + cv2.rectangle(annotated, (x, y), (x + w, y + h), (0, 255, 255), 2) + cv2.imwrite(str(fire_path), annotated) + except Exception: + fire_path = None notifier.send(Alert( kind="trigger", title=f"{tr.trigger} signal", - body=f"@ {now}", + body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}", + image_path=fire_path, direction=tr.trigger, )) levels_extractor = LevelsExtractor(cfg, tr.trigger, now)