From 24c01bd76db65978c015dd0f489eec076769d75c Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:57:10 +0200 Subject: [PATCH] fix: stop voice call hanging in "speaking" when TTS fails (#30372) When the configured TTS provider fails during a voice call, the text answer arrives but the overlay stays in "speaking" with no audio and no error until the user taps to interrupt. The sentence that failed never reaches the audio cache, so the playback loop re-queues it every 200 ms forever. A failed sentence now marks its message as failed. The playback loop drops that message's unplayed sentences, the rest of the turn requests no more TTS, and the overlay returns to listening once the text finishes. The OpenAI-compatible path now shows the provider error once per turn, the same way Read Aloud and the Kokoro path already do. The next turn tries TTS again. Failure is tracked per message so an outage (the report shows 16 parallel requests all failing) costs one toast and no further requests. The trade-off is that a one-off failure mutes the rest of that reply. Verified with the real fetch/playback code in a harness: base loops forever with no toast; with the fix, one toast, the loop ends, later sentences are not requested, a new turn plays normally, and a late failure from a previous turn does not affect the next one. Fixes #30052 --- .../chat/MessageInput/CallOverlay.svelte | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/MessageInput/CallOverlay.svelte b/src/lib/components/chat/MessageInput/CallOverlay.svelte index 6227c20359..b4379b57f9 100644 --- a/src/lib/components/chat/MessageInput/CallOverlay.svelte +++ b/src/lib/components/chat/MessageInput/CallOverlay.svelte @@ -379,6 +379,7 @@ }; let finishedMessages = {}; + let failedMessages = {}; let currentMessageId = null; let currentUtterance: SpeechSynthesisUtterance | null = null; @@ -502,7 +503,9 @@ const emojiCache = new Map(); const fetchAudio = async (content) => { - if (!audioCache.has(content)) { + const id = currentMessageId; + + if (!audioCache.has(content) && !failedMessages[id]) { try { // Set the emoji for the content if needed if ($settings?.showEmojiInCall ?? false) { @@ -535,6 +538,10 @@ const res = await synthesizeOpenAISpeech(localStorage.token, getVoiceId(), content).catch( (error) => { console.error(error); + if (!failedMessages[id]) { + failedMessages[id] = true; + toast.error(`${error}`); + } return null; } ); @@ -550,6 +557,10 @@ } catch (error) { console.error('Error synthesizing speech:', error); } + + if (!audioCache.has(content)) { + failedMessages[id] = true; + } } return audioCache.get(content); @@ -591,7 +602,7 @@ } else { await speakSpeechSynthesisHandler(content); } - } else { + } else if (!failedMessages[id]) { // If not available in the cache, push it back to the queue and delay messages[id].unshift(content); // Re-queue the content at the start console.log(`Audio for "${content}" not yet available in the cache, re-queued...`);