odysseus-dev/odysseus · error · Error
Synthesis failed
Error message
Synthesis failed
What it means
Fallback error when server-side TTS preview fails without a structured detail message. The code POSTs {text, format:'audio'} to /api/tts/synthesize; on non-OK it tries to read err.detail?.message and falls back to 'Synthesis failed' when the body isn't the expected JSON shape.
Source
Thrown at static/js/settings.js:944
var target = voiceVal.toLowerCase();
var match = voices.find(function(v) { return v.name.toLowerCase() === target; }) ||
voices.find(function(v) { return v.name.toLowerCase().includes(target); });
if (match) utt.voice = match;
}
utt.rate = parseFloat(speedSelect.value) || 1;
previewBtn.textContent = 'Stop'; previewBtn.style.borderColor = 'var(--red, #e55)';
await new Promise(function(resolve, reject) {
utt.onend = resolve;
utt.onerror = function(e) { reject(new Error('Browser TTS: ' + e.error)); };
window.speechSynthesis.speak(utt);
});
} else {
var res = await fetch('/api/tts/synthesize', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: testText, format: 'audio' })
});
if (!res.ok) { var err = await res.json().catch(function() { return {}; }); throw new Error(err.detail?.message || 'Synthesis failed'); }
var blob = await res.blob();
var url = URL.createObjectURL(blob);
previewAudio = new Audio(url);
previewBtn.textContent = 'Stop'; previewBtn.style.borderColor = 'var(--red, #e55)';
await new Promise(function(resolve, reject) {
previewAudio.onended = function() { URL.revokeObjectURL(url); previewAudio = null; resolve(); };
previewAudio.onerror = function() { URL.revokeObjectURL(url); previewAudio = null; reject(new Error('Playback failed')); };
previewAudio.play().catch(reject);
});
}
} catch (e) {
ttsMsg.textContent = 'Preview failed: ' + e.message; ttsMsg.style.color = 'var(--red, #e55)';
setTimeout(function() { ttsMsg.textContent = ''; }, 3000);
} finally {
resetPreview();
}
});
}View on GitHub (pinned to f9235ebbf1)
Solutions
- Check the response status and raw body of the failing request in devtools
- Verify the TTS provider credentials/config on the server
- If a proxy returns HTML/empty bodies, fix the proxy or make the client accept text bodies
- Improve the fallback to include res.status so users see which failure occurred
Example fix
// before
if (!res.ok) { var err = await res.json().catch(function() { return {}; }); throw new Error(err.detail?.message || 'Synthesis failed'); }
// after
if (!res.ok) {
var err = await res.json().catch(function() { return {}; });
throw new Error(err.detail?.message || 'Synthesis failed (HTTP ' + res.status + ')');
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!prov || prov === 'disabled') return;
if (!navigator.onLine) { ttsMsg.textContent = 'Offline'; return; } Try / catch
try { const res = await fetch('/api/tts/synthesize', {...}); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail?.message || `Synthesis failed (HTTP ${res.status})`); } ... } catch (e) { ttsMsg.textContent = 'Preview failed: ' + e.message; } Prevention
- Always pair the fallback message with res.status
- Verify server TTS provider credentials before offering preview
- Handle proxy/HTML error bodies via the json().catch fallback
- Revoke object URLs in a finally to avoid leaks on repeated failures
When it happens
Trigger: POST /api/tts/synthesize returns non-2xx with an empty or differently-shaped body (plain-text 500 traceback, HTML error page from a proxy, 502/504 from a gateway) — err.detail?.message is undefined so the generic message is used.
Common situations: TTS provider (e.g. ElevenLabs/other) API key missing or invalid; provider quota exhausted; reverse proxy intercepting the request; backend TTS subsystem not configured; network layer returning HTML error pages.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/06b3b0efc452d924.
Report an issue: GitHub.