"""Pure eligibility rules for Phase 5 voice prompting.""" from __future__ import annotations from .config_loader import VoicePromptSettings from .models import TrackEvent, TrackedPerson, TurnTrackState, VoicePromptTrackState def trigger_matches( config: VoicePromptSettings, person: TrackedPerson, events: list[TrackEvent], ) -> bool: """Return whether the configured spatial/track trigger occurred.""" own_events = {event.event_type for event in events if event.track_id == person.track_id} if config.trigger_strategy == "trigger_line_crossing": return "trigger_crossed" in own_events if config.trigger_strategy == "region_entry": return "region_entered" in own_events if config.trigger_strategy == "first_confirmed_track": return person.is_confirmed value = person.foot_x if config.fixed_position_axis == "x" else person.foot_y return value >= config.fixed_position_value if config.fixed_position_direction == "increasing" else value <= config.fixed_position_value def eligibility_reason( config: VoicePromptSettings, person: TrackedPerson, state: VoicePromptTrackState, turn_state: TurnTrackState | None, cooldown_remaining: float, nominal_fps: float = 30.0, ) -> str: if config.one_prompt_per_track and (state.prompt_attempted or state.pseudo_prompt): return "already_prompted" if config.require_confirmed_track and not person.is_confirmed: return "unconfirmed_track" if config.require_inside_region and not person.inside_evaluation_region: return "outside_region" if person.missed_frames > 0: return "stale_track" if person.track_age_frames <= 0 or person.track_age_frames / max(nominal_fps, 1.0) < config.minimum_track_age_sec: return "track_too_young" if config.skip_if_already_turned and turn_state is not None and turn_state.turn_confirmed: return "already_turned" if cooldown_remaining > 0: return "global_cooldown" return "eligible"