feat(v1): relax fall sensitivity for low overhead cameras

Adds configurable event.require_rapid_drop (default off), require_lower_body
(default off) and horizontal_angle_threshold_degrees (default 45). With the
lenient defaults a sustained horizontal pose alone enters SUSPECT and confirms
after the confirm window, which is now the main false-positive guard; missing
knees/ankles no longer reject the pose. Thresholds flow into config_version.
End-to-end smoke confirms a lying pose; 70 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-21 23:29:16 +08:00
co-authored by Claude Opus 4.8
parent 5f29a8e60f
commit b1f0e8aeb4
12 changed files with 104 additions and 12 deletions
+20
View File
@@ -69,6 +69,9 @@ class EventConfig:
confirm_window_seconds: float
recovery_window_seconds: float
cooldown_seconds: float
require_rapid_drop: bool = False
require_lower_body: bool = False
horizontal_angle_threshold_degrees: float = 45.0
@dataclass(frozen=True)
@@ -105,6 +108,9 @@ class AppConfig:
"confirm_window_seconds": self.event.confirm_window_seconds,
"recovery_window_seconds": self.event.recovery_window_seconds,
"cooldown_seconds": self.event.cooldown_seconds,
"require_rapid_drop": self.event.require_rapid_drop,
"require_lower_body": self.event.require_lower_body,
"horizontal_angle_threshold_degrees": self.event.horizontal_angle_threshold_degrees,
},
}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
@@ -139,6 +145,12 @@ def _number(value: Any, field_name: str, minimum: float, maximum: float) -> floa
return result
def _bool(value: Any, field_name: str) -> bool:
if not isinstance(value, bool):
raise ConfigError("{0} must be a boolean".format(field_name))
return value
def _resolve_path(config_path: Path, value: Any, field_name: str) -> Path:
raw_path = Path(_text(value, field_name))
if raw_path.is_absolute():
@@ -274,6 +286,14 @@ def load_config(path: Path) -> AppConfig:
0.0,
3600.0,
),
require_rapid_drop=_bool(event_raw.get("require_rapid_drop", False), "event.require_rapid_drop"),
require_lower_body=_bool(event_raw.get("require_lower_body", False), "event.require_lower_body"),
horizontal_angle_threshold_degrees=_number(
event_raw.get("horizontal_angle_threshold_degrees", 45.0),
"event.horizontal_angle_threshold_degrees",
0.0,
90.0,
),
)
artifacts = _mapping(root.get("artifacts"), "artifacts")