feat(v1): compose temporal fall event pipeline

This commit is contained in:
ila
2026-07-21 11:57:22 +08:00
parent 906e06565e
commit f03662bf5a
16 changed files with 554 additions and 34 deletions
+22 -2
View File
@@ -23,6 +23,7 @@ class Evidence:
class FallEvent:
event_id: str
track_id: str
config_version: str
suspected_at_monotonic: float
confirmed_at_monotonic: float
latency_seconds: float
@@ -33,6 +34,7 @@ class FallEvent:
class _Record:
state: FallState
suspect_started_at: Optional[float] = None
confirmed_at: Optional[float] = None
recovery_started_at: Optional[float] = None
last_updated_at: Optional[float] = None
@@ -40,13 +42,25 @@ class _Record:
class FallStateMachine:
"""Confirm only uninterrupted, accepted fall evidence for one tracked person."""
def __init__(self, confirm_window_seconds: float, recovery_window_seconds: float) -> None:
def __init__(
self,
confirm_window_seconds: float,
recovery_window_seconds: float,
config_version: str,
cooldown_seconds: float = 0.0,
) -> None:
if not 1.0 <= confirm_window_seconds <= 3.0:
raise ValueError("confirm_window_seconds must be between 1 and 3 seconds")
if recovery_window_seconds <= 0:
raise ValueError("recovery_window_seconds must be positive")
if cooldown_seconds < 0:
raise ValueError("cooldown_seconds must be non-negative")
if not isinstance(config_version, str) or not config_version.strip():
raise ValueError("config_version must be a non-empty string")
self._confirm_window_seconds = float(confirm_window_seconds)
self._recovery_window_seconds = float(recovery_window_seconds)
self._cooldown_seconds = float(cooldown_seconds)
self._config_version = config_version.strip()
self._records: Dict[str, _Record] = {}
self._next_event_number = 1
@@ -79,13 +93,17 @@ class FallStateMachine:
return []
if timestamp - record.suspect_started_at >= self._confirm_window_seconds:
record.state = FallState.CONFIRMED
record.confirmed_at = timestamp
event = self._new_event(track_id, record.suspect_started_at, timestamp)
record.suspect_started_at = None
return [event]
return []
if record.state is FallState.CONFIRMED:
if evidence.is_recovery_candidate:
if (
evidence.is_recovery_candidate
and timestamp - record.confirmed_at >= self._cooldown_seconds
):
record.state = FallState.RECOVERING
record.recovery_started_at = timestamp
return []
@@ -117,6 +135,7 @@ class FallStateMachine:
def _set_normal(record: _Record) -> None:
record.state = FallState.NORMAL
record.suspect_started_at = None
record.confirmed_at = None
record.recovery_started_at = None
def _new_event(
@@ -125,6 +144,7 @@ class FallStateMachine:
event = FallEvent(
event_id="FALL-{0:06d}".format(self._next_event_number),
track_id=track_id,
config_version=self._config_version,
suspected_at_monotonic=suspected_at,
confirmed_at_monotonic=confirmed_at,
latency_seconds=confirmed_at - suspected_at,