From 35dbd59c7fab295d555cb190c5500b17dacf1bb3 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 21 Jun 2025 00:20:47 +0100 Subject: [PATCH] integration and bugfixes after testing elevenlabs API calls --- .gitignore | 3 +++ TTS/elevenlabs.py | 50 +++++++++++++++++++++++++++-------------------- utils/settings.py | 41 +++++++++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index cc6bd18..b9036fb 100644 --- a/.gitignore +++ b/.gitignore @@ -246,3 +246,6 @@ video_creation/data/envvars.txt config.toml *.exe + +# /feat/elevenlabs_v2_support testing script +test_elevenlabs.py \ No newline at end of file diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index 99634ea..3deaeef 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -1,6 +1,6 @@ import random -from elevenlabs import save, Voice +from elevenlabs import Voice from elevenlabs.client import ElevenLabs from utils import settings @@ -10,23 +10,31 @@ class elevenlabs: def __init__(self): self.max_chars = 2500 self.client: ElevenLabs = None - self.available_voices: list[Voice] = [] # To store fetched voices + 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) -> list[str]: + def get_available_voices(api_key: str) -> dict[str, str]: """ - Fetches available voice names from ElevenLabs using a given API key. - Returns a list of voice names, or an empty list on failure. + 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 [] + return {} try: client = ElevenLabs(api_key=api_key) - voices = client.voices.get_all().voices - return sorted([voice.name for voice in voices]) - except Exception: - # Fail silently and return an empty list. The caller will handle the message. - return [] + 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.search(include_total_count=True, page_size=100) #page-size specified as endpoint returns paginated data + 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 {response.total_count} 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: @@ -40,25 +48,24 @@ class elevenlabs: if not configured_voice_name: # If name is blank, use random voice_id_to_use = self.randomvoice() else: - # Look up the configured voice name in the cached available voices - for voice_obj in self.available_voices: - if voice_obj.name.lower() == configured_voice_name.lower(): - voice_id_to_use = voice_obj.voice_id - break + # 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 = [v.name for v in self.available_voices] + 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 = self.client.text_to_speech.convert( + audio_stream = self.client.text_to_speech.convert( text=text, voice_id=voice_id_to_use, model_id="eleven_multilingual_v2" ) - save(audio=audio, filename=filepath) + 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") @@ -70,7 +77,8 @@ class elevenlabs: self.client = ElevenLabs(api_key=api_key) # Fetch and store available voices during initialization try: - self.available_voices = self.client.voices.get_all().voices + 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: @@ -79,7 +87,7 @@ class elevenlabs: def randomvoice(self): if self.client is None: self.initialize() - if not self.available_voices: + 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 diff --git a/utils/settings.py b/utils/settings.py index 62f0ea1..9a62795 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -6,7 +6,7 @@ import toml from rich.console import Console from utils.console import handle_input -from TTS.elevenlabs import elevenlabs +import elevenlabs console = Console() config = dict # autocomplete @@ -32,14 +32,22 @@ def check(value, checks, name): api_key = config.get("settings", {}).get("tts", {}).get("elevenlabs_api_key") if api_key: console.print("\n[blue]Attempting to fetch your ElevenLabs voices...[/blue]") - # Use a static method to get voices without initializing the full class - available_voices = elevenlabs.get_available_voices(api_key) - if available_voices: - console.print("[green]Successfully fetched voices![/green]") - checks["options"] = available_voices - checks["explanation"] = "Select a voice from your ElevenLabs account. Leave blank for random." - else: - console.print("[yellow]Could not fetch voices. Check your API key. You can enter a voice name manually.[/yellow]") + try: + # This logic is ported from TTS/elevenlabs.py to avoid import issues + client = elevenlabs.ElevenLabs(api_key=api_key) + response = client.voices.search(include_total_count=True, page_size=100) + if not response.voices: + console.print("[yellow]No voices found for your ElevenLabs account. Check your API key.[/yellow]") + else: + available_voice_names = [voice.name.lower() for voice in response.voices] + console.print(f"✅ [green]Success! Found {response.total_count} voices for your account.[/green]") + checks["options"] = available_voice_names + checks["explanation"] = "Select a voice from your ElevenLabs account. Leave blank for random." + except Exception as e: + console.print(f"❌ [red]Failed to fetch ElevenLabs voices: {e}[/red]") + console.print("[yellow]You can enter a voice name manually or leave blank for random.[/yellow]") + # This setting is always optional (blank means random voice) + checks["optional"] = True incorrect = False if value == {}: @@ -50,9 +58,20 @@ def check(value, checks, name): except: incorrect = True + # Prepare value for checks; especially for case-insensitive options like voice names + check_value = value + if name == "elevenlabs_voice_name": + check_value = str(value).lower().strip() + + # A blank value is acceptable for optional fields + is_optional_and_blank = "optional" in checks and checks["optional"] and str(value).strip() == "" + if ( - not incorrect and "options" in checks and value not in checks["options"] - ): # FAILSTATE Value is not one of the options + not incorrect + and not is_optional_and_blank + and "options" in checks + and check_value not in checks["options"] + ): incorrect = True if ( not incorrect