diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/requirements.txt b/requirements.txt index 742213c..fbc9097 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ fastapi==0.115.2 uvicorn==0.31.1 httpx==0.27.2 +elevenlabs==2.18.0 +python-dotenv==1.1.1 +fastf1==3.6.1 +pandas==2.3.3 +requests==2.32.5 \ No newline at end of file diff --git a/scripts/data/audio/pit_call.mp3 b/scripts/data/audio/pit_call.mp3 new file mode 100644 index 0000000..8e4f633 Binary files /dev/null and b/scripts/data/audio/pit_call.mp3 differ diff --git a/scripts/fetch_race_data.py b/scripts/fetch_race_data.py old mode 100644 new mode 100755 index ed8769b..1d5a1a9 --- a/scripts/fetch_race_data.py +++ b/scripts/fetch_race_data.py @@ -175,7 +175,7 @@ def prepare_telemetry_stream(telemetry: pd.DataFrame, sample_rate_hz: float = 10 # Resample to target rate if needed telemetry = telemetry.copy() telemetry['Time'] = pd.to_timedelta(telemetry['Time']) - telemetry = telemetry.sort_values('Time') + telemetry = telemetry.sort_values(['LapNumber', 'Time']) # Convert to milliseconds for easier time tracking telemetry['TimeMs'] = (telemetry['Time'].dt.total_seconds() * 1000).astype(int) diff --git a/scripts/strategy_voice_integration.py b/scripts/strategy_voice_integration.py new file mode 100644 index 0000000..0c39dae --- /dev/null +++ b/scripts/strategy_voice_integration.py @@ -0,0 +1,31 @@ +""" +Example: Integrate voice feedback with AI strategy decisions +""" + +from voice_service import RaceEngineerVoice +from pathlib import Path + +def announce_strategy_decision(decision: dict): + """ + Convert AI strategy decision to voice announcement. + + Args: + decision: Dict with keys like 'action', 'tire_compound', 'lap' + """ + engineer = RaceEngineerVoice() + + # Generate appropriate message + if decision['action'] == 'pit': + text = f"Box this lap for {decision['tire_compound']}. In, in, in!" + elif decision['action'] == 'stay_out': + text = "Stay out. These tires are still competitive" + elif decision['action'] == 'push': + text = f"Push mode. We need {decision.get('gap_target', 3)} seconds" + else: + text = decision.get('message', 'Copy that') + + # Synthesize and save + audio_path = Path(f"data/audio/lap_{decision.get('lap', 0)}_command.mp3") + engineer.synthesize_strategy_message(text, audio_path) + + return audio_path \ No newline at end of file diff --git a/scripts/voice_service.py b/scripts/voice_service.py new file mode 100644 index 0000000..d30454a --- /dev/null +++ b/scripts/voice_service.py @@ -0,0 +1,90 @@ +""" +ElevenLabs Voice Service for F1 Race Engineer + +Converts AI strategy recommendations to natural speech. +""" + +import os +from pathlib import Path +from elevenlabs.client import ElevenLabs +from elevenlabs import save +from dotenv import load_dotenv + +load_dotenv() + +class RaceEngineerVoice: + def __init__(self, voice_id: str = "mbBupyLcEivjpxh8Brkf"): # Default: Rachel + """ + Initialize ElevenLabs voice service. + + Args: + voice_id: ElevenLabs voice ID (Rachel is default, professional female voice) + """ + self.client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY")) + self.voice_id = voice_id + + def synthesize_strategy_message( + self, + text: str, + output_path: Path, + stability: float = 0.4, + similarity_boost: float = 0.95 + ) -> Path: + """ + Convert strategy text to speech. + + Args: + text: Message to synthesize (e.g., "Box this lap, box this lap") + output_path: Where to save audio file + stability: Voice stability (0-1) + similarity_boost: Voice similarity (0-1) + + Returns: + Path to generated audio file + """ + audio = self.client.text_to_speech.convert( + voice_id=self.voice_id, + text=text, + model_id="eleven_multilingual_v2", # Fast, low-latency model + voice_settings={ + "stability": stability, + "similarity_boost": similarity_boost, + "style": 0.7, + "use_speaker_boost": True + } + ) + + # Save audio + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Write the audio generator to file + save(audio, str(output_path)) + + return output_path + + def race_engineer_commands(self) -> dict: + """Common F1 race engineer commands""" + return { + "box_now": "Box this lap, box this lap", + "stay_out": "Stay out, stay out. We're looking good on these tires", + "push": "Push now, push. We need to build a gap", + "save_tires": "Manage those tires. Lift and coast into the corners", + "traffic_ahead": "Traffic ahead. Blue flags expected", + "safety_car": "Safety car, safety car. We're checking strategy", + "undercut_threat": "Undercut threat from behind. We may need to respond", + "fastest_lap": "You're on for fastest lap. Push this lap", + } + + +# Example usage +if __name__ == "__main__": + engineer = RaceEngineerVoice() + + # Generate pit call + audio_file = engineer.synthesize_strategy_message( + text="Mama Mia! That was a close one! Let's keep going and finish this out strong, like a cheetah on espresso!", + output_path=Path("data/audio/pit_call.mp3") + ) + + print(f"✓ Generated: {audio_file}") \ No newline at end of file