d10g commited on
Commit
ba7e9b3
·
1 Parent(s): c95ad37

Updates to full race commentary

Browse files
reachy_f1_commentator/__init__.py CHANGED
@@ -11,3 +11,5 @@ __author__ = "Dave Starling"
11
  from .main import ReachyF1Commentator
12
 
13
  __all__ = ["ReachyF1Commentator"]
 
 
 
11
  from .main import ReachyF1Commentator
12
 
13
  __all__ = ["ReachyF1Commentator"]
14
+
15
+
reachy_f1_commentator/full_race_mode.py CHANGED
@@ -101,27 +101,24 @@ class FullRaceMode:
101
  playback_speed=self.playback_speed
102
  )
103
 
104
- # Create OpenF1 client for data ingestion
105
- openf1_api_client = OpenF1Client(api_key="")
106
- openf1_api_client.authenticate()
107
-
108
  # Create data ingestion module in replay mode
109
  from .src.config import Config
110
  from .src.event_queue import PriorityEventQueue
111
 
112
  config = Config()
113
- event_queue = PriorityEventQueue()
 
 
 
 
114
 
115
  self.data_ingestion = DataIngestionModule(
116
  config=config,
117
- openf1_client=openf1_api_client,
118
  event_queue=event_queue
119
  )
120
 
121
- # Set replay mode
122
- self.data_ingestion.set_replay_mode(
123
- replay_controller=self.replay_controller
124
- )
125
 
126
  self._initialized = True
127
  logger.info("Full Race Mode initialized successfully")
@@ -166,25 +163,47 @@ class FullRaceMode:
166
  )
167
  ingestion_thread.start()
168
 
 
 
 
169
  # Yield events from queue
 
 
 
170
  while True:
171
  try:
172
- # Get event from queue (with timeout)
173
- event = event_queue.get(timeout=1.0)
174
-
175
- if event is None:
176
- # End of race signal
177
- logger.info("End of race reached")
178
- break
179
 
180
- yield event
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
  except Exception as e:
183
- # Timeout or other error
184
- if not ingestion_thread.is_alive():
185
- logger.info("Ingestion thread stopped")
186
- break
187
- continue
188
 
189
  # Stop ingestion
190
  self.data_ingestion.stop()
 
101
  playback_speed=self.playback_speed
102
  )
103
 
 
 
 
 
104
  # Create data ingestion module in replay mode
105
  from .src.config import Config
106
  from .src.event_queue import PriorityEventQueue
107
 
108
  config = Config()
109
+ config.replay_mode = True # Enable replay mode
110
+ config.replay_race_id = self.session_key # Set the session key
111
+ config.replay_speed = self.playback_speed # Set playback speed
112
+ # skip_large_gaps defaults to True, which is fine now that we handle starting grid -> race start
113
+ event_queue = PriorityEventQueue(max_size=100) # Larger queue for replay mode
114
 
115
  self.data_ingestion = DataIngestionModule(
116
  config=config,
 
117
  event_queue=event_queue
118
  )
119
 
120
+ # The replay controller will be created by DataIngestionModule
121
+ # when it starts in replay mode
 
 
122
 
123
  self._initialized = True
124
  logger.info("Full Race Mode initialized successfully")
 
163
  )
164
  ingestion_thread.start()
165
 
166
+ # Give the thread a moment to start
167
+ time.sleep(0.1)
168
+
169
  # Yield events from queue
170
+ no_event_count = 0
171
+ max_no_event_iterations = 600 # Increased to 60 seconds to handle long waits during replay
172
+
173
  while True:
174
  try:
175
+ # Get event from queue using dequeue()
176
+ event = event_queue.dequeue()
 
 
 
 
 
177
 
178
+ if event is not None:
179
+ no_event_count = 0 # Reset counter when we get an event
180
+ yield event
181
+ else:
182
+ # No event available
183
+ no_event_count += 1
184
+
185
+ # Check if thread is still alive
186
+ if not ingestion_thread.is_alive():
187
+ # Thread stopped, check if there are any remaining events
188
+ remaining_event = event_queue.dequeue()
189
+ if remaining_event is None:
190
+ logger.info("Ingestion thread stopped and queue is empty")
191
+ break
192
+ else:
193
+ # Still have events, yield them
194
+ yield remaining_event
195
+ no_event_count = 0
196
+ elif no_event_count >= max_no_event_iterations:
197
+ logger.warning(f"No events received for {max_no_event_iterations} iterations, stopping")
198
+ logger.warning("This may indicate the replay is stuck or has very long gaps between events")
199
+ break
200
+ else:
201
+ # Wait a bit before checking again
202
+ time.sleep(0.1)
203
 
204
  except Exception as e:
205
+ logger.error(f"Error getting event from queue: {e}", exc_info=True)
206
+ break
 
 
 
207
 
208
  # Stop ingestion
209
  self.data_ingestion.stop()
reachy_f1_commentator/main.py CHANGED
@@ -364,7 +364,12 @@ class ReachyF1Commentator(ReachyMiniApp):
364
  # Generate commentary
365
  try:
366
  commentary = self.commentary_generator.generate(event)
367
- if commentary: # Only log non-empty commentary
 
 
 
 
 
368
  logger.info(f"[Lap {lap_number}] {commentary}")
369
  event_count += 1
370
 
@@ -385,6 +390,13 @@ class ReachyF1Commentator(ReachyMiniApp):
385
  self.speech_synthesizer.synthesize_and_play(commentary)
386
  except Exception as e:
387
  logger.error(f"Audio synthesis error: {e}", exc_info=True)
 
 
 
 
 
 
 
388
 
389
  except Exception as e:
390
  logger.error(f"Error generating commentary: {e}", exc_info=True)
@@ -514,8 +526,13 @@ async def get_races(year: int):
514
  if _app_instance is None:
515
  raise HTTPException(status_code=503, detail="App not initialized")
516
 
 
517
  races = _app_instance.openf1_client.get_races_by_year(year)
518
 
 
 
 
 
519
  # Convert to dict format
520
  races_data = [
521
  {
@@ -528,10 +545,11 @@ async def get_races(year: int):
528
  for race in races
529
  ]
530
 
 
531
  return {"races": races_data}
532
  except Exception as e:
533
- logger.error(f"Failed to get races for year {year}: {e}")
534
- raise HTTPException(status_code=500, detail=str(e))
535
 
536
 
537
  @app.post("/api/commentary/start")
 
364
  # Generate commentary
365
  try:
366
  commentary = self.commentary_generator.generate(event)
367
+
368
+ # Debug: check what we got back
369
+ logger.debug(f"Event type: {event.event_type.value}, Commentary: {repr(commentary)[:100]}")
370
+
371
+ # Skip empty or whitespace-only commentary
372
+ if commentary and isinstance(commentary, str) and commentary.strip():
373
  logger.info(f"[Lap {lap_number}] {commentary}")
374
  event_count += 1
375
 
 
390
  self.speech_synthesizer.synthesize_and_play(commentary)
391
  except Exception as e:
392
  logger.error(f"Audio synthesis error: {e}", exc_info=True)
393
+
394
+ # Add a small delay between commentary pieces to prevent queue overflow
395
+ # and give more natural pacing. At 1x speed, this ensures we don't
396
+ # generate commentary faster than race events occur.
397
+ # The delay is scaled by playback speed.
398
+ delay = 1.0 / playback_speed # 1 second at 1x, 0.1s at 10x, 0.05s at 20x
399
+ time.sleep(delay)
400
 
401
  except Exception as e:
402
  logger.error(f"Error generating commentary: {e}", exc_info=True)
 
526
  if _app_instance is None:
527
  raise HTTPException(status_code=503, detail="App not initialized")
528
 
529
+ logger.info(f"Fetching races for year {year}")
530
  races = _app_instance.openf1_client.get_races_by_year(year)
531
 
532
+ if not races:
533
+ logger.warning(f"No races found for year {year}")
534
+ return {"races": []}
535
+
536
  # Convert to dict format
537
  races_data = [
538
  {
 
545
  for race in races
546
  ]
547
 
548
+ logger.info(f"Returning {len(races_data)} races for year {year}")
549
  return {"races": races_data}
550
  except Exception as e:
551
+ logger.error(f"Failed to get races for year {year}: {e}", exc_info=True)
552
+ raise HTTPException(status_code=500, detail=f"Failed to load races: {str(e)}")
553
 
554
 
555
  @app.post("/api/commentary/start")
reachy_f1_commentator/openf1_client.py CHANGED
@@ -67,13 +67,23 @@ class OpenF1APIClient:
67
  def get_years(self) -> List[int]:
68
  """
69
  Get list of available years with race data.
 
70
 
71
  Returns:
72
  List of years in descending order
73
  """
74
  try:
 
 
75
  races = self.get_race_sessions()
76
- years = sorted(set(r.get('year', 0) for r in races if r.get('year')), reverse=True)
 
 
 
 
 
 
 
77
  logger.info(f"Found {len(years)} years with race data: {years}")
78
  return years
79
  except Exception as e:
 
67
  def get_years(self) -> List[int]:
68
  """
69
  Get list of available years with race data.
70
+ Only returns years with completed races (excludes current/future years).
71
 
72
  Returns:
73
  List of years in descending order
74
  """
75
  try:
76
+ from datetime import datetime
77
+
78
  races = self.get_race_sessions()
79
+ current_year = datetime.now().year
80
+
81
+ # Filter to only include years before current year
82
+ # (current year races may not have telemetry data yet)
83
+ years = sorted(
84
+ set(r.get('year', 0) for r in races if r.get('year') and r.get('year') < current_year),
85
+ reverse=True
86
+ )
87
  logger.info(f"Found {len(years)} years with race data: {years}")
88
  return years
89
  except Exception as e:
reachy_f1_commentator/src/commentary_generator.py CHANGED
@@ -441,7 +441,7 @@ class CommentaryGenerator:
441
  style: Commentary style
442
 
443
  Returns:
444
- Template-based commentary text
445
 
446
  Validates: Requirement 5.2
447
  """
@@ -459,6 +459,11 @@ class CommentaryGenerator:
459
  # Normalize event data for template compatibility
460
  normalized_data = self._normalize_event_data(event)
461
 
 
 
 
 
 
462
  # Get additional state data if needed
463
  state_data = self._get_state_data(event)
464
 
@@ -602,4 +607,13 @@ class CommentaryGenerator:
602
  if grid:
603
  data['pole_driver'] = grid[0].get('full_name', 'Unknown')
604
 
 
 
 
 
 
 
 
 
 
605
  return data
 
441
  style: Commentary style
442
 
443
  Returns:
444
+ Template-based commentary text (empty string if event should be skipped)
445
 
446
  Validates: Requirement 5.2
447
  """
 
459
  # Normalize event data for template compatibility
460
  normalized_data = self._normalize_event_data(event)
461
 
462
+ # If normalization returns empty dict, skip this event
463
+ if not normalized_data:
464
+ logger.debug(f"Skipping event {event.event_type.value} - no data after normalization")
465
+ return ""
466
+
467
  # Get additional state data if needed
468
  state_data = self._get_state_data(event)
469
 
 
607
  if grid:
608
  data['pole_driver'] = grid[0].get('full_name', 'Unknown')
609
 
610
+ # Normalize regular position update data (during race)
611
+ elif event.event_type == EventType.POSITION_UPDATE:
612
+ # Position updates are frequent but not very interesting
613
+ # We'll skip most of them but show occasional updates
614
+ # For now, skip all non-starting-grid position updates
615
+ # TODO: Implement logic to show periodic position updates (every 5 laps?)
616
+ logger.debug("Skipping regular position update (not starting grid)")
617
+ return {} # Return empty dict to skip this event
618
+
619
  return data
reachy_f1_commentator/src/data_ingestion.py CHANGED
@@ -501,6 +501,13 @@ class EventParser:
501
  """
502
  Parse race control data to detect flags, safety car, and incidents.
503
 
 
 
 
 
 
 
 
504
  Args:
505
  data: List of race control message dictionaries
506
 
@@ -520,7 +527,41 @@ class EventParser:
520
  category = entry.get('category', '').lower()
521
  lap_number = entry.get('lap_number', 1)
522
 
523
- # Detect flags
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  if 'flag' in message or 'flag' in category:
525
  flag_type = 'yellow'
526
  if 'red' in message:
@@ -528,9 +569,11 @@ class EventParser:
528
  elif 'green' in message:
529
  flag_type = 'green'
530
  elif 'blue' in message:
531
- flag_type = 'blue'
532
  elif 'chequered' in message or 'checkered' in message:
533
  flag_type = 'chequered'
 
 
534
 
535
  # Check if this is the race start (first green flag after grid)
536
  is_race_start = False
@@ -555,7 +598,7 @@ class EventParser:
555
  logger.info(f"Detected flag: {flag_type}")
556
 
557
  # Detect race start from "SESSION STARTED" message
558
- if 'session started' in message and not self._race_started and self._starting_grid_announced:
559
  self._race_started = True
560
  event = RaceEvent(
561
  event_type=EventType.FLAG,
@@ -572,7 +615,7 @@ class EventParser:
572
  logger.info("Detected race start from SESSION STARTED message!")
573
 
574
  # Detect safety car
575
- if 'safety car' in message or 'sc' in category:
576
  status = 'deployed'
577
  if 'in' in message:
578
  status = 'in'
@@ -591,19 +634,20 @@ class EventParser:
591
  events.append(event)
592
  logger.info(f"Detected safety car: {status}")
593
 
594
- # Detect incidents
595
- if 'incident' in message or 'crash' in message or 'collision' in message:
596
- event = RaceEvent(
597
- event_type=EventType.INCIDENT,
598
- timestamp=datetime.now(),
599
- data={
600
- 'description': entry.get('message', ''),
601
- 'drivers_involved': [], # Would need more parsing
602
- 'lap_number': lap_number
603
- }
604
- )
605
- events.append(event)
606
- logger.info(f"Detected incident: {entry.get('message', '')}")
 
607
 
608
  except Exception as e:
609
  logger.error(f"[DataIngestion] Error parsing race control data: {e}", exc_info=True)
@@ -660,14 +704,22 @@ class EventParser:
660
  """
661
  events = []
662
 
 
 
663
  if not data:
 
664
  return events
665
 
 
 
666
  try:
667
  for entry in data:
668
- overtaking_driver_num = entry.get('driver_number')
669
  overtaken_driver_num = entry.get('overtaken_driver_number')
670
  lap_number = entry.get('lap_number', 1)
 
 
 
671
 
672
  if overtaking_driver_num and overtaken_driver_num:
673
  # Get driver names
@@ -682,15 +734,19 @@ class EventParser:
682
  'overtaken_driver': overtaken_driver,
683
  'overtaking_driver_number': str(overtaking_driver_num),
684
  'overtaken_driver_number': str(overtaken_driver_num),
 
685
  'lap_number': lap_number
686
  }
687
  )
688
  events.append(event)
689
- logger.info(f"Detected overtake: {overtaking_driver} overtakes {overtaken_driver} on lap {lap_number}")
 
 
690
 
691
  except Exception as e:
692
  logger.error(f"[DataIngestion] Error parsing overtakes data: {e}", exc_info=True)
693
 
 
694
  return events
695
 
696
  def parse_starting_grid_data(self, data: List[Dict]) -> List[RaceEvent]:
@@ -879,6 +935,13 @@ class DataIngestionModule:
879
 
880
  self._running = True
881
  logger.info(f"Data ingestion module started in REPLAY mode at {self.config.replay_speed}x speed")
 
 
 
 
 
 
 
882
  return True
883
 
884
  def _replay_event_callback(self, endpoint: str, data: Dict) -> None:
@@ -910,9 +973,16 @@ class DataIngestionModule:
910
  logger.warning(f"Unknown endpoint in replay: {endpoint}")
911
  return
912
 
 
 
 
913
  # Parse events (parser expects a list)
914
  events = parser_func([data])
915
 
 
 
 
 
916
  # Emit events to queue
917
  for event in events:
918
  self.event_queue.enqueue(event)
 
501
  """
502
  Parse race control data to detect flags, safety car, and incidents.
503
 
504
+ Filters out boring race control messages and only keeps important ones like:
505
+ - Race start
506
+ - Safety car deployment/withdrawal
507
+ - Red flags
508
+ - Chequered flag
509
+ - Major incidents
510
+
511
  Args:
512
  data: List of race control message dictionaries
513
 
 
527
  category = entry.get('category', '').lower()
528
  lap_number = entry.get('lap_number', 1)
529
 
530
+ # Filter out boring messages - only keep important race control events
531
+ boring_keywords = [
532
+ 'track limits',
533
+ 'deleted',
534
+ 'time',
535
+ 'under investigation',
536
+ 'noted',
537
+ 'reported',
538
+ 'car stopped',
539
+ 'drs enabled',
540
+ 'drs disabled',
541
+ 'permission',
542
+ 'allowed',
543
+ 'document',
544
+ 'stewards',
545
+ 'penalty'
546
+ ]
547
+
548
+ # Skip boring messages unless they're about important events
549
+ is_boring = any(keyword in message for keyword in boring_keywords)
550
+ is_important = (
551
+ 'safety car' in message or
552
+ 'red flag' in message or
553
+ 'chequered' in message or 'checkered' in message or
554
+ 'session started' in message or
555
+ 'green flag' in message or
556
+ 'incident' in message or
557
+ 'crash' in message or
558
+ 'collision' in message
559
+ )
560
+
561
+ if is_boring and not is_important:
562
+ continue # Skip this boring message
563
+
564
+ # Detect flags (only important ones)
565
  if 'flag' in message or 'flag' in category:
566
  flag_type = 'yellow'
567
  if 'red' in message:
 
569
  elif 'green' in message:
570
  flag_type = 'green'
571
  elif 'blue' in message:
572
+ continue # Skip blue flags (not interesting for commentary)
573
  elif 'chequered' in message or 'checkered' in message:
574
  flag_type = 'chequered'
575
+ elif 'yellow' not in message:
576
+ continue # Skip other flag types
577
 
578
  # Check if this is the race start (first green flag after grid)
579
  is_race_start = False
 
598
  logger.info(f"Detected flag: {flag_type}")
599
 
600
  # Detect race start from "SESSION STARTED" message
601
+ elif 'session started' in message and not self._race_started and self._starting_grid_announced:
602
  self._race_started = True
603
  event = RaceEvent(
604
  event_type=EventType.FLAG,
 
615
  logger.info("Detected race start from SESSION STARTED message!")
616
 
617
  # Detect safety car
618
+ elif 'safety car' in message or 'sc' in category:
619
  status = 'deployed'
620
  if 'in' in message:
621
  status = 'in'
 
634
  events.append(event)
635
  logger.info(f"Detected safety car: {status}")
636
 
637
+ # Skip incidents for now - they flood the queue at race start
638
+ # TODO: Re-enable incidents with better filtering later
639
+ # elif 'incident' in message or 'crash' in message or 'collision' in message:
640
+ # event = RaceEvent(
641
+ # event_type=EventType.INCIDENT,
642
+ # timestamp=datetime.now(),
643
+ # data={
644
+ # 'description': entry.get('message', ''),
645
+ # 'drivers_involved': [], # Would need more parsing
646
+ # 'lap_number': lap_number
647
+ # }
648
+ # )
649
+ # events.append(event)
650
+ # logger.info(f"Detected incident: {entry.get('message', '')}")
651
 
652
  except Exception as e:
653
  logger.error(f"[DataIngestion] Error parsing race control data: {e}", exc_info=True)
 
704
  """
705
  events = []
706
 
707
+ logger.debug(f"[EventParser] parse_overtakes_data called with {len(data) if data else 0} records")
708
+
709
  if not data:
710
+ logger.debug("[EventParser] No overtake data to parse")
711
  return events
712
 
713
+ logger.info(f"[EventParser] Parsing {len(data)} overtake records")
714
+
715
  try:
716
  for entry in data:
717
+ overtaking_driver_num = entry.get('overtaking_driver_number')
718
  overtaken_driver_num = entry.get('overtaken_driver_number')
719
  lap_number = entry.get('lap_number', 1)
720
+ position = entry.get('position') # New position after overtake
721
+
722
+ logger.debug(f"[EventParser] Processing overtake: {overtaking_driver_num} -> {overtaken_driver_num}")
723
 
724
  if overtaking_driver_num and overtaken_driver_num:
725
  # Get driver names
 
734
  'overtaken_driver': overtaken_driver,
735
  'overtaking_driver_number': str(overtaking_driver_num),
736
  'overtaken_driver_number': str(overtaken_driver_num),
737
+ 'new_position': position, # Add the position
738
  'lap_number': lap_number
739
  }
740
  )
741
  events.append(event)
742
+ logger.debug(f"Parsed overtake: {overtaking_driver} overtakes {overtaken_driver} for P{position} on lap {lap_number}")
743
+ else:
744
+ logger.warning(f"[EventParser] Skipping overtake with missing driver numbers: {entry}")
745
 
746
  except Exception as e:
747
  logger.error(f"[DataIngestion] Error parsing overtakes data: {e}", exc_info=True)
748
 
749
+ logger.info(f"[EventParser] Created {len(events)} overtake events")
750
  return events
751
 
752
  def parse_starting_grid_data(self, data: List[Dict]) -> List[RaceEvent]:
 
935
 
936
  self._running = True
937
  logger.info(f"Data ingestion module started in REPLAY mode at {self.config.replay_speed}x speed")
938
+
939
+ # Wait for replay to complete (keep thread alive)
940
+ # The replay controller runs in its own thread, so we need to wait for it
941
+ while self._running and self._replay_controller and not self._replay_controller.is_stopped():
942
+ time.sleep(0.1)
943
+
944
+ logger.info("Replay mode completed")
945
  return True
946
 
947
  def _replay_event_callback(self, endpoint: str, data: Dict) -> None:
 
973
  logger.warning(f"Unknown endpoint in replay: {endpoint}")
974
  return
975
 
976
+ # Debug: log endpoint being processed
977
+ logger.debug(f"[DataIngestion] Processing {endpoint} event")
978
+
979
  # Parse events (parser expects a list)
980
  events = parser_func([data])
981
 
982
+ # Debug: log how many events were generated
983
+ if events:
984
+ logger.debug(f"[DataIngestion] Generated {len(events)} events from {endpoint}")
985
+
986
  # Emit events to queue
987
  for event in events:
988
  self.event_queue.enqueue(event)
reachy_f1_commentator/src/event_queue.py CHANGED
@@ -139,9 +139,9 @@ class PriorityEventQueue:
139
  Assign priority based on event type.
140
 
141
  Priority assignment logic:
142
- - CRITICAL: Starting grid, race start, incidents, safety car, lead changes
143
- - HIGH: Overtakes, pit stops
144
- - MEDIUM: Fastest laps
145
  - LOW: Routine position updates
146
 
147
  Args:
@@ -154,11 +154,22 @@ class PriorityEventQueue:
154
  if event.data.get('is_starting_grid') or event.data.get('is_race_start'):
155
  return EventPriority.CRITICAL
156
 
157
- if event.event_type in [EventType.INCIDENT, EventType.SAFETY_CAR, EventType.LEAD_CHANGE]:
 
158
  return EventPriority.CRITICAL
159
- elif event.event_type in [EventType.OVERTAKE, EventType.PIT_STOP]:
160
- return EventPriority.HIGH
 
 
 
 
161
  elif event.event_type == EventType.FASTEST_LAP:
 
 
 
 
162
  return EventPriority.MEDIUM
 
 
163
  else:
164
  return EventPriority.LOW
 
139
  Assign priority based on event type.
140
 
141
  Priority assignment logic:
142
+ - CRITICAL: Starting grid, race start, overtakes, pit stops, incidents, safety car, lead changes
143
+ - HIGH: Fastest laps
144
+ - MEDIUM: Race control messages (flags, etc.)
145
  - LOW: Routine position updates
146
 
147
  Args:
 
154
  if event.data.get('is_starting_grid') or event.data.get('is_race_start'):
155
  return EventPriority.CRITICAL
156
 
157
+ # Overtakes and pit stops are the most interesting events - make them CRITICAL
158
+ if event.event_type in [EventType.OVERTAKE, EventType.PIT_STOP]:
159
  return EventPriority.CRITICAL
160
+
161
+ # Safety car and lead changes also CRITICAL (incidents disabled for now)
162
+ if event.event_type in [EventType.SAFETY_CAR, EventType.LEAD_CHANGE]:
163
+ return EventPriority.CRITICAL
164
+
165
+ # Fastest laps are interesting but less critical
166
  elif event.event_type == EventType.FASTEST_LAP:
167
+ return EventPriority.HIGH
168
+
169
+ # Race control messages (flags, etc.) are medium priority
170
+ elif event.event_type == EventType.FLAG:
171
  return EventPriority.MEDIUM
172
+
173
+ # Everything else is low priority
174
  else:
175
  return EventPriority.LOW
reachy_f1_commentator/src/replay_mode.py CHANGED
@@ -50,6 +50,10 @@ class HistoricalDataLoader:
50
 
51
  # Setup session (no auth needed for historical data)
52
  self.session = requests.Session()
 
 
 
 
53
 
54
  def find_session_key(self, year: int, country_name: str, session_name: str = "Race") -> Optional[int]:
55
  """
@@ -171,6 +175,7 @@ class HistoricalDataLoader:
171
  def _fetch_endpoint(self, endpoint: str, session_key: int) -> List[Dict]:
172
  """
173
  Fetch data from a specific endpoint for a session.
 
174
 
175
  Args:
176
  endpoint: API endpoint path (e.g., '/position')
@@ -179,11 +184,20 @@ class HistoricalDataLoader:
179
  Returns:
180
  List of data dictionaries
181
  """
 
 
 
 
 
 
 
 
182
  url = f"{self.base_url}{endpoint}"
183
  params = {'session_key': session_key}
184
 
185
  try:
186
  response = self.session.get(url, params=params, timeout=10) # Increased timeout for large datasets
 
187
  response.raise_for_status()
188
 
189
  data = response.json()
@@ -197,6 +211,23 @@ class HistoricalDataLoader:
197
  logger.warning(f"Unexpected data type from {endpoint}: {type(data)}")
198
  return []
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  except requests.exceptions.RequestException as e:
201
  logger.error(f"[ReplayMode] Failed to fetch {endpoint} for session {session_key}: {e}", exc_info=True)
202
  return []
@@ -306,8 +337,12 @@ class ReplayController:
306
  List of events with 'endpoint', 'data', and 'timestamp' fields
307
  """
308
  timeline = []
 
309
 
310
  for endpoint, data_list in self.race_data.items():
 
 
 
311
  for data in data_list:
312
  # Extract timestamp
313
  timestamp = self._extract_timestamp(data)
@@ -322,6 +357,7 @@ class ReplayController:
322
  timeline.sort(key=lambda x: x['timestamp'])
323
 
324
  logger.info(f"Built timeline with {len(timeline)} events")
 
325
  return timeline
326
 
327
  def _extract_timestamp(self, data: Dict) -> datetime:
@@ -420,6 +456,8 @@ class ReplayController:
420
 
421
  Validates: Requirement 9.4
422
  """
 
 
423
  self._stopped = True
424
  self._paused = False
425
 
@@ -495,61 +533,92 @@ class ReplayController:
495
 
496
  Validates: Requirements 9.2, 9.4
497
  """
498
- if not self._timeline:
499
- logger.warning("No events in timeline to replay")
500
- return
501
-
502
- # Get the first event's timestamp as reference
503
- first_timestamp = self._timeline[0]['timestamp']
504
- last_event_timestamp = first_timestamp
505
-
506
- # Track cumulative race time (excluding large gaps if enabled)
507
- cumulative_race_time = 0.0
508
-
509
- while self._current_index < len(self._timeline) and not self._stopped:
510
- # Handle pause
511
- while self._paused and not self._stopped:
512
- time.sleep(0.1)
513
-
514
- if self._stopped:
515
- break
516
-
517
- # Get current event
518
- event = self._timeline[self._current_index]
519
- event_timestamp = event['timestamp']
520
-
521
- # Calculate time since last event
522
- time_since_last = (event_timestamp - last_event_timestamp).total_seconds()
523
-
524
- # ALWAYS skip absurdly large gaps (> 600 seconds = 10 minutes)
525
- # These are data artifacts, not actual race time
526
- if time_since_last > 600.0:
527
- logger.info(f"Skipping absurd time gap of {time_since_last:.1f}s at event {self._current_index} (data artifact)")
528
- time_since_last = 0.0
529
- # Skip moderate gaps (> 60 seconds) if skip_large_gaps is enabled
530
- elif self.skip_large_gaps and time_since_last > 60.0:
531
- logger.info(f"Skipping large time gap of {time_since_last:.1f}s at event {self._current_index}")
532
- time_since_last = 0.0
533
 
534
- # Add to cumulative race time
535
- cumulative_race_time += time_since_last
536
 
537
- # Time since playback started (adjusted for speed and pauses)
538
- playback_time_elapsed = (time.time() - self._start_time - self._total_paused_duration) * self.playback_speed
 
539
 
540
- # Wait if we're ahead of schedule
541
- wait_time = cumulative_race_time - playback_time_elapsed
542
- if wait_time > 0:
543
- time.sleep(wait_time / self.playback_speed)
544
 
545
- # Emit event
546
- if self._event_callback and not self._stopped:
547
- try:
548
- self._event_callback(event['endpoint'], event['data'])
549
- except Exception as e:
550
- logger.error(f"[ReplayMode] Error in event callback: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
- last_event_timestamp = event_timestamp
553
- self._current_index += 1
554
 
555
- logger.info("Replay playback completed")
 
 
 
 
 
50
 
51
  # Setup session (no auth needed for historical data)
52
  self.session = requests.Session()
53
+
54
+ # Rate limiting: track last request time
55
+ self._last_request_time = 0
56
+ self._min_request_interval = 0.5 # Minimum 0.5 seconds between requests
57
 
58
  def find_session_key(self, year: int, country_name: str, session_name: str = "Race") -> Optional[int]:
59
  """
 
175
  def _fetch_endpoint(self, endpoint: str, session_key: int) -> List[Dict]:
176
  """
177
  Fetch data from a specific endpoint for a session.
178
+ Includes rate limiting to avoid 429 errors.
179
 
180
  Args:
181
  endpoint: API endpoint path (e.g., '/position')
 
184
  Returns:
185
  List of data dictionaries
186
  """
187
+ # Rate limiting: ensure minimum interval between requests
188
+ current_time = time.time()
189
+ time_since_last_request = current_time - self._last_request_time
190
+ if time_since_last_request < self._min_request_interval:
191
+ sleep_time = self._min_request_interval - time_since_last_request
192
+ logger.debug(f"Rate limiting: sleeping {sleep_time:.2f}s before request")
193
+ time.sleep(sleep_time)
194
+
195
  url = f"{self.base_url}{endpoint}"
196
  params = {'session_key': session_key}
197
 
198
  try:
199
  response = self.session.get(url, params=params, timeout=10) # Increased timeout for large datasets
200
+ self._last_request_time = time.time() # Update last request time
201
  response.raise_for_status()
202
 
203
  data = response.json()
 
211
  logger.warning(f"Unexpected data type from {endpoint}: {type(data)}")
212
  return []
213
 
214
+ except requests.exceptions.HTTPError as e:
215
+ if e.response.status_code == 429:
216
+ logger.warning(f"Rate limit hit for {endpoint}, waiting 2 seconds and retrying...")
217
+ time.sleep(2)
218
+ # Retry once
219
+ try:
220
+ response = self.session.get(url, params=params, timeout=10)
221
+ self._last_request_time = time.time()
222
+ response.raise_for_status()
223
+ data = response.json()
224
+ return data if isinstance(data, list) else [data] if isinstance(data, dict) else []
225
+ except Exception as retry_error:
226
+ logger.error(f"[ReplayMode] Retry failed for {endpoint}: {retry_error}")
227
+ return []
228
+ else:
229
+ logger.error(f"[ReplayMode] Failed to fetch {endpoint} for session {session_key}: {e}", exc_info=True)
230
+ return []
231
  except requests.exceptions.RequestException as e:
232
  logger.error(f"[ReplayMode] Failed to fetch {endpoint} for session {session_key}: {e}", exc_info=True)
233
  return []
 
337
  List of events with 'endpoint', 'data', and 'timestamp' fields
338
  """
339
  timeline = []
340
+ endpoint_counts = {}
341
 
342
  for endpoint, data_list in self.race_data.items():
343
+ count = len(data_list)
344
+ endpoint_counts[endpoint] = count
345
+
346
  for data in data_list:
347
  # Extract timestamp
348
  timestamp = self._extract_timestamp(data)
 
357
  timeline.sort(key=lambda x: x['timestamp'])
358
 
359
  logger.info(f"Built timeline with {len(timeline)} events")
360
+ logger.info(f"Endpoint breakdown: {endpoint_counts}")
361
  return timeline
362
 
363
  def _extract_timestamp(self, data: Dict) -> datetime:
 
456
 
457
  Validates: Requirement 9.4
458
  """
459
+ logger.info(f"[ReplayMode] stop() called at event {self._current_index}, stopped was {self._stopped}")
460
+
461
  self._stopped = True
462
  self._paused = False
463
 
 
533
 
534
  Validates: Requirements 9.2, 9.4
535
  """
536
+ try:
537
+ if not self._timeline:
538
+ logger.warning("No events in timeline to replay")
539
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
+ logger.info(f"[ReplayMode] Starting playback loop with {len(self._timeline)} events")
 
542
 
543
+ # Get the first event's timestamp as reference
544
+ first_timestamp = self._timeline[0]['timestamp']
545
+ last_event_timestamp = first_timestamp
546
 
547
+ # Track cumulative race time (excluding large gaps if enabled)
548
+ cumulative_race_time = 0.0
 
 
549
 
550
+ while self._current_index < len(self._timeline) and not self._stopped:
551
+ # Debug: log loop condition
552
+ if self._current_index == 42:
553
+ logger.info(f"[ReplayMode] At event 42: current_index={self._current_index}, timeline_len={len(self._timeline)}, stopped={self._stopped}")
554
+
555
+ # Handle pause
556
+ while self._paused and not self._stopped:
557
+ time.sleep(0.1)
558
+
559
+ if self._stopped:
560
+ logger.info(f"[ReplayMode] Stopped at event {self._current_index}/{len(self._timeline)}")
561
+ break
562
+
563
+ # Get current event
564
+ event = self._timeline[self._current_index]
565
+ event_timestamp = event['timestamp']
566
+
567
+ # Calculate time since last event
568
+ time_since_last = (event_timestamp - last_event_timestamp).total_seconds()
569
+
570
+ # ALWAYS skip absurdly large gaps (> 600 seconds = 10 minutes)
571
+ # These are data artifacts, not actual race time
572
+ if time_since_last > 600.0:
573
+ logger.info(f"Skipping absurd time gap of {time_since_last:.1f}s at event {self._current_index} (data artifact)")
574
+ time_since_last = 0.0
575
+ # Handle pre-race to race transition (starting grid -> race start)
576
+ # Skip ALL gaps > 10 seconds in the first 100 events (pre-race phase)
577
+ # This handles the gap from grid formation to lights out without long waits
578
+ elif time_since_last > 10.0 and self._current_index < 100:
579
+ logger.info(f"Skipping pre-race time gap of {time_since_last:.1f}s at event {self._current_index} (grid -> race start)")
580
+ time_since_last = 0.0
581
+ # Skip moderate gaps (> 60 seconds) if skip_large_gaps is enabled (after first 100 events)
582
+ elif self.skip_large_gaps and time_since_last > 60.0 and self._current_index >= 100:
583
+ logger.info(f"Skipping large time gap of {time_since_last:.1f}s at event {self._current_index}")
584
+ time_since_last = 0.0
585
+
586
+ # Add to cumulative race time
587
+ cumulative_race_time += time_since_last
588
+
589
+ # Time since playback started (adjusted for speed and pauses)
590
+ playback_time_elapsed = (time.time() - self._start_time - self._total_paused_duration) * self.playback_speed
591
+
592
+ # Wait if we're ahead of schedule
593
+ wait_time = cumulative_race_time - playback_time_elapsed
594
+ if wait_time > 0:
595
+ # Log long waits
596
+ if wait_time > 10.0:
597
+ logger.info(f"[ReplayMode] Long wait: {wait_time:.1f}s at event {self._current_index}")
598
+ time.sleep(wait_time / self.playback_speed)
599
+ elif wait_time < -60.0:
600
+ # We're way behind schedule - log it
601
+ logger.warning(f"[ReplayMode] Behind schedule by {-wait_time:.1f}s at event {self._current_index}")
602
+
603
+ # Emit event
604
+ if self._event_callback and not self._stopped:
605
+ try:
606
+ self._event_callback(event['endpoint'], event['data'])
607
+ except Exception as e:
608
+ logger.error(f"[ReplayMode] Error in event callback: {e}", exc_info=True)
609
+
610
+ last_event_timestamp = event_timestamp
611
+ self._current_index += 1
612
+
613
+ # Log progress every 100 events
614
+ if self._current_index % 100 == 0:
615
+ logger.info(f"[ReplayMode] Progress: {self._current_index}/{len(self._timeline)} events processed")
616
 
617
+ # Loop exited - log why
618
+ logger.info(f"[ReplayMode] Loop exited: current_index={self._current_index}, timeline_len={len(self._timeline)}, stopped={self._stopped}")
619
 
620
+ logger.info(f"[ReplayMode] Playback loop completed: {self._current_index}/{len(self._timeline)} events processed")
621
+ logger.info("Replay playback completed")
622
+
623
+ except Exception as e:
624
+ logger.error(f"[ReplayMode] Exception in playback loop at event {self._current_index}: {e}", exc_info=True)
reachy_f1_commentator/static/index.html CHANGED
@@ -109,7 +109,7 @@
109
 
110
  <div class="info-panel">
111
  <h3>Full Historical Race Mode</h3>
112
- <p>Replay any F1 race from 2018-2024 with:</p>
113
  <ul>
114
  <li>Real race data from OpenF1 API</li>
115
  <li>Configurable playback speed</li>
 
109
 
110
  <div class="info-panel">
111
  <h3>Full Historical Race Mode</h3>
112
+ <p>Replay any F1 race from 2023-2025 with:</p>
113
  <ul>
114
  <li>Real race data from OpenF1 API</li>
115
  <li>Configurable playback speed</li>
test_full_race_commentary.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to simulate full race commentary without TTS.
4
+
5
+ This script runs through a complete race replay and generates all commentary,
6
+ simulating TTS delays to see the actual event processing order and timing.
7
+ """
8
+
9
+ import logging
10
+ import time
11
+ import sys
12
+ import os
13
+ from datetime import datetime
14
+ from collections import defaultdict
15
+
16
+ # Add parent directory to path
17
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
+
19
+ # Setup logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Suppress verbose logs from other modules
28
+ logging.getLogger('reachy_f1_commentator.src.replay_mode').setLevel(logging.WARNING)
29
+ logging.getLogger('reachy_f1_commentator.src.data_ingestion').setLevel(logging.INFO)
30
+
31
+
32
+ def main():
33
+ """Run full race commentary test."""
34
+ # Direct imports to avoid main.py
35
+ from reachy_f1_commentator.src.replay_mode import HistoricalDataLoader, ReplayController
36
+ from reachy_f1_commentator.src.data_ingestion import DataIngestionModule
37
+ from reachy_f1_commentator.src.commentary_generator import CommentaryGenerator
38
+ from reachy_f1_commentator.src.race_state_tracker import RaceStateTracker
39
+ from reachy_f1_commentator.src.config import Config
40
+ from reachy_f1_commentator.src.event_queue import PriorityEventQueue
41
+ from reachy_f1_commentator.src.models import EventType
42
+
43
+ # Configuration
44
+ SESSION_KEY = 9998 # Session with complete data
45
+ PLAYBACK_SPEED = 10 # 10x speed
46
+ SIMULATE_TTS_DELAY = True # Simulate TTS taking time
47
+ TTS_DELAY_PER_CHAR = 0.015 # ~3.7 seconds for 250 chars
48
+ MAX_EVENTS = 100 # Limit events for testing (set to None for full race)
49
+
50
+ logger.info("=" * 80)
51
+ logger.info("FULL RACE COMMENTARY TEST")
52
+ logger.info("=" * 80)
53
+ logger.info(f"Session: {SESSION_KEY}")
54
+ logger.info(f"Playback Speed: {PLAYBACK_SPEED}x")
55
+ logger.info(f"Simulate TTS: {SIMULATE_TTS_DELAY}")
56
+ logger.info(f"Max Events: {MAX_EVENTS if MAX_EVENTS else 'unlimited'}")
57
+ logger.info("=" * 80)
58
+
59
+ # Initialize components
60
+ logger.info("\n📥 Loading race data...")
61
+
62
+ # Create historical data loader
63
+ data_loader = HistoricalDataLoader(
64
+ api_key="",
65
+ base_url="https://api.openf1.org/v1",
66
+ cache_dir=".test_cache"
67
+ )
68
+
69
+ # Load race data
70
+ race_data = data_loader.load_race(SESSION_KEY)
71
+
72
+ if not race_data:
73
+ logger.error("❌ Failed to load race data")
74
+ return
75
+
76
+ # Get metadata
77
+ total_records = sum(len(v) for v in race_data.values())
78
+ logger.info(f"✅ Race loaded:")
79
+ logger.info(f" - Total records: {total_records}")
80
+ logger.info(f" - Drivers: {len(race_data.get('drivers', []))}")
81
+ logger.info(f" - Position updates: {len(race_data.get('position', []))}")
82
+ logger.info(f" - Pit stops: {len(race_data.get('pit', []))}")
83
+ logger.info(f" - Overtakes: {len(race_data.get('overtakes', []))}")
84
+
85
+ # Initialize config and components
86
+ config = Config()
87
+ config.replay_mode = True
88
+ config.replay_race_id = SESSION_KEY
89
+ config.replay_speed = PLAYBACK_SPEED
90
+ config.enhanced_mode = False
91
+
92
+ event_queue = PriorityEventQueue(max_size=100)
93
+ state_tracker = RaceStateTracker()
94
+ commentary_generator = CommentaryGenerator(config, state_tracker)
95
+
96
+ # Create data ingestion module
97
+ data_ingestion = DataIngestionModule(config=config, event_queue=event_queue)
98
+
99
+ # Statistics tracking
100
+ event_counts = defaultdict(int)
101
+ commentary_counts = defaultdict(int)
102
+ total_events = 0
103
+ total_commentary = 0
104
+ total_tts_time = 0.0
105
+ start_time = time.time()
106
+
107
+ logger.info("\n🏁 Starting race playback...\n")
108
+
109
+ # Start data ingestion in background thread
110
+ import threading
111
+ ingestion_thread = threading.Thread(target=data_ingestion.start, daemon=True)
112
+ ingestion_thread.start()
113
+
114
+ # Give it a moment to start
115
+ time.sleep(0.5)
116
+
117
+ # Process events from queue
118
+ try:
119
+ no_event_count = 0
120
+ max_no_event_iterations = 50
121
+
122
+ while True:
123
+ # Get event from queue
124
+ event = event_queue.dequeue()
125
+
126
+ if event is not None:
127
+ no_event_count = 0
128
+ total_events += 1
129
+ event_counts[event.event_type.value] += 1
130
+
131
+ # Check max events limit
132
+ if MAX_EVENTS and total_events > MAX_EVENTS:
133
+ logger.info(f"\n⚠️ Reached max events limit ({MAX_EVENTS}), stopping...")
134
+ break
135
+
136
+ # Get lap number
137
+ lap_number = event.data.get('lap_number', 0)
138
+
139
+ # Generate commentary
140
+ try:
141
+ commentary = commentary_generator.generate(event)
142
+
143
+ # Skip empty commentary
144
+ if not commentary or not commentary.strip():
145
+ continue
146
+
147
+ total_commentary += 1
148
+ commentary_counts[event.event_type.value] += 1
149
+
150
+ # Calculate TTS delay
151
+ tts_delay = 0.0
152
+ if SIMULATE_TTS_DELAY:
153
+ tts_delay = len(commentary) * TTS_DELAY_PER_CHAR
154
+ total_tts_time += tts_delay
155
+
156
+ # Log commentary with timing info
157
+ logger.info(
158
+ f"[Lap {lap_number:2d}] [{event.event_type.value:15s}] "
159
+ f"{commentary[:80]}{'...' if len(commentary) > 80 else ''}"
160
+ )
161
+ if SIMULATE_TTS_DELAY:
162
+ logger.info(f" 💬 TTS delay: {tts_delay:.2f}s")
163
+
164
+ # Simulate TTS delay
165
+ if SIMULATE_TTS_DELAY:
166
+ time.sleep(tts_delay)
167
+
168
+ # Add pacing delay (same as in main.py)
169
+ pacing_delay = 1.0 / PLAYBACK_SPEED
170
+ time.sleep(pacing_delay)
171
+
172
+ except Exception as e:
173
+ logger.error(f"❌ Error generating commentary: {e}", exc_info=True)
174
+
175
+ else:
176
+ # No event available
177
+ no_event_count += 1
178
+
179
+ # Check if thread is still alive
180
+ if not ingestion_thread.is_alive():
181
+ # Thread stopped, check if there are any remaining events
182
+ remaining_event = event_queue.dequeue()
183
+ if remaining_event is None:
184
+ logger.info("\n✅ Ingestion thread stopped and queue is empty")
185
+ break
186
+ else:
187
+ # Process remaining event
188
+ event = remaining_event
189
+ no_event_count = 0
190
+ continue
191
+ elif no_event_count >= max_no_event_iterations:
192
+ logger.warning(f"\n⚠️ No events for {max_no_event_iterations} iterations, stopping")
193
+ break
194
+ else:
195
+ # Wait a bit before checking again
196
+ time.sleep(0.1)
197
+
198
+ except KeyboardInterrupt:
199
+ logger.info("\n⚠️ Interrupted by user")
200
+ finally:
201
+ # Stop data ingestion
202
+ data_ingestion.stop()
203
+
204
+ # Print statistics
205
+ elapsed_time = time.time() - start_time
206
+
207
+ logger.info("\n" + "=" * 80)
208
+ logger.info("RACE COMMENTARY STATISTICS")
209
+ logger.info("=" * 80)
210
+
211
+ logger.info(f"\n📊 Event Statistics:")
212
+ logger.info(f" Total events processed: {total_events}")
213
+ for event_type, count in sorted(event_counts.items(), key=lambda x: x[1], reverse=True):
214
+ logger.info(f" - {event_type:20s}: {count:4d}")
215
+
216
+ logger.info(f"\n🎙️ Commentary Statistics:")
217
+ logger.info(f" Total commentary pieces: {total_commentary}")
218
+ for event_type, count in sorted(commentary_counts.items(), key=lambda x: x[1], reverse=True):
219
+ logger.info(f" - {event_type:20s}: {count:4d}")
220
+
221
+ logger.info(f"\n⏱️ Timing Statistics:")
222
+ logger.info(f" Elapsed time: {elapsed_time:.1f}s")
223
+ logger.info(f" Simulated TTS time: {total_tts_time:.1f}s")
224
+ logger.info(f" Average TTS per commentary: {total_tts_time/total_commentary if total_commentary > 0 else 0:.2f}s")
225
+
226
+ # Calculate what percentage of events got commentary
227
+ if total_events > 0:
228
+ commentary_rate = (total_commentary / total_events) * 100
229
+ logger.info(f"\n📈 Commentary Rate: {commentary_rate:.1f}% of events generated commentary")
230
+
231
+ logger.info("\n" + "=" * 80)
232
+
233
+ # Identify missing event types
234
+ events_without_commentary = set(event_counts.keys()) - set(commentary_counts.keys())
235
+ if events_without_commentary:
236
+ logger.info("\n⚠️ Event types that generated NO commentary:")
237
+ for event_type in events_without_commentary:
238
+ logger.info(f" - {event_type} ({event_counts[event_type]} events)")
239
+
240
+ logger.info("\n✅ Test complete!")
241
+
242
+
243
+ if __name__ == "__main__":
244
+ main()
test_race_events.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple test to see what events are being generated from a race replay.
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ import logging
9
+ import time
10
+ import threading
11
+ from collections import defaultdict
12
+
13
+ # Setup logging
14
+ logging.basicConfig(
15
+ level=logging.DEBUG, # Changed to DEBUG to see more details
16
+ format='%(message)s'
17
+ )
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Suppress verbose logs
22
+ logging.getLogger('urllib3').setLevel(logging.WARNING)
23
+ logging.getLogger('reachy_f1_commentator.src.replay_mode').setLevel(logging.INFO) # Show replay logs
24
+
25
+ # Direct imports
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+
28
+ from reachy_f1_commentator.src.replay_mode import HistoricalDataLoader
29
+ from reachy_f1_commentator.src.data_ingestion import DataIngestionModule
30
+ from reachy_f1_commentator.src.config import Config
31
+ from reachy_f1_commentator.src.event_queue import PriorityEventQueue
32
+
33
+ # Configuration
34
+ SESSION_KEY = 9998
35
+ PLAYBACK_SPEED = 10
36
+ MAX_EVENTS = 500 # Increased to see more events
37
+
38
+ logger.info("=" * 80)
39
+ logger.info(f"RACE EVENT ANALYSIS - Session {SESSION_KEY} at {PLAYBACK_SPEED}x speed")
40
+ logger.info("=" * 80)
41
+
42
+ # Load race data
43
+ logger.info("\nLoading race data...")
44
+ data_loader = HistoricalDataLoader(
45
+ api_key="",
46
+ base_url="https://api.openf1.org/v1",
47
+ cache_dir=".test_cache"
48
+ )
49
+
50
+ race_data = data_loader.load_race(SESSION_KEY)
51
+
52
+ if not race_data:
53
+ logger.error("Failed to load race data")
54
+ sys.exit(1)
55
+
56
+ # Print data summary
57
+ logger.info(f"\nRace Data Summary:")
58
+ logger.info(f" Drivers: {len(race_data.get('drivers', []))}")
59
+ logger.info(f" Starting Grid: {len(race_data.get('starting_grid', []))}")
60
+ logger.info(f" Position Updates: {len(race_data.get('position', []))}")
61
+ logger.info(f" Pit Stops: {len(race_data.get('pit', []))}")
62
+ logger.info(f" Overtakes: {len(race_data.get('overtakes', []))}")
63
+ logger.info(f" Laps: {len(race_data.get('laps', []))}")
64
+ logger.info(f" Race Control: {len(race_data.get('race_control', []))}")
65
+
66
+ # Setup replay
67
+ config = Config()
68
+ config.replay_mode = True
69
+ config.replay_race_id = SESSION_KEY
70
+ config.replay_speed = PLAYBACK_SPEED
71
+
72
+ event_queue = PriorityEventQueue(max_size=100)
73
+ data_ingestion = DataIngestionModule(config=config, event_queue=event_queue)
74
+
75
+ # Start ingestion
76
+ logger.info(f"\nStarting replay at {PLAYBACK_SPEED}x speed...")
77
+ logger.info("=" * 80)
78
+
79
+ ingestion_thread = threading.Thread(target=data_ingestion.start, daemon=True)
80
+ ingestion_thread.start()
81
+
82
+ time.sleep(0.5)
83
+
84
+ # Track events
85
+ event_counts = defaultdict(int)
86
+ event_samples = defaultdict(list)
87
+ total_events = 0
88
+
89
+ try:
90
+ no_event_count = 0
91
+
92
+ while total_events < MAX_EVENTS:
93
+ event = event_queue.dequeue()
94
+
95
+ if event is not None:
96
+ no_event_count = 0
97
+ total_events += 1
98
+ event_type = event.event_type.value
99
+ event_counts[event_type] += 1
100
+
101
+ # Store first 3 samples of each type
102
+ if len(event_samples[event_type]) < 3:
103
+ event_samples[event_type].append(event.data)
104
+
105
+ # Print event
106
+ lap = event.data.get('lap_number', 0)
107
+ logger.info(f"[{total_events:3d}] [Lap {lap:2d}] {event_type:20s} - {str(event.data)[:80]}")
108
+
109
+ else:
110
+ no_event_count += 1
111
+ if not ingestion_thread.is_alive() and event_queue.size() == 0:
112
+ logger.info("\nIngestion complete, queue empty")
113
+ break
114
+ elif no_event_count >= 50:
115
+ logger.info(f"\nNo events for 5 seconds, stopping")
116
+ break
117
+ time.sleep(0.1)
118
+
119
+ except KeyboardInterrupt:
120
+ logger.info("\nInterrupted by user")
121
+
122
+ # Stop ingestion
123
+ data_ingestion.stop()
124
+
125
+ # Print summary
126
+ logger.info("\n" + "=" * 80)
127
+ logger.info("EVENT SUMMARY")
128
+ logger.info("=" * 80)
129
+ logger.info(f"\nTotal events processed: {total_events}")
130
+ logger.info(f"\nEvent counts:")
131
+ for event_type, count in sorted(event_counts.items(), key=lambda x: x[1], reverse=True):
132
+ logger.info(f" {event_type:20s}: {count:4d}")
133
+
134
+ logger.info(f"\nEvent samples (first 3 of each type):")
135
+ for event_type, samples in sorted(event_samples.items()):
136
+ logger.info(f"\n {event_type}:")
137
+ for i, sample in enumerate(samples, 1):
138
+ logger.info(f" {i}. {sample}")
139
+
140
+ logger.info("\n" + "=" * 80)