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 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-03 14:17:00 +00:00
parent 1e177a6d5c
commit a22fe5c1bc
5 changed files with 44 additions and 18 deletions

View file

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

View file

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

View file

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

View file

@ -376,22 +376,35 @@ class ApiClient {
stability?: number,
similarityBoost?: number,
): Promise<Blob> {
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

View file

@ -85,6 +85,7 @@ export interface ProviderVoicesResponse {
provider: string;
voices: VoiceInfo[];
default: string;
available?: boolean;
}
/** @deprecated Use ProviderVoicesResponse instead */