Spaces:
Sleeping
Sleeping
File size: 15,080 Bytes
6c65498 d12d00d 6c65498 251faa9 46f8ebc 6c65498 251faa9 6c65498 d12d00d 6c65498 fbeda03 46f8ebc 251faa9 46f8ebc 251faa9 46f8ebc bb03a73 46f8ebc bb03a73 46f8ebc d12d00d 6c65498 d12d00d fbeda03 251faa9 fbeda03 f7a96ab 251faa9 eecfaf7 251faa9 fbeda03 137c6cf fbeda03 251faa9 46f8ebc 47d79b8 46f8ebc 5d257ae 46f8ebc fbeda03 5d257ae 46f8ebc 5d257ae f7a96ab 5d257ae f7a96ab 5d257ae 46f8ebc 5d257ae 46f8ebc 5d257ae 46f8ebc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | """
Pydantic models for play tracking.
These models represent detected plays, their temporal boundaries,
and the state machine configuration/state for play detection.
Architecture:
- PlayTracker (parent): Coordinates sub-trackers, holds all plays
- NormalPlayTracker: Handles standard plays (clock reset to 40, countdown)
- SpecialPlayTracker: Handles 40→25 transitions (timeouts, punts, FGs, XPs)
"""
from enum import Enum
from typing import Optional, List, Tuple
from pydantic import BaseModel, Field
# =============================================================================
# Utility Functions
# =============================================================================
def determine_timeout_team(home_change: int, away_change: int) -> Optional[str]:
"""
Determine which team called a timeout based on timeout count changes.
Validates that EXACTLY one team's count decreased by 1 while the other
team's count stayed the same.
Args:
home_change: Change in home team timeouts (before - after, positive = decrease)
away_change: Change in away team timeouts (before - after, positive = decrease)
Returns:
"home" or "away" if a valid timeout pattern detected, None otherwise
"""
if home_change == 1 and away_change == 0:
return "home"
if away_change == 1 and home_change == 0:
return "away"
return None
# =============================================================================
# Enums
# =============================================================================
class TrackerMode(Enum):
"""Which sub-tracker is currently active."""
NORMAL = "normal" # Normal play tracker is active
SPECIAL = "special" # Special play tracker is handling 40→25 transition
class PlayState(Enum):
"""Current state of play detection (used by NormalPlayTracker)."""
IDLE = "idle" # No scorebug detected, waiting
PRE_SNAP = "pre_snap" # Scorebug visible, clock ticking down before snap
PLAY_IN_PROGRESS = "play_in_progress" # Ball snapped, play is live
POST_PLAY = "post_play" # Play ended, waiting for next play setup
NO_SCOREBUG = "no_scorebug" # Scorebug lost during/after play (e.g., replay)
class SpecialPlayPhase(Enum):
"""
State machine phases for SpecialPlayTracker when handling 40→25 transitions.
Flow:
1. CHECKING_COUNTDOWN: Check if 25 immediately counts down (weird clock)
2. MONITORING_TIMEOUT: Wait 4-7s for timeout indicator change
3. WAITING_FOR_END: Track as special play until 7s or scorebug disappears
4. RESOLVED: Classification complete, ready to hand back control
"""
CHECKING_COUNTDOWN = "checking_countdown" # First 2s: checking if 25 counts down
MONITORING_TIMEOUT = "monitoring_timeout" # 4-7s: watching for timeout indicator change
WAITING_FOR_END = "waiting_for_end" # Tracking special play until end condition
RESOLVED = "resolved" # Classification complete
class PlayEvent(BaseModel):
"""Represents a detected play with start and end times."""
play_number: int = Field(..., description="Sequential play number")
start_time: float = Field(..., description="Video timestamp (seconds) when play started")
end_time: float = Field(..., description="Video timestamp (seconds) when play ended - from backward counting")
confidence: float = Field(..., description="Overall confidence score")
start_method: str = Field(..., description="How start was detected: 'clock_reset', 'clock_reset_25', 'clock_freeze'")
end_method: str = Field(..., description="How end was detected: 'backward_calc' (primary), 'direct_detect' (secondary)")
direct_end_time: Optional[float] = Field(None, description="End time from direct detection (for comparison)")
start_clock_value: Optional[int] = Field(None, description="Clock value at start detection")
end_clock_value: Optional[int] = Field(None, description="Clock value used for backward calculation")
play_type: str = Field("normal", description="Type of play: 'normal', 'special' (punt/fg/xp after 25-second reset)")
has_flag: bool = Field(False, description="Whether a penalty FLAG was detected during this play")
class TrackPlayStateConfig(BaseModel):
"""Configuration settings for play state tracking.
These values control the detection thresholds and timing parameters
used by the state machine to identify play boundaries.
"""
clock_stable_frames: int = Field(3, description="Frames with same clock value to consider it 'stable'")
max_play_duration: float = Field(15.0, description="Maximum expected play duration in seconds")
scorebug_lost_timeout: float = Field(30.0, description="Seconds before resetting state when scorebug lost")
required_countdown_ticks: int = Field(3, description="Number of consecutive descending ticks required to confirm play end")
min_clock_jump_for_reset: int = Field(5, description="Minimum jump in clock value to consider it a valid reset (40 from X where X <= 40 - this value)")
# FLAG detection settings (penalty flag indicator)
flag_extension_timeout: float = Field(15.0, description="Max seconds to extend play capture after FLAG first detected")
capture_flag_plays: bool = Field(True, description="Whether to capture plays with FLAGS (bypasses quiet time filter)")
# Opening kickoff detection settings
opening_kickoff_min_consecutive_frames: int = Field(3, description="Required consecutive frames with valid clock readings to confirm kickoff start")
max_opening_kickoff_duration: float = Field(90.0, description="Maximum valid kickoff duration (seconds). If exceeded, reject and keep searching.")
class TimeoutInfo(BaseModel):
"""Timeout information for a frame."""
home_timeouts: Optional[int] = Field(None, description="Number of home team timeouts remaining")
away_timeouts: Optional[int] = Field(None, description="Number of away team timeouts remaining")
confidence: float = Field(0.0, description="Confidence of the timeout reading")
class FlagInfo(BaseModel):
"""FLAG indicator information for a frame."""
detected: bool = Field(False, description="Whether FLAG is detected (yellow present AND valid hue)")
yellow_ratio: float = Field(0.0, description="Ratio of yellow pixels in FLAG region")
mean_hue: float = Field(0.0, description="Mean hue of yellow pixels (helps distinguish orange)")
scorebug_verified: bool = Field(True, description="Whether scorebug was verified present via template matching")
is_valid_yellow: bool = Field(False, description="True if mean_hue >= threshold (not orange)")
class ClockResetStats(BaseModel):
"""Statistics about clock reset classifications."""
total: int = Field(0, description="Total 40→25 resets detected")
weird_clock: int = Field(0, description="Class A: 25 counts down immediately (rejected)")
timeout: int = Field(0, description="Class B: Timeout indicator changed")
special: int = Field(0, description="Class C: Special play (injury/punt/FG/XP)")
# =============================================================================
# State models for restructured tracker architecture
# =============================================================================
class SpecialPlayHandoff(BaseModel):
"""
Data passed from NormalPlayTracker to SpecialPlayTracker when a 40→25 transition is detected.
Contains all context needed for the SpecialPlayTracker to classify and handle the transition.
"""
transition_timestamp: float = Field(..., description="When the 40→25 transition occurred")
home_timeouts_at_40: Optional[int] = Field(None, description="Home team timeouts before transition")
away_timeouts_at_40: Optional[int] = Field(None, description="Away team timeouts before transition")
timeout_confidence_at_40: float = Field(0.0, description="Confidence of timeout reading at 40")
was_in_play: bool = Field(False, description="Whether a play was in progress when transition detected")
play_start_time: Optional[float] = Field(None, description="Start time of play in progress (if any)")
time_at_40: float = Field(0.0, description="How long clock was at 40 before transitioning to 25")
from_freeze_detection: bool = Field(False, description="Whether this handoff came from clock freeze→25 detection (always treat as special play)")
class NormalTrackerState(BaseModel):
"""
State for NormalPlayTracker.
Tracks clock behavior and play boundaries for normal plays.
"""
# Current detection state
state: PlayState = Field(PlayState.IDLE, description="Current state of the normal play tracker")
# Clock tracking
last_clock_value: Optional[int] = Field(None, description="Last observed play clock value")
last_clock_timestamp: Optional[float] = Field(None, description="Timestamp of last clock reading")
clock_stable_count: int = Field(0, description="Number of consecutive frames with same clock value")
last_scorebug_timestamp: Optional[float] = Field(None, description="Timestamp of last scorebug detection")
# Current play tracking
current_play_start_time: Optional[float] = Field(None, description="Start time of the current play")
current_play_start_method: Optional[str] = Field(None, description="Method used to detect play start")
current_play_start_clock: Optional[int] = Field(None, description="Clock value when play started")
current_play_clock_base: int = Field(40, description="Clock base for current play (40 or 25)")
current_play_type: str = Field("normal", description="Type of current play: 'normal' or 'special'")
first_40_timestamp: Optional[float] = Field(None, description="When we first saw 40 in current play")
countdown_history: List[Tuple[float, int]] = Field(default_factory=list, description="(timestamp, clock_value) pairs")
direct_end_time: Optional[float] = Field(None, description="Direct end time observation")
# Timeout tracking
last_home_timeouts: Optional[int] = Field(None, description="Last observed home team timeouts")
last_away_timeouts: Optional[int] = Field(None, description="Last observed away team timeouts")
last_timeout_confidence: float = Field(0.0, description="Confidence of last timeout reading")
# FLAG tracking (penalty flag indicator)
flag_detected_at: Optional[float] = Field(None, description="When FLAG first appeared in current FLAG event")
flag_last_seen_at: Optional[float] = Field(None, description="Last time FLAG was visible")
flag_active: bool = Field(False, description="Whether FLAG is currently active (extends play capture)")
current_play_has_flag: bool = Field(False, description="Whether FLAG was detected during the current play")
# Clock freeze tracking (for special play detection)
clock_freeze_start_timestamp: Optional[float] = Field(None, description="When clock first reached current frozen value")
clock_freeze_value: Optional[int] = Field(None, description="The clock value that has been frozen/stable")
# Last play info (for continuation detection)
last_play_end_time: Optional[float] = Field(None, description="End time of the last completed play")
# Handoff signaling
request_special_handoff: bool = Field(False, description="Flag to request handoff to SpecialPlayTracker")
pending_handoff: Optional[SpecialPlayHandoff] = Field(None, description="Data for pending handoff to special tracker")
# Opening kickoff tracking
# The opening kickoff is special because the scorebug appears mid-play
# We detect it when scorebug first appears and end it on first clock reset to 40
# Requires k consecutive valid clock readings to confirm kickoff (filters out pre-game noise)
opening_kickoff_active: bool = Field(False, description="Whether we're currently tracking the opening kickoff")
opening_kickoff_complete: bool = Field(False, description="Whether opening kickoff has been recorded (only happens once)")
first_scorebug_timestamp: Optional[float] = Field(None, description="When scorebug first appeared in the video (verified by template matching)")
first_clock_reading_timestamp: Optional[float] = Field(None, description="When we first got a valid clock reading")
opening_kickoff_consecutive_readings: int = Field(0, description="Count of consecutive frames with valid clock readings for kickoff detection")
opening_kickoff_candidate_timestamp: Optional[float] = Field(None, description="Timestamp when consecutive clock readings started (candidate kickoff start)")
class SpecialTrackerState(BaseModel):
"""
State for SpecialPlayTracker.
Manages the 40→25 transition classification state machine.
"""
# Current phase of special play handling
phase: SpecialPlayPhase = Field(SpecialPlayPhase.RESOLVED, description="Current phase of special play handling")
# Transition context (from handoff)
transition_timestamp: Optional[float] = Field(None, description="When the 40→25 transition occurred")
home_timeouts_at_40: Optional[int] = Field(None, description="Home team timeouts before transition")
away_timeouts_at_40: Optional[int] = Field(None, description="Away team timeouts before transition")
timeout_confidence_at_40: float = Field(0.0, description="Confidence of timeout reading at 40")
was_in_play: bool = Field(False, description="Whether a play was in progress when transition detected")
play_start_time: Optional[float] = Field(None, description="Start time of play that led to this transition")
time_at_40: float = Field(0.0, description="How long clock was at 40 before transitioning")
from_freeze_detection: bool = Field(False, description="Whether this came from clock freeze→25 detection (always special play)")
# Classification tracking
lowest_clock_seen: int = Field(25, description="Lowest clock value seen since 25 (for countdown detection)")
last_scorebug_timestamp: Optional[float] = Field(None, description="Last time scorebug was visible")
# Scorebug disappearance tracking (debounce for noisy detection)
consecutive_scorebug_absent: int = Field(0, description="Number of consecutive frames without scorebug detection")
# Timeout confirmation tracking (require k consecutive confirmations)
pending_timeout_team: Optional[str] = Field(None, description="Team with pending timeout detection")
consecutive_timeout_confirmations: int = Field(0, description="Number of consecutive frames confirming timeout")
saw_timeout_increase: bool = Field(False, description="Whether we saw any timeout count INCREASE (invalidates timeout detection)")
# Resolution
resolution_complete: bool = Field(False, description="Whether classification is complete")
classification: Optional[str] = Field(None, description="Final classification: 'weird_clock', 'timeout', 'special'")
timeout_team: Optional[str] = Field(None, description="Which team called timeout: 'home' or 'away'")
|