From f7266f598032f5d8f43e4a9afcf23deb8edb23f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=B0=BD=EC=9A=B1?= Date: Sat, 11 Jul 2026 22:17:48 +0900 Subject: [PATCH] fix(edge): identity-aware GHOST dedup keyed on track_id (FIX 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit edge/engine.py keyed the ghost cooldown as (GHOST, round(x), round(y)) — purely spatial, ignoring track identity. Two distinct workers collapsing at ~the same pixel within the cooldown window would suppress each other's ghost event, potentially masking a second CRITICAL incident. edge/detectors/ghost.py GhostIncidentDetector.update now includes the candidate's track_id in each emitted dict. edge/engine.py keys the cooldown as (EventType.GHOST, track_id) and sets the emitted Event's track_id accordingly, matching how FALL/NO_MOTION/IMMOBILITY/zone events are already keyed per-track. Tests: tests/test_ghost.py asserts ghosts[0]["track_id"] == 1 on the existing reappear-timeout test. --- edge/detectors/ghost.py | 2 +- edge/engine.py | 5 +++-- tests/test_ghost.py | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/edge/detectors/ghost.py b/edge/detectors/ghost.py index fb45b0e..1612227 100644 --- a/edge/detectors/ghost.py +++ b/edge/detectors/ghost.py @@ -42,7 +42,7 @@ class GhostIncidentDetector: out = [] for tid, (spatial, state, _height, lost_at) in list(self._candidates.items()): if now - lost_at >= self.cfg.reappear_s: - out.append({"spatial": spatial, "last_state": state, + out.append({"track_id": tid, "spatial": spatial, "last_state": state, "waited_s": round(now - lost_at, 1)}) self._candidates.pop(tid, None) return out diff --git a/edge/engine.py b/edge/engine.py index bea72a4..b469a70 100644 --- a/edge/engine.py +++ b/edge/engine.py @@ -67,9 +67,10 @@ class EventEngine: events.append(self._mk(etype, now, tr.track_id, zone_id=hit["zone_id"])) for g in self.ghost.update(active_tracks, now): - key = (EventType.GHOST, round(g["spatial"][0]), round(g["spatial"][1])) + key = (EventType.GHOST, g["track_id"]) if self._allow(key, now): - events.append(self._mk(EventType.GHOST, now, spatial=g["spatial"], + events.append(self._mk(EventType.GHOST, now, track_id=g["track_id"], + spatial=g["spatial"], metrics={"last_state": g["last_state"], "waited_s": g["waited_s"]})) return events diff --git a/tests/test_ghost.py b/tests/test_ghost.py index 550a665..07eb39f 100644 --- a/tests/test_ghost.py +++ b/tests/test_ghost.py @@ -16,6 +16,7 @@ def test_lost_dangerous_track_becomes_ghost_after_wait(): assert len(ghosts) == 1 assert ghosts[0]["last_state"] == "FALLEN" assert ghosts[0]["spatial"] == (110.0, 130.0) # bbox 중심 + assert ghosts[0]["track_id"] == 1 def test_non_dangerous_lost_track_is_ignored(): d = GhostIncidentDetector(GhostConfig(reappear_s=1.0))