Spaces:
Running
Running
File size: 6,011 Bytes
a375dbf 681c05f e91a94a fc85b67 7e7e83a 5a007ca a375dbf 104737f 234af57 c0a60aa 5a007ca a375dbf 5ed9749 36b195f a375dbf d1ed6b1 fc85b67 0e508c8 a375dbf fc85b67 d1ed6b1 a375dbf 104737f a375dbf 104737f a375dbf 104737f a375dbf 104737f a375dbf 63ef86b a375dbf d1ed6b1 a375dbf 234af57 104737f a375dbf 104737f c0a60aa a375dbf d1ed6b1 a375dbf 104737f fc85b67 a375dbf 104737f a375dbf 104737f ba3994f 104737f a375dbf 5bf19b3 104737f a375dbf 63ef86b a375dbf 1ed6720 e91a94a a375dbf e91a94a 5bf19b3 a375dbf 0e508c8 a375dbf e91a94a 5ed9749 e91a94a 7f25817 e91a94a a375dbf d4b2b49 0e508c8 d4b2b49 0e508c8 104737f ba3994f a375dbf 7854f13 5ed9749 7854f13 5ed9749 7854f13 a375dbf 7854f13 2192d9b 7854f13 5ed9749 7854f13 2192d9b 7854f13 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# Standard Library Imports
import logging
import random
import time
from dataclasses import dataclass, field
from typing import Optional, Tuple
# Third-Party Library Imports
from elevenlabs import AsyncElevenLabs, TextToVoiceCreatePreviewsRequestOutputFormat
from elevenlabs.core import ApiError
from tenacity import after_log, before_log, retry, retry_if_exception, stop_after_attempt, wait_fixed
# Local Application Imports
from src.common import Config, logger, save_base64_audio_to_file, validate_env_var
from src.common.constants import CLIENT_ERROR_CODE, GENERIC_API_ERROR_MESSAGE, RATE_LIMIT_ERROR_CODE, SERVER_ERROR_CODE
@dataclass(frozen=True)
class ElevenLabsConfig:
"""Immutable configuration for interacting with the ElevenLabs TTS API."""
api_key: str = field(init=False)
output_format: TextToVoiceCreatePreviewsRequestOutputFormat = "mp3_44100_128"
def __post_init__(self):
# Validate required attributes.
if not self.output_format:
raise ValueError("ElevenLabs TTS API output format is not set.")
computed_key = validate_env_var("ELEVENLABS_API_KEY")
object.__setattr__(self, "api_key", computed_key)
@property
def client(self) -> AsyncElevenLabs:
"""
Lazy initialization of the asynchronous ElevenLabs client.
Returns:
AsyncElevenLabs: Configured async client instance.
"""
return AsyncElevenLabs(api_key=self.api_key)
class ElevenLabsError(Exception):
"""Custom exception for errors related to the ElevenLabs TTS API."""
def __init__(self, message: str, original_exception: Optional[Exception] = None):
super().__init__(message)
self.original_exception = original_exception
self.message = message
class UnretryableElevenLabsError(ElevenLabsError):
"""Custom exception for errors related to the ElevenLabs TTS API that should not be retried."""
def __init__(self, message: str, original_exception: Optional[Exception] = None):
super().__init__(message, original_exception)
self.original_exception = original_exception
self.message = message
@retry(
retry=retry_if_exception(lambda e: not isinstance(e, UnretryableElevenLabsError)),
stop=stop_after_attempt(2),
wait=wait_fixed(2),
before=before_log(logger, logging.DEBUG),
after=after_log(logger, logging.DEBUG),
reraise=True,
)
async def text_to_speech_with_elevenlabs(
character_description: str, text: str, config: Config
) -> Tuple[None, str]:
"""
Asynchronously synthesizes speech using the ElevenLabs TTS API, processes the audio data, and writes it to a file.
Args:
character_description (str): The character description used for voice synthesis.
text (str): The text to be synthesized into speech.
config (Config): Application configuration containing ElevenLabs API settings.
Returns:
Tuple[None, str]: A tuple containing:
- generation_id (None): A placeholder (no generation ID is returned).
- file_path (str): The relative file path to the saved audio file.
Raises:
ElevenLabsError: If there is an error communicating with the ElevenLabs API or processing the response.
"""
logger.debug(f"Synthesizing speech with ElevenLabs. Text length: {len(text)} characters.")
elevenlabs_config = config.elevenlabs_config
client = elevenlabs_config.client
start_time = time.time()
try:
response = await client.text_to_voice.create_previews(
voice_description=character_description,
text=text,
output_format=elevenlabs_config.output_format,
)
elapsed_time = time.time() - start_time
logger.info(f"Elevenlabs API request completed in {elapsed_time:.2f} seconds.")
previews = response.previews
if not previews:
raise ElevenLabsError(message="No previews returned by ElevenLabs API.")
preview = random.choice(previews)
generated_voice_id = preview.generated_voice_id
base64_audio = preview.audio_base_64
filename = f"{generated_voice_id}.mp3"
audio_file_path = save_base64_audio_to_file(base64_audio, filename, config)
return None, audio_file_path
except ApiError as e:
logger.error(f"ElevenLabs API request failed: {e!s}")
clean_message = __extract_elevenlabs_error_message(e)
if hasattr(e, 'status_code') and e.status_code is not None:
if e.status_code == RATE_LIMIT_ERROR_CODE:
raise ElevenLabsError(message=clean_message, original_exception=e) from e
if CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE:
raise UnretryableElevenLabsError(message=clean_message, original_exception=e) from e
raise ElevenLabsError(message=clean_message, original_exception=e) from e
except Exception as e:
error_type = type(e).__name__
error_message = str(e) if str(e) else f"An error of type {error_type} occurred"
logger.error(f"Error during ElevenLabs API call: {error_type} - {error_message}")
clean_message = GENERIC_API_ERROR_MESSAGE
raise ElevenLabsError(message=error_message, original_exception=e) from e
def __extract_elevenlabs_error_message(e: ApiError) -> str:
"""
Extracts a clean, user-friendly error message from an ElevenLabs API error response.
Args:
e (ApiError): The ElevenLabs API error exception containing response information.
Returns:
str: A clean, user-friendly error message suitable for display to end users.
"""
clean_message = GENERIC_API_ERROR_MESSAGE
if (
hasattr(e, 'body') and e.body
and isinstance(e.body, dict)
and 'detail' in e.body
and isinstance(e.body['detail'], dict)
):
detail = e.body['detail']
if 'message' in detail:
clean_message = detail['message']
return clean_message
|