integration and bugfixes after testing elevenlabs API calls

pull/2350/head^2
JT 3 months ago
parent 84eea1fd73
commit 35dbd59c7f

3
.gitignore vendored

@ -246,3 +246,6 @@ video_creation/data/envvars.txt
config.toml config.toml
*.exe *.exe
# /feat/elevenlabs_v2_support testing script
test_elevenlabs.py

@ -1,6 +1,6 @@
import random import random
from elevenlabs import save, Voice from elevenlabs import Voice
from elevenlabs.client import ElevenLabs from elevenlabs.client import ElevenLabs
from utils import settings from utils import settings
@ -10,23 +10,31 @@ class elevenlabs:
def __init__(self): def __init__(self):
self.max_chars = 2500 self.max_chars = 2500
self.client: ElevenLabs = None 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 @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. Fetches available voice names and their IDs from ElevenLabs using a given API key.
Returns a list of voice names, or an empty list on failure. Returns a dictionary mapping voice names (lowercase) to voice IDs, or an empty dict on failure.
""" """
if not api_key: if not api_key:
return [] return {}
try: try:
client = ElevenLabs(api_key=api_key) client = ElevenLabs(api_key=api_key)
voices = client.voices.get_all().voices print("\n[1/4] Fetching available ElevenLabs voices...")
return sorted([voice.name for voice in voices]) # Keep client.voices.search as per original code and potential test dependency.
except Exception: # The search method returns GetVoicesV2Response which has a 'voices' attribute.
# Fail silently and return an empty list. The caller will handle the message. response = client.voices.search(include_total_count=True, page_size=100) #page-size specified as endpoint returns paginated data
return [] 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): def run(self, text, filepath, random_voice: bool = False):
if self.client is None: if self.client is None:
@ -40,25 +48,24 @@ class elevenlabs:
if not configured_voice_name: # If name is blank, use random if not configured_voice_name: # If name is blank, use random
voice_id_to_use = self.randomvoice() voice_id_to_use = self.randomvoice()
else: else:
# Look up the configured voice name in the cached available voices # Use the pre-built map for efficient lookup
for voice_obj in self.available_voices: voice_id_to_use = self.voice_name_to_id_map.get(configured_voice_name.lower())
if voice_obj.name.lower() == configured_voice_name.lower():
voice_id_to_use = voice_obj.voice_id
break
if voice_id_to_use is None: if voice_id_to_use is None:
# Provide a helpful error message if the configured voice is not found # 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( raise ValueError(
f"Configured ElevenLabs voice '{configured_voice_name}' not found. " f"Configured ElevenLabs voice '{configured_voice_name}' not found. "
f"Available voices for your account are: {', '.join(available_voice_names)}. " f"Available voices for your account are: {', '.join(available_voice_names)}. "
"Please update 'elevenlabs_voice_name' in your config.toml or GUI." "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" 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): def initialize(self):
api_key = settings.config["settings"]["tts"].get("elevenlabs_api_key") api_key = settings.config["settings"]["tts"].get("elevenlabs_api_key")
@ -70,7 +77,8 @@ class elevenlabs:
self.client = ElevenLabs(api_key=api_key) self.client = ElevenLabs(api_key=api_key)
# Fetch and store available voices during initialization # Fetch and store available voices during initialization
try: 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: if not self.available_voices:
raise RuntimeError("No voices found for your ElevenLabs account. Please check your API key and account status.") raise RuntimeError("No voices found for your ElevenLabs account. Please check your API key and account status.")
except Exception as e: except Exception as e:
@ -79,7 +87,7 @@ class elevenlabs:
def randomvoice(self): def randomvoice(self):
if self.client is None: if self.client is None:
self.initialize() 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.") raise RuntimeError("No voices available from ElevenLabs account to choose from.")
# Return the voice_id of a randomly selected voice object # Return the voice_id of a randomly selected voice object
return random.choice(self.available_voices).voice_id return random.choice(self.available_voices).voice_id

@ -6,7 +6,7 @@ import toml
from rich.console import Console from rich.console import Console
from utils.console import handle_input from utils.console import handle_input
from TTS.elevenlabs import elevenlabs import elevenlabs
console = Console() console = Console()
config = dict # autocomplete config = dict # autocomplete
@ -32,14 +32,22 @@ def check(value, checks, name):
api_key = config.get("settings", {}).get("tts", {}).get("elevenlabs_api_key") api_key = config.get("settings", {}).get("tts", {}).get("elevenlabs_api_key")
if api_key: if api_key:
console.print("\n[blue]Attempting to fetch your ElevenLabs voices...[/blue]") console.print("\n[blue]Attempting to fetch your ElevenLabs voices...[/blue]")
# Use a static method to get voices without initializing the full class try:
available_voices = elevenlabs.get_available_voices(api_key) # This logic is ported from TTS/elevenlabs.py to avoid import issues
if available_voices: client = elevenlabs.ElevenLabs(api_key=api_key)
console.print("[green]Successfully fetched voices![/green]") response = client.voices.search(include_total_count=True, page_size=100)
checks["options"] = available_voices if not response.voices:
checks["explanation"] = "Select a voice from your ElevenLabs account. Leave blank for random." console.print("[yellow]No voices found for your ElevenLabs account. Check your API key.[/yellow]")
else: else:
console.print("[yellow]Could not fetch voices. Check your API key. You can enter a voice name manually.[/yellow]") 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 incorrect = False
if value == {}: if value == {}:
@ -50,9 +58,20 @@ def check(value, checks, name):
except: except:
incorrect = True 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 ( if (
not incorrect and "options" in checks and value not in checks["options"] not incorrect
): # FAILSTATE Value is not one of the options and not is_optional_and_blank
and "options" in checks
and check_value not in checks["options"]
):
incorrect = True incorrect = True
if ( if (
not incorrect not incorrect

Loading…
Cancel
Save