{"record":{"id":"14ab7e6ce796765d","repo":"odysseus-dev/odysseus","slug":"browser-tts-not-supported","errorCode":null,"errorMessage":"Browser TTS not supported","messagePattern":"Browser TTS not supported","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"static/js/settings.js","lineNumber":921,"sourceCode":"    var previewPlaying = false;\n    function resetPreview() { previewPlaying = false; previewBtn.textContent = 'Preview'; previewBtn.style.borderColor = ''; }\n\n    previewBtn.addEventListener('click', async function() {\n      if (previewPlaying) {\n        if (previewAudio) { previewAudio.pause(); previewAudio = null; }\n        window.speechSynthesis.cancel();\n        resetPreview(); return;\n      }\n      var prov = provSel.value;\n      if (prov === 'disabled') {\n        ttsMsg.textContent = 'Select a provider first'; ttsMsg.style.color = 'var(--red, #e55)';\n        setTimeout(function() { ttsMsg.textContent = ''; }, 2000); return;\n      }\n      var testText = 'Hello, this is a test of text to speech.';\n      previewPlaying = true; previewBtn.textContent = 'Loading...';\n      try {\n        if (prov === 'browser') {\n          if (!('speechSynthesis' in window)) throw new Error('Browser TTS not supported');\n          var utt = new SpeechSynthesisUtterance(testText);\n          var voiceVal = getVoice();\n          if (voiceVal) {\n            var voices = window.speechSynthesis.getVoices();\n            var target = voiceVal.toLowerCase();\n            var match = voices.find(function(v) { return v.name.toLowerCase() === target; }) ||\n                        voices.find(function(v) { return v.name.toLowerCase().includes(target); });\n            if (match) utt.voice = match;\n          }\n          utt.rate = parseFloat(speedSelect.value) || 1;\n          previewBtn.textContent = 'Stop'; previewBtn.style.borderColor = 'var(--red, #e55)';\n          await new Promise(function(resolve, reject) {\n            utt.onend = resolve;\n            utt.onerror = function(e) { reject(new Error('Browser TTS: ' + e.error)); };\n            window.speechSynthesis.speak(utt);\n          });\n        } else {\n          var res = await fetch('/api/tts/synthesize', {","sourceCodeStart":903,"sourceCodeEnd":939,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/settings.js#L903-L939","documentation":"Thrown when the user clicks TTS preview with provider 'browser' but window.speechSynthesis is absent. It is a hard feature-detection failure, not a network or config error — the browser genuinely lacks the Web Speech API.","triggerScenarios":"Selecting the 'browser' TTS provider and clicking preview in any browser without speechSynthesis: older/NIghtly builds, some WebViews/embedded browsers (Electron without the flag, kiosk WebViews), or speechSynthesis removed at runtime.","commonSituations":"App loaded inside an embedded WebView; user on an exotic or locked-down browser; CI/screenshot environments; the API exists but getVoices() returns empty on some platforms (related but distinct failure).","solutions":["Hide or disable the 'browser' provider option when 'speechSynthesis' in window is false, before the user can pick it","Fall back to the server-side /api/tts/synthesize provider for these browsers","Detect once at settings-render time and show a hint instead of failing at preview time","Example guard: if (!('speechSynthesis' in window)) disableOption()"],"exampleFix":"// before\n          if (!('speechSynthesis' in window)) throw new Error('Browser TTS not supported');\n\n// after\n          if (!('speechSynthesis' in window)) {\n            ttsMsg.textContent = 'Browser TTS not supported — use a server TTS provider';\n            ttsMsg.style.color = 'var(--red, #e55)';\n            return;\n          }","handlingStrategy":"validation","validationCode":"const browserTtsAvailable = 'speechSynthesis' in window && typeof window.speechSynthesis.speak === 'function';\nif (!browserTtsAvailable) { /* disable 'browser' provider option in provSel */ }","typeGuard":"function supportsBrowserTts() {\n  return typeof window !== 'undefined' &&\n    'speechSynthesis' in window &&\n    typeof window.speechSynthesis.speak === 'function';\n}","tryCatchPattern":"try { if (!supportsBrowserTts()) { showHint('Browser TTS unsupported — pick a server provider'); return; } ... } catch (e) { ttsMsg.textContent = 'Preview failed: ' + e.message; }","preventionTips":["Feature-detect at render time, not at click time","Offer the server TTS provider as fallback","Test in the lowest-common WebView you support","Check getVoices() non-empty before promising browser TTS"],"tags":["tts","feature-detection","browser-support","settings"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}