import random from elevenlabs import Voice from elevenlabs.client import ElevenLabs from utils import settings class ElevenLabsGenerate: def __init__(self): self.max_chars = 2500 self.client: ElevenLabs = None self.available_voices: list[Voice] = [] # To store fetched Voice objects self.voice_name_to_id_map: dict[str, str] = ( {} ) # To store name -> id mapping for quick lookup @staticmethod def get_available_voices(api_key: str) -> dict[str, str]: """ Fetches available voice names and their IDs from ElevenLabs using a given API key. Returns a dictionary mapping voice names (lowercase) to voice IDs, or an empty dict on failure. """ if not api_key: return {} try: client = ElevenLabs(api_key=api_key) print("\n[1/4] Fetching available ElevenLabs voices...") # Keep client.voices.search as per original code and potential test dependency. # The search method returns GetVoicesV2Response which has a 'voices' attribute. response = client.voices.get_all() # page-size specified as endpoint returns paginated data, only endpoint available for this if not response.voices: raise RuntimeError( "No voices found for your ElevenLabs account. Please check your API key." ) voice_mapping = { voice.name.lower(): voice.voice_id for voice in response.voices } print(f"✅ Success! Found {len(list(response.voices))} voices for your account:") return voice_mapping except Exception as e: print(f"❌ Failed to fetch ElevenLabs voices: {e}") return {} def run(self, text, filepath, random_voice: bool = False): if self.client is None: self.initialize() voice_id_to_use = None if random_voice: voice_id_to_use = self.randomvoice() else: configured_voice_name = str( settings.config["settings"]["tts"]["elevenlabs_voice_name"] ).strip() if not configured_voice_name: # If name is blank, use random voice_id_to_use = self.randomvoice() else: # Use the pre-built map for efficient lookup voice_id_to_use = self.voice_name_to_id_map.get( configured_voice_name.lower() ) if voice_id_to_use is None: # Provide a helpful error message if the configured voice is not found available_voice_names = list( self.voice_name_to_id_map.keys() ) # Get names from the map raise ValueError( f"Configured ElevenLabs voice '{configured_voice_name}' not found. " f"Available voices for your account are: {', '.join(available_voice_names)}. " "Please update 'elevenlabs_voice_name' in your config.toml or GUI." ) audio_stream = self.client.text_to_speech.convert( text=text, voice_id=voice_id_to_use, model_id="eleven_multilingual_v2" ) with open(filepath, "wb") as f: for chunk in audio_stream: f.write(chunk) def initialize(self): api_key = settings.config["settings"]["tts"].get("elevenlabs_api_key") if not api_key: raise ValueError( "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." ) self.client = ElevenLabs(api_key=api_key) # Fetch and store available voices during initialization try: self.available_voices = ( self.client.voices.get_all().voices ) # Store Voice objects self.voice_name_to_id_map = { voice.name.lower(): voice.voice_id for voice in self.available_voices } # Build the map if not self.available_voices: raise RuntimeError( "No voices found for your ElevenLabs account. Please check your API key and account status." ) except Exception as e: raise RuntimeError( f"Failed to fetch ElevenLabs voices: {e}. Please check your API key and internet connection." ) from e def randomvoice(self): if self.client is None: self.initialize() if not self.available_voices: # Use the list of Voice objects raise RuntimeError( "No voices available from ElevenLabs account to choose from." ) # Return the voice_id of a randomly selected voice object return random.choice(self.available_voices).voice_id