From a22fe5c1bcbb3bb99b876ee4150640da87aeec6d Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Tue, 3 Mar 2026 14:17:00 +0000 Subject: [PATCH] fix: surface ElevenLabs config errors and add availability flag - Extract actual error message from blob response in previewVoice so users see the real API error instead of generic "Failed to generate preview" - VoicePreviewButton now reads err.message from thrown Error objects - Add available: bool field to ProviderVoicesResponse; returns false when ELEVENLABS_API_KEY is not configured so the frontend can react proactively instead of hitting a 400 on preview - VoiceSelector shows a descriptive config warning when available=false Co-Authored-By: Claude Sonnet 4.6 --- backend/app/api/v1/routes_tts.py | 9 ++++ .../src/components/VoicePreviewButton.tsx | 3 +- frontend/src/components/VoiceSelector.tsx | 4 +- frontend/src/lib/api.ts | 45 ++++++++++++------- frontend/src/types/api.ts | 1 + 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/backend/app/api/v1/routes_tts.py b/backend/app/api/v1/routes_tts.py index c715de3..11c07da 100644 --- a/backend/app/api/v1/routes_tts.py +++ b/backend/app/api/v1/routes_tts.py @@ -48,6 +48,7 @@ class ProviderVoicesResponse(BaseModel): provider: str voices: list[VoiceInfo] default: str + available: bool = True class LanguagesResponse(BaseModel): @@ -99,6 +100,13 @@ async def list_voices( List available TTS voices for the specified provider. """ if provider == "elevenlabs": + if not tts_service.elevenlabs_available: + return ProviderVoicesResponse( + provider="elevenlabs", + voices=[], + default="", + available=False, + ) el_voices = await elevenlabs_voice_service.get_voices() voices = [ VoiceInfo( @@ -116,6 +124,7 @@ async def list_voices( provider="elevenlabs", voices=voices, default=default_id, + available=True, ) # Default: Gemini diff --git a/frontend/src/components/VoicePreviewButton.tsx b/frontend/src/components/VoicePreviewButton.tsx index 2a5b095..b6a8976 100644 --- a/frontend/src/components/VoicePreviewButton.tsx +++ b/frontend/src/components/VoicePreviewButton.tsx @@ -106,7 +106,8 @@ export function VoicePreviewButton({ await audio.play(); setIsPlaying(true); } catch (err) { - setError('Failed to generate preview'); + const errorMessage = err instanceof Error ? err.message : 'Failed to generate preview'; + setError(errorMessage); console.error('Voice preview error:', err); } finally { setIsLoading(false); diff --git a/frontend/src/components/VoiceSelector.tsx b/frontend/src/components/VoiceSelector.tsx index 7abc2b8..45fa9c6 100644 --- a/frontend/src/components/VoiceSelector.tsx +++ b/frontend/src/components/VoiceSelector.tsx @@ -38,7 +38,9 @@ export function VoiceSelector({ setLanguages(languagesData); // Set default voice from API response if switching providers - if (voicesData.default && voicesData.voices.length > 0) { + if (voicesData.available === false) { + setError(`ElevenLabs TTS is not configured on the server. Contact your administrator to set up the ELEVENLABS_API_KEY.`); + } else if (voicesData.default && voicesData.voices.length > 0) { // Only reset default voice if the current one isn't in the new voice list const currentVoiceExists = voicesData.voices.some(v => v.id === preferences.default_voice); if (!currentVoiceExists) { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 70fa1ca..9a6c516 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -376,22 +376,35 @@ class ApiClient { stability?: number, similarityBoost?: number, ): Promise { - const response = await this.client.post( - '/tts/preview', - { - voice_name: voiceName, - language, - provider: provider || 'gemini', - model: model || 'flash', - speed: speed || 1.0, - style_preset: stylePreset || 'neutral', - custom_style_prompt: customStylePrompt, - stability: stability, - similarity_boost: similarityBoost, - }, - { responseType: 'blob' } - ); - return response.data; + try { + const response = await this.client.post( + '/tts/preview', + { + voice_name: voiceName, + language, + provider: provider || 'gemini', + model: model || 'flash', + speed: speed || 1.0, + style_preset: stylePreset || 'neutral', + custom_style_prompt: customStylePrompt, + stability: stability, + similarity_boost: similarityBoost, + }, + { responseType: 'blob' } + ); + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.data instanceof Blob) { + const text = await error.response.data.text(); + try { + const json = JSON.parse(text); + throw new Error(json.detail || 'Failed to generate preview'); + } catch { + throw new Error(text || 'Failed to generate preview'); + } + } + throw error; + } } // Review Notes endpoints diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 8d76ef4..ca851a8 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -85,6 +85,7 @@ export interface ProviderVoicesResponse { provider: string; voices: VoiceInfo[]; default: string; + available?: boolean; } /** @deprecated Use ProviderVoicesResponse instead */