#!/usr/bin/env python3 """ Debug script to trace special play state machine during extraction. This script runs extraction on a small segment and logs detailed state information from the special play tracker to understand why scorebug disappearance isn't triggering early end. """ import argparse import json import logging import sys from pathlib import Path import cv2 # Add src to path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from detection.scorebug import DetectScoreBug from detection.timeouts import CalibratedTimeoutDetector from readers import PlayClockReading, ReadPlayClock from setup import DigitTemplateLibrary, PlayClockRegionConfig, PlayClockRegionExtractor # Set up verbose logging logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger(__name__) # Also enable debug on the special play tracker logging.getLogger("tracking.special_play_tracker").setLevel(logging.DEBUG) logging.getLogger("tracking.play_tracker").setLevel(logging.DEBUG) def load_session_config(config_path: str) -> dict: """Load session config.""" with open(config_path, "r", encoding="utf-8") as f: return json.load(f) def main(): parser = argparse.ArgumentParser(description="Debug special play state machine") parser.add_argument("--video", required=True, help="Path to video file") parser.add_argument("--config", required=True, help="Path to session config JSON") parser.add_argument("--start", type=float, required=True, help="Start time in seconds") parser.add_argument("--end", type=float, required=True, help="End time in seconds") parser.add_argument("--template-dir", default="output/debug/digit_templates", help="Path to digit templates") args = parser.parse_args() # Load config config = load_session_config(args.config) # Set up fixed coordinates scorebug_bbox = ( config["scorebug_x"], config["scorebug_y"], config["scorebug_width"], config["scorebug_height"], ) playclock_coords = ( config["scorebug_x"] + config["playclock_x_offset"], config["scorebug_y"] + config["playclock_y_offset"], config["playclock_width"], config["playclock_height"], ) template_path = config["template_path"] logger.info("Video: %s", args.video) logger.info("Segment: %.1f - %.1f", args.start, args.end) logger.info("Scorebug bbox: %s", scorebug_bbox) logger.info("Playclock coords: %s", playclock_coords) # Initialize components scorebug_detector = DetectScoreBug(template_path=template_path, fixed_region=scorebug_bbox, use_split_detection=True) # Load digit templates template_library = DigitTemplateLibrary() if not template_library.load(args.template_dir): logger.error("Could not load digit templates from %s", args.template_dir) return template_reader = ReadPlayClock(template_library, config["playclock_width"], config["playclock_height"]) # Initialize play tracker with custom logging from tracking import TrackPlayState, TimeoutInfo play_tracker = TrackPlayState() # Open video cap = cv2.VideoCapture(args.video) fps = cap.get(cv2.CAP_PROP_FPS) frame_interval = 0.5 logger.info("FPS: %.2f, Frame interval: %.2f", fps, frame_interval) # Process frames current_time = args.start frame_count = 0 while current_time <= args.end: frame_num = int(current_time * fps) cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num) ret, frame = cap.read() if not ret or frame is None: logger.warning("Could not read frame at %.1fs", current_time) current_time += frame_interval continue frame_count += 1 # Detect scorebug scorebug = scorebug_detector.detect(frame) # Read clock clock_result = template_reader.read_from_fixed_location(frame, playclock_coords, padding=4) clock_reading = PlayClockReading( detected=clock_result.detected if clock_result else False, value=clock_result.value if clock_result and clock_result.detected else None, confidence=clock_result.confidence if clock_result else 0.0, raw_text="DEBUG", ) # Log state BEFORE update # Access the internal PlayTracker for detailed state inner_tracker = play_tracker._tracker mode = inner_tracker.active_mode.value state = play_tracker.state.value if hasattr(play_tracker.state, "value") else str(play_tracker.state) # Check if we're in special mode if mode == "special": special_state = inner_tracker._special_tracker._state logger.info( " t=%.1f | sb=%s (%.3f) | clk=%s | MODE=%s | phase=%s | last_sb_ts=%s", current_time, "Y" if scorebug.detected else "N", scorebug.confidence, clock_reading.value if clock_reading.detected else "---", mode, special_state.phase.value, special_state.last_scorebug_timestamp, ) else: logger.info( " t=%.1f | sb=%s (%.3f) | clk=%s | MODE=%s | state=%s", current_time, "Y" if scorebug.detected else "N", scorebug.confidence, clock_reading.value if clock_reading.detected else "---", mode, state, ) # Update tracker play = play_tracker.update(current_time, scorebug, clock_reading, None, None) if play: logger.info( " >>> PLAY CREATED: #%d, %.1f-%.1f, type=%s, end_method=%s", play.play_number, play.start_time, play.end_time, play.play_type, play.end_method, ) current_time += frame_interval cap.release() # Summary plays = play_tracker.get_plays() logger.info("\n=== SUMMARY ===") logger.info("Frames processed: %d", frame_count) logger.info("Plays detected: %d", len(plays)) for p in plays: logger.info(" #%d: %.1f-%.1f (%.1fs) type=%s end=%s", p.play_number, p.start_time, p.end_time, p.end_time - p.start_time, p.play_type, p.end_method) if __name__ == "__main__": main()