andytaylor-smg commited on
Commit
47d79b8
·
1 Parent(s): 4a4dfa0

removing dead code

Browse files
src/detection/__init__.py CHANGED
@@ -10,7 +10,7 @@ from .models import (
10
  TimeoutRegionConfig,
11
  TimeoutReading,
12
  )
13
- from .scorebug import DetectScoreBug, create_template_from_frame
14
  from .timeouts import DetectTimeouts
15
 
16
  __all__ = [
@@ -20,7 +20,6 @@ __all__ = [
20
  "TimeoutReading",
21
  # Scorebug detection
22
  "DetectScoreBug",
23
- "create_template_from_frame",
24
  # Timeout tracking
25
  "DetectTimeouts",
26
  ]
 
10
  TimeoutRegionConfig,
11
  TimeoutReading,
12
  )
13
+ from .scorebug import DetectScoreBug
14
  from .timeouts import DetectTimeouts
15
 
16
  __all__ = [
 
20
  "TimeoutReading",
21
  # Scorebug detection
22
  "DetectScoreBug",
 
23
  # Timeout tracking
24
  "DetectTimeouts",
25
  ]
src/detection/scorebug.py CHANGED
@@ -405,18 +405,3 @@ class DetectScoreBug:
405
  cv2.putText(vis_frame, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
406
 
407
  return vis_frame
408
-
409
-
410
- def create_template_from_frame(frame: np.ndarray[Any, Any], bbox: Tuple[int, int, int, int], output_path: str) -> None:
411
- """
412
- Extract a region from a frame to use as a template.
413
-
414
- Args:
415
- frame: Source frame
416
- bbox: Bounding box (x, y, width, height)
417
- output_path: Path to save the template image
418
- """
419
- x, y, w, h = bbox
420
- template = frame[y : y + h, x : x + w]
421
- cv2.imwrite(output_path, template)
422
- logger.info("Created template: %s (size: %dx%d)", output_path, w, h)
 
405
  cv2.putText(vis_frame, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
406
 
407
  return vis_frame
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/detection/timeout_calibrator.py CHANGED
@@ -228,52 +228,6 @@ def _validate_oval_pattern(ovals: List[OvalLocation]) -> bool:
228
  return True
229
 
230
 
231
- def check_oval_brightness(
232
- frame: np.ndarray[Any, Any],
233
- region_bbox: Tuple[int, int, int, int],
234
- oval: OvalLocation,
235
- brightness_threshold_ratio: float = 0.5,
236
- ) -> Tuple[bool, float]:
237
- """
238
- Check if a specific oval is still bright (timeout available).
239
-
240
- Args:
241
- frame: Full video frame (BGR format)
242
- region_bbox: Bounding box of the timeout region (x, y, width, height)
243
- oval: OvalLocation to check
244
- brightness_threshold_ratio: Ratio of baseline brightness below which oval is considered "dark"
245
-
246
- Returns:
247
- Tuple of (is_bright, current_brightness)
248
- - is_bright: True if oval is still bright (timeout available), False if dark (used)
249
- - current_brightness: Current mean brightness value
250
- """
251
- rx, ry, _, _ = region_bbox
252
-
253
- # Calculate absolute position in frame
254
- abs_x = rx + oval.x
255
- abs_y = ry + oval.y
256
-
257
- # Validate bounds
258
- frame_h, frame_w = frame.shape[:2]
259
- if abs_x < 0 or abs_y < 0 or abs_x + oval.width > frame_w or abs_y + oval.height > frame_h:
260
- logger.warning("Oval position out of bounds")
261
- return False, 0.0
262
-
263
- # Extract the oval region
264
- oval_roi = frame[abs_y : abs_y + oval.height, abs_x : abs_x + oval.width]
265
-
266
- # Convert to grayscale and calculate mean brightness
267
- gray = cv2.cvtColor(oval_roi, cv2.COLOR_BGR2GRAY)
268
- current_brightness = float(np.mean(np.asarray(gray)))
269
-
270
- # Compare to baseline
271
- threshold = oval.baseline_brightness * brightness_threshold_ratio
272
- is_bright = current_brightness >= threshold
273
-
274
- return is_bright, current_brightness
275
-
276
-
277
  def visualize_calibration(
278
  frame: np.ndarray[Any, Any],
279
  calibrated_region: CalibratedTimeoutRegion,
 
228
  return True
229
 
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  def visualize_calibration(
232
  frame: np.ndarray[Any, Any],
233
  calibrated_region: CalibratedTimeoutRegion,
src/readers/__init__.py CHANGED
@@ -17,10 +17,7 @@ from .models import (
17
  TemplatePlayClockReading,
18
  )
19
  from .flags import FlagReader
20
- from .playclock import (
21
- ReadPlayClock,
22
- backfill_missing_readings,
23
- )
24
 
25
 
26
  class FlagConfig(Protocol): # pylint: disable=too-few-public-methods
@@ -66,5 +63,4 @@ __all__ = [
66
  "create_flag_reader",
67
  # Play clock reading
68
  "ReadPlayClock",
69
- "backfill_missing_readings",
70
  ]
 
17
  TemplatePlayClockReading,
18
  )
19
  from .flags import FlagReader
20
+ from .playclock import ReadPlayClock
 
 
 
21
 
22
 
23
  class FlagConfig(Protocol): # pylint: disable=too-few-public-methods
 
63
  "create_flag_reader",
64
  # Play clock reading
65
  "ReadPlayClock",
 
66
  ]
src/readers/playclock.py CHANGED
@@ -381,163 +381,3 @@ class ReadPlayClock:
381
  region = frame[y : y + h, x : x + w].copy()
382
 
383
  return self.read(region)
384
-
385
-
386
- # =============================================================================
387
- # Gap Interpolation
388
- # =============================================================================
389
-
390
-
391
- def _find_gap_extent(readings: List[PlayClockReading], start_idx: int) -> int:
392
- """Find the end index of a gap starting at start_idx."""
393
- gap_end = start_idx
394
- while gap_end < len(readings) and (not readings[gap_end].detected or readings[gap_end].value is None):
395
- gap_end += 1
396
- return gap_end
397
-
398
-
399
- def _validate_gap_boundaries(readings: List[PlayClockReading], gap_start: int, gap_end: int, max_gap_seconds: int) -> Tuple[bool, int, int, int]:
400
- """
401
- Validate that a gap can be backfilled.
402
-
403
- Returns:
404
- Tuple of (is_valid, left_value, right_value, seconds_gap).
405
- is_valid is False if gap cannot be backfilled.
406
- """
407
- # Check if we have valid readings on both sides
408
- if gap_start == 0:
409
- logger.debug("Gap at start of sequence (index 0), no left side for backfill")
410
- return False, 0, 0, 0
411
-
412
- if gap_end >= len(readings):
413
- logger.debug("Gap at end of sequence (index %d), no right side for backfill", gap_start)
414
- return False, 0, 0, 0
415
-
416
- # Get the clock values on either side
417
- left_value = readings[gap_start - 1].value
418
- right_value = readings[gap_end].value
419
-
420
- if left_value is None or right_value is None:
421
- logger.debug("Adjacent values are None, cannot backfill")
422
- return False, 0, 0, 0
423
-
424
- # Left should be higher than right in a countdown
425
- if left_value <= right_value:
426
- logger.debug("Invalid countdown: left=%d not greater than right=%d", left_value, right_value)
427
- return False, 0, 0, 0
428
-
429
- seconds_gap = left_value - right_value - 1
430
-
431
- # Check if gap in seconds is within our limit
432
- if seconds_gap > max_gap_seconds:
433
- logger.debug(
434
- "Gap of %d seconds (left=%d, right=%d) exceeds max_gap_seconds=%d, skipping backfill",
435
- seconds_gap,
436
- left_value,
437
- right_value,
438
- max_gap_seconds,
439
- )
440
- return False, 0, 0, 0
441
-
442
- return True, left_value, right_value, seconds_gap
443
-
444
-
445
- def _backfill_interpolate(result: List[PlayClockReading], gap_start: int, gap_frame_count: int, left_value: int, right_value: int) -> None:
446
- """Backfill a gap using nearest value interpolation (no missing clock values)."""
447
- logger.debug("No missing clock values between %d and %d, using nearest value interpolation", left_value, right_value)
448
- for j in range(gap_frame_count):
449
- # Use left value for first half, right value for second half
450
- backfill_value = left_value if j < gap_frame_count / 2 else right_value
451
- result[gap_start + j] = PlayClockReading(
452
- detected=True,
453
- value=backfill_value,
454
- confidence=0.0,
455
- raw_text=f"BACKFILLED_{backfill_value}",
456
- )
457
-
458
-
459
- def _backfill_with_missing_values(result: List[PlayClockReading], gap_start: int, gap_end: int, left_value: int, right_value: int) -> None:
460
- """Backfill a gap by distributing missing clock values across frames."""
461
- gap_frame_count = gap_end - gap_start
462
- missing_values = list(range(left_value - 1, right_value, -1))
463
-
464
- logger.info(
465
- "Backfilling gap: frames %d-%d, clock values %d to %d, missing values: %s",
466
- gap_start,
467
- gap_end - 1,
468
- left_value,
469
- right_value,
470
- missing_values,
471
- )
472
-
473
- for j in range(gap_frame_count):
474
- # Calculate which missing value this frame should have
475
- position = j / gap_frame_count
476
- value_index = min(int(position * len(missing_values)), len(missing_values) - 1)
477
- backfill_value = missing_values[value_index]
478
-
479
- result[gap_start + j] = PlayClockReading(
480
- detected=True,
481
- value=backfill_value,
482
- confidence=0.0,
483
- raw_text=f"BACKFILLED_{backfill_value}",
484
- )
485
- logger.debug(" Backfilled index %d with value %d", gap_start + j, backfill_value)
486
-
487
-
488
- def backfill_missing_readings(readings: List[PlayClockReading], max_gap_seconds: int = 3) -> List[PlayClockReading]:
489
- """
490
- Backfill missing play clock readings when there's a clear countdown sequence with gaps.
491
-
492
- This function fills in missing readings when:
493
- 1. The gap in clock VALUES is 1-3 seconds (configurable via max_gap_seconds)
494
- 2. There's at least one valid reading on EACH side of the gap
495
- 3. The readings form a valid countdown sequence
496
-
497
- Since the video may be sampled at higher than 1fps, each clock value may appear
498
- multiple times. This function looks at the actual clock values, not frame indices.
499
-
500
- Examples (showing clock values, with ? for undetected):
501
- - [6, 6, 5, 5, ?, ?, 3, 3] → missing value is 4, backfill the gaps
502
- - [10, 9, ?, ?, ?] → no backfill (no right side continuation)
503
- - [6, 6, ?, ?, ?, ?, ?, 0, 0] → no backfill if gap > max_gap_seconds
504
-
505
- Args:
506
- readings: List of PlayClockReading objects (in chronological order)
507
- max_gap_seconds: Maximum number of missing SECONDS to backfill (default: 3)
508
-
509
- Returns:
510
- New list with backfilled readings (original list is not modified)
511
- """
512
- if not readings:
513
- return readings
514
-
515
- result = list(readings)
516
- i = 0
517
-
518
- while i < len(result):
519
- # Skip if this reading is valid
520
- if result[i].detected and result[i].value is not None:
521
- i += 1
522
- continue
523
-
524
- # Found a missing reading - find the extent of the gap
525
- gap_start = i
526
- gap_end = _find_gap_extent(result, i)
527
- gap_frame_count = gap_end - gap_start
528
-
529
- # Validate that we can backfill this gap
530
- is_valid, left_value, right_value, seconds_gap = _validate_gap_boundaries(result, gap_start, gap_end, max_gap_seconds)
531
- if not is_valid:
532
- i = gap_end
533
- continue
534
-
535
- # Perform the backfill
536
- if seconds_gap <= 0:
537
- _backfill_interpolate(result, gap_start, gap_frame_count, left_value, right_value)
538
- else:
539
- _backfill_with_missing_values(result, gap_start, gap_end, left_value, right_value)
540
-
541
- i = gap_end
542
-
543
- return result
 
381
  region = frame[y : y + h, x : x + w].copy()
382
 
383
  return self.read(region)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/tracking/__init__.py CHANGED
@@ -29,7 +29,6 @@ from .models import (
29
  NormalTrackerState,
30
  PlayEvent,
31
  PlayState,
32
- PlayTrackingState,
33
  SpecialPlayHandoff,
34
  SpecialPlayPhase,
35
  SpecialTrackerState,
@@ -52,7 +51,6 @@ __all__ = [
52
  "NormalTrackerState",
53
  "PlayEvent",
54
  "PlayState",
55
- "PlayTrackingState",
56
  "SpecialPlayHandoff",
57
  "SpecialPlayPhase",
58
  "SpecialTrackerState",
 
29
  NormalTrackerState,
30
  PlayEvent,
31
  PlayState,
 
32
  SpecialPlayHandoff,
33
  SpecialPlayPhase,
34
  SpecialTrackerState,
 
51
  "NormalTrackerState",
52
  "PlayEvent",
53
  "PlayState",
 
54
  "SpecialPlayHandoff",
55
  "SpecialPlayPhase",
56
  "SpecialTrackerState",
src/tracking/models.py CHANGED
@@ -15,7 +15,6 @@ from typing import Optional, List, Tuple
15
 
16
  from pydantic import BaseModel, Field
17
 
18
-
19
  # =============================================================================
20
  # Utility Functions
21
  # =============================================================================
@@ -141,61 +140,8 @@ class ClockResetStats(BaseModel):
141
  special: int = Field(0, description="Class C: Special play (injury/punt/FG/XP)")
142
 
143
 
144
- class PlayTrackingState(BaseModel):
145
- """Internal mutable state for play tracking.
146
-
147
- Tracks the current detection state, detected plays, and all intermediate
148
- tracking variables used during play boundary detection.
149
- """
150
-
151
- # Current detection state
152
- state: PlayState = Field(PlayState.IDLE, description="Current state of the play detection state machine")
153
- plays: List[PlayEvent] = Field(default_factory=list, description="List of all detected plays")
154
-
155
- # Tracking counters and values
156
- play_count: int = Field(0, description="Total number of plays detected so far")
157
- last_clock_value: Optional[int] = Field(None, description="Last observed play clock value")
158
- last_clock_timestamp: Optional[float] = Field(None, description="Timestamp of last clock reading")
159
- clock_stable_count: int = Field(0, description="Number of consecutive frames with same clock value")
160
-
161
- # Current play tracking
162
- current_play_start_time: Optional[float] = Field(None, description="Start time of the current play being tracked")
163
- current_play_start_method: Optional[str] = Field(None, description="Method used to detect current play start")
164
- current_play_start_clock: Optional[int] = Field(None, description="Clock value when current play started")
165
- last_scorebug_timestamp: Optional[float] = Field(None, description="Timestamp of last scorebug detection")
166
- direct_end_time: Optional[float] = Field(None, description="Direct end time observation (for comparison)")
167
- countdown_history: List[Tuple[float, int]] = Field(default_factory=list, description="List of (timestamp, clock_value) for countdown tracking")
168
- first_40_timestamp: Optional[float] = Field(None, description="When we first saw 40 in current play (for turnover detection)")
169
- current_play_clock_base: int = Field(40, description="Clock base for current play (40 for normal, 25 for special teams)")
170
- current_play_type: str = Field("normal", description="Type of current play being tracked: 'normal' or 'special'")
171
-
172
- # Timeout tracking for clock reset classification
173
- last_home_timeouts: Optional[int] = Field(None, description="Last observed home team timeouts")
174
- last_away_timeouts: Optional[int] = Field(None, description="Last observed away team timeouts")
175
- last_timeout_confidence: float = Field(0.0, description="Confidence of last timeout reading")
176
- clock_reset_stats: ClockResetStats = Field(default_factory=ClockResetStats, description="Statistics about clock reset classifications")
177
-
178
- # Pending timeout resolution (for delayed timeout detection)
179
- # When 40→25 detected, we store the "before" state and check again after delay
180
- pending_timeout_check_time: Optional[float] = Field(None, description="Timestamp when we should resolve pending timeout check")
181
- pending_timeout_transition_time: Optional[float] = Field(None, description="Timestamp when the 40→25 transition occurred")
182
- timeout_home_at_40: Optional[int] = Field(None, description="Home timeouts when clock was at 40 (before 40→25)")
183
- timeout_away_at_40: Optional[int] = Field(None, description="Away timeouts when clock was at 40 (before 40→25)")
184
- timeout_conf_at_40: float = Field(0.0, description="Timeout confidence when clock was at 40")
185
-
186
- # Pending abnormal drop timeout check (silent background check)
187
- # When an "abnormal clock drop" is detected (40→25 in <2s), we reject the play but silently
188
- # monitor for timeout indicator changes. If a timeout is detected within 4-10s, we retroactively create a timeout play.
189
- pending_abnormal_drop_timestamp: Optional[float] = Field(None, description="When the 40→25 occurred for abnormal drop")
190
- pending_abnormal_drop_check_start: Optional[float] = Field(None, description="When to start checking for timeout (4s after)")
191
- pending_abnormal_drop_check_end: Optional[float] = Field(None, description="When to stop checking for timeout (10s after)")
192
- pending_abnormal_drop_home_at_40: Optional[int] = Field(None, description="Home timeouts before the abnormal drop")
193
- pending_abnormal_drop_away_at_40: Optional[int] = Field(None, description="Away timeouts before the abnormal drop")
194
- pending_abnormal_drop_conf_at_40: float = Field(0.0, description="Confidence of timeout reading before abnormal drop")
195
-
196
-
197
  # =============================================================================
198
- # New state models for restructured tracker architecture
199
  # =============================================================================
200
 
201
 
 
15
 
16
  from pydantic import BaseModel, Field
17
 
 
18
  # =============================================================================
19
  # Utility Functions
20
  # =============================================================================
 
140
  special: int = Field(0, description="Class C: Special play (injury/punt/FG/XP)")
141
 
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  # =============================================================================
144
+ # State models for restructured tracker architecture
145
  # =============================================================================
146
 
147
 
src/ui/__init__.py CHANGED
@@ -22,7 +22,6 @@ from .sessions import (
22
 
23
  # Public API
24
  from .api import (
25
- calibrate_timeout_region,
26
  extract_sample_frames_for_selection,
27
  get_video_path_from_user,
28
  print_banner,
@@ -47,7 +46,6 @@ __all__ = [
47
  "ScorebugSelectionSession",
48
  "TimeoutSelectionSession",
49
  # Public API
50
- "calibrate_timeout_region",
51
  "extract_sample_frames_for_selection",
52
  "get_video_path_from_user",
53
  "print_banner",
 
22
 
23
  # Public API
24
  from .api import (
 
25
  extract_sample_frames_for_selection,
26
  get_video_path_from_user,
27
  print_banner,
 
46
  "ScorebugSelectionSession",
47
  "TimeoutSelectionSession",
48
  # Public API
 
49
  "extract_sample_frames_for_selection",
50
  "get_video_path_from_user",
51
  "print_banner",
src/ui/api.py CHANGED
@@ -13,8 +13,6 @@ import cv2
13
  import numpy as np
14
 
15
  from video.frame_extractor import extract_sample_frames
16
- from detection.timeout_calibrator import calibrate_timeout_ovals, visualize_calibration
17
- from detection.models import CalibratedTimeoutRegion
18
 
19
  from .models import BBox
20
  from .sessions import FlagRegionSelectionSession, PlayClockSelectionSession, ScorebugSelectionSession, TimeoutSelectionSession
@@ -192,61 +190,6 @@ def select_flag_region(selected_frame: Tuple[float, np.ndarray[Any, Any]], score
192
  return bbox.to_tuple() if bbox else None
193
 
194
 
195
- def calibrate_timeout_region(
196
- frame: np.ndarray[Any, Any],
197
- region_bbox: Tuple[int, int, int, int],
198
- team: str,
199
- timestamp: float = 0.0,
200
- show_visualization: bool = True,
201
- ) -> Optional[CalibratedTimeoutRegion]:
202
- """
203
- Calibrate a timeout region by detecting oval positions.
204
-
205
- This function should be called after region selection to find the precise
206
- locations of the 3 timeout indicator ovals within the region.
207
-
208
- Args:
209
- frame: Video frame (BGR format) where all 3 timeouts should be visible.
210
- region_bbox: Timeout region bounding box (x, y, width, height).
211
- team: "home" or "away".
212
- timestamp: Video timestamp for reference.
213
- show_visualization: Whether to display the calibration result.
214
-
215
- Returns:
216
- CalibratedTimeoutRegion with discovered oval positions, or None if calibration failed.
217
- """
218
- logger.info("Calibrating %s timeout region at timestamp %.1fs", team, timestamp)
219
-
220
- # Run calibration
221
- calibrated = calibrate_timeout_ovals(frame, region_bbox, team, timestamp)
222
-
223
- if calibrated is None:
224
- logger.error("Calibration failed for %s timeout region", team)
225
- return None
226
-
227
- if len(calibrated.ovals) == 0:
228
- logger.error("No ovals detected in %s timeout region", team)
229
- return None
230
-
231
- # Log results
232
- logger.info("Calibrated %s timeout region: %d ovals found", team, len(calibrated.ovals))
233
- for i, oval in enumerate(calibrated.ovals):
234
- logger.info(" Oval %d: pos=(%d,%d) size=%dx%d brightness=%.1f", i + 1, oval.x, oval.y, oval.width, oval.height, oval.baseline_brightness)
235
-
236
- # Show visualization if requested
237
- if show_visualization:
238
- vis_frame = visualize_calibration(frame, calibrated)
239
- window_name = f"{team.upper()} Timeout Calibration"
240
- cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
241
- cv2.resizeWindow(window_name, 800, 600)
242
- cv2.imshow(window_name, vis_frame)
243
- print(f"\n{team.upper()} timeout calibration complete. Press any key to continue...")
244
- cv2.waitKey(0)
245
- cv2.destroyWindow(window_name)
246
-
247
- return calibrated
248
-
249
-
250
  def get_video_path_from_user(project_root: Path) -> Optional[str]:
251
  """
252
  Prompt user to enter video file path.
 
13
  import numpy as np
14
 
15
  from video.frame_extractor import extract_sample_frames
 
 
16
 
17
  from .models import BBox
18
  from .sessions import FlagRegionSelectionSession, PlayClockSelectionSession, ScorebugSelectionSession, TimeoutSelectionSession
 
190
  return bbox.to_tuple() if bbox else None
191
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  def get_video_path_from_user(project_root: Path) -> Optional[str]:
194
  """
195
  Prompt user to enter video file path.
src/utils/__init__.py CHANGED
@@ -18,7 +18,6 @@ from .logging import (
18
  log_flag_plays,
19
  log_play_complete,
20
  log_play_created,
21
- log_clock_transition,
22
  )
23
 
24
  __all__ = [
@@ -37,5 +36,4 @@ __all__ = [
37
  "log_flag_plays",
38
  "log_play_complete",
39
  "log_play_created",
40
- "log_clock_transition",
41
  ]
 
18
  log_flag_plays,
19
  log_play_complete,
20
  log_play_created,
 
21
  )
22
 
23
  __all__ = [
 
36
  "log_flag_plays",
37
  "log_play_complete",
38
  "log_play_created",
 
39
  ]
src/utils/logging.py CHANGED
@@ -99,36 +99,3 @@ def log_play_created(
99
  play.end_time,
100
  duration,
101
  )
102
-
103
-
104
- def log_clock_transition(
105
- timestamp: float,
106
- time_at_40: float,
107
- action: str,
108
- logger_instance: logging.Logger,
109
- to_clock: int = 25,
110
- ) -> None:
111
- """
112
- Log 40→25 clock transition events.
113
-
114
- Replaces 5-6 line logging blocks like:
115
- logger.info(
116
- "40→25 transition at %.1fs (%.1fs at 40). Handing off for timeout check.",
117
- timestamp, time_at_40,
118
- )
119
-
120
- Args:
121
- timestamp: When the transition occurred
122
- time_at_40: How long clock was at 40 before transitioning
123
- action: Description of what's happening ("handoff for timeout check",
124
- "mid-play, signaling handoff", etc.)
125
- logger_instance: Logger to use for output
126
- to_clock: Target clock value (default 25)
127
- """
128
- logger_instance.info(
129
- "40→%d transition at %.1fs (%.1fs at 40). %s.",
130
- to_clock,
131
- timestamp,
132
- time_at_40,
133
- action,
134
- )
 
99
  play.end_time,
100
  duration,
101
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/video/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
  """Video processing modules for frame extraction and FFmpeg operations."""
2
 
3
- from .frame_extractor import extract_sample_frames, get_video_duration, get_video_fps
4
  from .frame_reader import ThreadedFrameReader
5
  from .ffmpeg_ops import (
6
  extract_clip_stream_copy,
@@ -13,7 +13,6 @@ from .ffmpeg_ops import (
13
  __all__ = [
14
  "extract_sample_frames",
15
  "get_video_duration",
16
- "get_video_fps",
17
  "ThreadedFrameReader",
18
  "extract_clip_stream_copy",
19
  "extract_clip_reencode",
 
1
  """Video processing modules for frame extraction and FFmpeg operations."""
2
 
3
+ from .frame_extractor import extract_sample_frames, get_video_duration
4
  from .frame_reader import ThreadedFrameReader
5
  from .ffmpeg_ops import (
6
  extract_clip_stream_copy,
 
13
  __all__ = [
14
  "extract_sample_frames",
15
  "get_video_duration",
 
16
  "ThreadedFrameReader",
17
  "extract_clip_stream_copy",
18
  "extract_clip_reencode",
src/video/frame_extractor.py CHANGED
@@ -15,28 +15,6 @@ import numpy as np
15
  logger = logging.getLogger(__name__)
16
 
17
 
18
- def get_video_fps(video_path: str) -> float:
19
- """
20
- Get the frames per second (FPS) of a video file.
21
-
22
- Args:
23
- video_path: Path to video file.
24
-
25
- Returns:
26
- FPS as a float.
27
-
28
- Raises:
29
- ValueError: If video cannot be opened.
30
- """
31
- cap = cv2.VideoCapture(video_path)
32
- if not cap.isOpened():
33
- raise ValueError(f"Could not open video: {video_path}")
34
-
35
- fps = cap.get(cv2.CAP_PROP_FPS)
36
- cap.release()
37
- return fps
38
-
39
-
40
  def get_video_duration(video_path: str) -> Optional[float]:
41
  """
42
  Get duration of a video file using ffprobe.
 
15
  logger = logging.getLogger(__name__)
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  def get_video_duration(video_path: str) -> Optional[float]:
19
  """
20
  Get duration of a video file using ffprobe.