Spaces:
Running
Running
zach
commited on
Commit
·
7854f13
1
Parent(s):
bf6610d
raise friendlier error message to UI from integration code
Browse files- src/integrations/anthropic_api.py +44 -19
- src/integrations/elevenlabs_api.py +40 -14
- src/integrations/hume_api.py +35 -37
src/integrations/anthropic_api.py
CHANGED
@@ -14,7 +14,7 @@ Key Features:
|
|
14 |
# Standard Library Imports
|
15 |
import logging
|
16 |
from dataclasses import dataclass, field
|
17 |
-
from typing import
|
18 |
|
19 |
# Third-Party Library Imports
|
20 |
from anthropic import APIError
|
@@ -214,22 +214,47 @@ async def generate_text_with_claude(character_description: str, config: Config)
|
|
214 |
logger.warning(f"Unexpected response type: {type(blocks)}")
|
215 |
return str(blocks or "No content generated.")
|
216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
217 |
except Exception as e:
|
218 |
-
|
219 |
-
if
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
# Standard Library Imports
|
15 |
import logging
|
16 |
from dataclasses import dataclass, field
|
17 |
+
from typing import List, Optional, Union
|
18 |
|
19 |
# Third-Party Library Imports
|
20 |
from anthropic import APIError
|
|
|
214 |
logger.warning(f"Unexpected response type: {type(blocks)}")
|
215 |
return str(blocks or "No content generated.")
|
216 |
|
217 |
+
except APIError as e:
|
218 |
+
logger.error(f"Anthropic API request failed: {e!s}")
|
219 |
+
clean_message = _extract_anthropic_error_message(e)
|
220 |
+
|
221 |
+
if (
|
222 |
+
hasattr(e, 'status_code')
|
223 |
+
and e.status_code is not None
|
224 |
+
and CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE
|
225 |
+
):
|
226 |
+
raise UnretryableAnthropicError(message=clean_message, original_exception=e) from e
|
227 |
+
|
228 |
+
raise AnthropicError(message=clean_message, original_exception=e) from e
|
229 |
+
|
230 |
except Exception as e:
|
231 |
+
error_type = type(e).__name__
|
232 |
+
error_message = str(e) if str(e) else f"An error of type {error_type} occurred"
|
233 |
+
logger.error(f"Error during Anthropic API call: {error_type} - {error_message}")
|
234 |
+
clean_message = "An unexpected error occurred while processing your request. Please try again later."
|
235 |
+
|
236 |
+
raise AnthropicError(message=clean_message, original_exception=e) from e
|
237 |
+
|
238 |
+
|
239 |
+
def _extract_anthropic_error_message(e: APIError) -> str:
|
240 |
+
"""
|
241 |
+
Extracts a clean, user-friendly error message from an Anthropic API error response.
|
242 |
+
|
243 |
+
Args:
|
244 |
+
e (APIError): The Anthropic API error exception containing response information.
|
245 |
+
|
246 |
+
Returns:
|
247 |
+
str: A clean, user-friendly error message suitable for display to end users.
|
248 |
+
"""
|
249 |
+
clean_message = "An unknown error has occurred. Please try again later."
|
250 |
+
|
251 |
+
if hasattr(e, 'body') and isinstance(e.body, dict):
|
252 |
+
error_body = e.body
|
253 |
+
if (
|
254 |
+
'error' in error_body
|
255 |
+
and isinstance(error_body['error'], dict)
|
256 |
+
and 'message' in error_body['error']
|
257 |
+
):
|
258 |
+
clean_message = error_body['error']['message']
|
259 |
+
|
260 |
+
return clean_message
|
src/integrations/elevenlabs_api.py
CHANGED
@@ -128,18 +128,44 @@ async def text_to_speech_with_elevenlabs(
|
|
128 |
|
129 |
return None, audio_file_path
|
130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
except Exception as e:
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
|
129 |
return None, audio_file_path
|
130 |
|
131 |
+
except ApiError as e:
|
132 |
+
logger.error(f"ElevenLabs API request failed: {e!s}")
|
133 |
+
clean_message = _extract_elevenlabs_error_message(e)
|
134 |
+
|
135 |
+
if e.status_code is not None and CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE:
|
136 |
+
raise UnretryableElevenLabsError(message=clean_message, original_exception=e) from e
|
137 |
+
|
138 |
+
raise ElevenLabsError(message=clean_message, original_exception=e) from e
|
139 |
+
|
140 |
except Exception as e:
|
141 |
+
error_type = type(e).__name__
|
142 |
+
error_message = str(e) if str(e) else f"An error of type {error_type} occurred"
|
143 |
+
logger.error(f"Error during ElevenLabs API call: {error_type} - {error_message}")
|
144 |
+
clean_message = "An unexpected error occurred while processing your speech request. Please try again later."
|
145 |
+
|
146 |
+
raise ElevenLabsError(message=error_message, original_exception=e) from e
|
147 |
+
|
148 |
+
|
149 |
+
def _extract_elevenlabs_error_message(e: ApiError) -> str:
|
150 |
+
"""
|
151 |
+
Extracts a clean, user-friendly error message from an ElevenLabs API error response.
|
152 |
+
|
153 |
+
Args:
|
154 |
+
e (ApiError): The ElevenLabs API error exception containing response information.
|
155 |
+
|
156 |
+
Returns:
|
157 |
+
str: A clean, user-friendly error message suitable for display to end users.
|
158 |
+
"""
|
159 |
+
clean_message = "An unknown error has occurred. Please try again later."
|
160 |
+
|
161 |
+
if (
|
162 |
+
hasattr(e, 'body') and e.body
|
163 |
+
and isinstance(e.body, dict)
|
164 |
+
and 'detail' in e.body
|
165 |
+
and isinstance(e.body['detail'], dict)
|
166 |
+
):
|
167 |
+
detail = e.body['detail']
|
168 |
+
if 'message' in detail:
|
169 |
+
clean_message = detail['message']
|
170 |
+
|
171 |
+
return clean_message
|
src/integrations/hume_api.py
CHANGED
@@ -77,30 +77,26 @@ class UnretryableHumeError(HumeError):
|
|
77 |
async def text_to_speech_with_hume(
|
78 |
character_description: str,
|
79 |
text: str,
|
80 |
-
num_generations: int,
|
81 |
config: Config,
|
82 |
-
) ->
|
83 |
"""
|
84 |
Asynchronously synthesizes speech using the Hume TTS API, processes audio data, and writes audio to a file.
|
85 |
|
86 |
This function uses the Hume Python SDK to send a request to the Hume TTS API with a character description
|
87 |
-
and text to be converted to speech.
|
88 |
-
|
89 |
-
and generation ID, saves the audio as an MP3 file, and returns the relevant details.
|
90 |
|
91 |
Args:
|
92 |
character_description (str): Description used for voice synthesis.
|
93 |
text (str): Text to be converted to speech.
|
94 |
-
num_generations (int): Number of audio generations to request (1 or 2).
|
95 |
config (Config): Application configuration containing Hume API settings.
|
96 |
|
97 |
Returns:
|
98 |
-
|
99 |
-
-
|
100 |
-
-
|
101 |
|
102 |
Raises:
|
103 |
-
ValueError: If num_generations is not 1 or 2.
|
104 |
HumeError: For errors communicating with the Hume API.
|
105 |
UnretryableHumeError: For client-side HTTP errors (status code 4xx).
|
106 |
"""
|
@@ -110,30 +106,23 @@ async def text_to_speech_with_hume(
|
|
110 |
f"Text length: {len(text)}."
|
111 |
)
|
112 |
|
113 |
-
if num_generations < 1 or num_generations > 2:
|
114 |
-
raise ValueError("Invalid number of generations specified. Must be 1 or 2.")
|
115 |
-
|
116 |
hume_config = config.hume_config
|
117 |
|
118 |
start_time = time.time()
|
119 |
try:
|
120 |
-
# Initialize the client for this request
|
121 |
hume_client = AsyncHumeClient(
|
122 |
api_key=hume_config.api_key,
|
123 |
timeout=hume_config.request_timeout
|
124 |
)
|
125 |
|
126 |
-
# Create the utterance with the character description and text
|
127 |
utterance = PostedUtterance(
|
128 |
text=text,
|
129 |
description=character_description or None
|
130 |
)
|
131 |
|
132 |
-
# Call the TTS API through the SDK
|
133 |
response: ReturnTts = await hume_client.tts.synthesize_json(
|
134 |
utterances=[utterance],
|
135 |
format=hume_config.file_format,
|
136 |
-
num_generations=num_generations
|
137 |
)
|
138 |
|
139 |
elapsed_time = time.time() - start_time
|
@@ -148,31 +137,22 @@ async def text_to_speech_with_hume(
|
|
148 |
generation_a = generations[0]
|
149 |
generation_a_id, audio_a_path = _parse_hume_tts_generation(generation_a, config)
|
150 |
|
151 |
-
|
152 |
-
return (generation_a_id, audio_a_path)
|
153 |
-
|
154 |
-
generation_b = generations[1]
|
155 |
-
generation_b_id, audio_b_path = _parse_hume_tts_generation(generation_b, config)
|
156 |
-
return (generation_a_id, audio_a_path, generation_b_id, audio_b_path)
|
157 |
|
158 |
except ApiError as e:
|
159 |
elapsed_time = time.time() - start_time
|
160 |
logger.error(f"Hume API request failed after {elapsed_time:.2f} seconds: {e!s}")
|
|
|
|
|
161 |
|
162 |
-
if
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
logger.error(error_message)
|
172 |
-
raise HumeError(
|
173 |
-
message=error_message,
|
174 |
-
original_exception=e,
|
175 |
-
) from e
|
176 |
|
177 |
except Exception as e:
|
178 |
error_type = type(e).__name__
|
@@ -205,3 +185,21 @@ def _parse_hume_tts_generation(generation: ReturnGeneration, config: Config) ->
|
|
205 |
filename = f"{generation.generation_id}.mp3"
|
206 |
audio_file_path = save_base64_audio_to_file(generation.audio, filename, config)
|
207 |
return generation.generation_id, audio_file_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
async def text_to_speech_with_hume(
|
78 |
character_description: str,
|
79 |
text: str,
|
|
|
80 |
config: Config,
|
81 |
+
) -> Tuple[str, str]:
|
82 |
"""
|
83 |
Asynchronously synthesizes speech using the Hume TTS API, processes audio data, and writes audio to a file.
|
84 |
|
85 |
This function uses the Hume Python SDK to send a request to the Hume TTS API with a character description
|
86 |
+
and text to be converted to speech. It extracts the base64-encoded audio and generation ID from the response,
|
87 |
+
saves the audio as an MP3 file, and returns the relevant details.
|
|
|
88 |
|
89 |
Args:
|
90 |
character_description (str): Description used for voice synthesis.
|
91 |
text (str): Text to be converted to speech.
|
|
|
92 |
config (Config): Application configuration containing Hume API settings.
|
93 |
|
94 |
Returns:
|
95 |
+
Tuple[str, str]: A tuple containing:
|
96 |
+
- generation_id (str): Unique identifier for the generated audio.
|
97 |
+
- audio_file_path (str): Path to the saved audio file.
|
98 |
|
99 |
Raises:
|
|
|
100 |
HumeError: For errors communicating with the Hume API.
|
101 |
UnretryableHumeError: For client-side HTTP errors (status code 4xx).
|
102 |
"""
|
|
|
106 |
f"Text length: {len(text)}."
|
107 |
)
|
108 |
|
|
|
|
|
|
|
109 |
hume_config = config.hume_config
|
110 |
|
111 |
start_time = time.time()
|
112 |
try:
|
|
|
113 |
hume_client = AsyncHumeClient(
|
114 |
api_key=hume_config.api_key,
|
115 |
timeout=hume_config.request_timeout
|
116 |
)
|
117 |
|
|
|
118 |
utterance = PostedUtterance(
|
119 |
text=text,
|
120 |
description=character_description or None
|
121 |
)
|
122 |
|
|
|
123 |
response: ReturnTts = await hume_client.tts.synthesize_json(
|
124 |
utterances=[utterance],
|
125 |
format=hume_config.file_format,
|
|
|
126 |
)
|
127 |
|
128 |
elapsed_time = time.time() - start_time
|
|
|
137 |
generation_a = generations[0]
|
138 |
generation_a_id, audio_a_path = _parse_hume_tts_generation(generation_a, config)
|
139 |
|
140 |
+
return (generation_a_id, audio_a_path)
|
|
|
|
|
|
|
|
|
|
|
141 |
|
142 |
except ApiError as e:
|
143 |
elapsed_time = time.time() - start_time
|
144 |
logger.error(f"Hume API request failed after {elapsed_time:.2f} seconds: {e!s}")
|
145 |
+
clean_message = _extract_hume_api_error_message(e)
|
146 |
+
logger.error(f"Full Hume API error: {e!s}")
|
147 |
|
148 |
+
if (
|
149 |
+
hasattr(e, 'status_code')
|
150 |
+
and e.status_code is not None
|
151 |
+
and CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE
|
152 |
+
):
|
153 |
+
raise UnretryableHumeError(message=clean_message, original_exception=e) from e
|
154 |
+
|
155 |
+
raise HumeError(message=clean_message, original_exception=e) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
|
157 |
except Exception as e:
|
158 |
error_type = type(e).__name__
|
|
|
185 |
filename = f"{generation.generation_id}.mp3"
|
186 |
audio_file_path = save_base64_audio_to_file(generation.audio, filename, config)
|
187 |
return generation.generation_id, audio_file_path
|
188 |
+
|
189 |
+
|
190 |
+
def _extract_hume_api_error_message(e: ApiError) -> str:
|
191 |
+
"""
|
192 |
+
Extracts a clean, user-friendly error message from a Hume API error response.
|
193 |
+
|
194 |
+
Args:
|
195 |
+
e (ApiError): The Hume API error exception containing response information.
|
196 |
+
|
197 |
+
Returns:
|
198 |
+
str: A clean, user-friendly error message suitable for display to end users.
|
199 |
+
"""
|
200 |
+
clean_message = "An unknown error has occurred. Please try again later."
|
201 |
+
|
202 |
+
if hasattr(e, 'body') and isinstance(e.body, dict) and 'message' in e.body:
|
203 |
+
clean_message = e.body['message']
|
204 |
+
|
205 |
+
return clean_message
|