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
*.exe
# /feat/elevenlabs_v2_support testing script
test_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

@ -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

Loading…
Cancel
Save