from dataclasses import dataclass @dataclass(frozen=True) class HandoffDecision: stale_lock_active: bool allow_fast_handoff: bool required_switch_hits: int disable_prediction_hold: bool reasons: list[str] def reason_text(self) -> str: return "|".join(self.reasons) def evaluate_stale_lock( *, confirmed, have_fresh_candidate, target_has_fresh_update, candidate_dist_px, pred_diag_px, miss_streak, klt_valid, klt_quality, klt_iou, candidate_motion_ok, target_motion_ok, stale_break_dist_diag, stale_break_min_miss, stale_break_klt_quality, stale_break_klt_iou, ): reasons = [] if not confirmed or not have_fresh_candidate: return HandoffDecision(False, False, 0, False, []) far_enough = candidate_dist_px >= max(40.0, stale_break_dist_diag * max(pred_diag_px, 1.0)) weak_anchor = ( (not klt_valid) or (klt_quality < stale_break_klt_quality) or (klt_iou < stale_break_klt_iou) or (miss_streak >= stale_break_min_miss) or (candidate_motion_ok and not target_motion_ok) or (not target_has_fresh_update) ) if not target_has_fresh_update: reasons.append("target_not_fresh") if not klt_valid: reasons.append("klt_invalid") if klt_quality < stale_break_klt_quality: reasons.append("weak_klt_quality") if klt_iou < stale_break_klt_iou: reasons.append("weak_klt_iou") if miss_streak >= stale_break_min_miss: reasons.append("miss_streak") if candidate_motion_ok and not target_motion_ok: reasons.append("motion_conflict") stale = bool(far_enough and weak_anchor) return HandoffDecision( stale_lock_active=stale, allow_fast_handoff=stale, required_switch_hits=0, disable_prediction_hold=stale, reasons=reasons if stale else [], ) def compute_fast_handoff_hits( *, stale_lock_active, motion_switch_mode, approach_extra_hits, fast_maneuver_extra_hits, default_switch_hits, fast_handoff_hits, motion_switch_hits, ): if stale_lock_active: return int(fast_handoff_hits) hits = int(default_switch_hits) + int(approach_extra_hits) + int(fast_maneuver_extra_hits) if motion_switch_mode: hits = max(hits, int(motion_switch_hits)) return int(hits) def should_override_guidance( *, confirmed, target_track_missing, have_fresh_candidate, candidate_matches_target, miss_streak, override_miss_ge, ): return bool( confirmed and target_track_missing and have_fresh_candidate and (not candidate_matches_target) and (int(miss_streak) >= int(override_miss_ge)) ) def pick_guidance_override_box(*, track_candidate_box, raw_yolo_box): if track_candidate_box is not None: return track_candidate_box return raw_yolo_box def update_guidance_override_latch(*, new_box, prev_box, prev_ttl, max_ttl): if new_box is not None: return new_box, int(max(0, max_ttl)) if prev_box is not None and int(prev_ttl) > 0: return prev_box, int(prev_ttl) - 1 return None, 0