"""Binary SAFE / UNSAFE classification — every item in REQUIRED_PPE must be worn. The required list comes from ``config/core_config.json::compliance.required_ppe`` so it can be retuned without a code change. """ from __future__ import annotations from typing import Dict, List, Tuple from core.detection import POSITIVE_TO_NEGATIVE, PPE_DISPLAY_ORDER from utils.config import load_config # PPE items required for SAFE status. Other items (gloves/goggles/boots) are # still detected and logged but don't affect compliance. REQUIRED_PPE: Tuple[str, ...] = tuple( load_config("core").get("compliance", {}).get("required_ppe", ["helmet", "vest"]) ) def _is_wearing(items: Dict[str, float], ppe: str) -> bool: pos = items.get(ppe, 0.0) neg = items.get(POSITIVE_TO_NEGATIVE[ppe], 0.0) return pos > neg and pos > 0 def status_from_items(items: Dict[str, float]) -> str: """SAFE only when every required item is confidently worn.""" if all(_is_wearing(items, p) for p in REQUIRED_PPE): return "SAFE" return "UNSAFE" def split_wearing_missing(items: Dict[str, float]) -> Tuple[List[str], List[str], List[str]]: """Bucket each PPE item into wearing / missing / unknown. ``missing`` only contains REQUIRED items that aren't confidently worn — this is what the TTS announcement keys off of, so it maps cleanly to a recorded clip. Non-required items (gloves/goggles/boots) are never in ``missing`` regardless of whether their ``no-X`` class was detected; they go to ``unknown`` so they're still visible in the event log. """ wearing, missing, unknown = [], [], [] for pos in PPE_DISPLAY_ORDER: neg = POSITIVE_TO_NEGATIVE[pos] pos_conf = items.get(pos, 0.0) neg_conf = items.get(neg, 0.0) worn = pos_conf > neg_conf and pos_conf > 0 if worn: wearing.append(pos) elif pos in REQUIRED_PPE: missing.append(pos) else: unknown.append(pos) return wearing, missing, unknown