From ce54b1df23fb82435125ee97da2bd0c3fca75f93 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:32:05 +0100 Subject: [PATCH] perf: guard TTS sentence parsing behind showCallOverlay check (#22195) The chatCompletionEventHandler runs getMessageContentParts() and removeAllDetails() on every streaming token to extract sentences for real-time TTS dispatch via CustomEvent('chat'). These functions perform multiple O(n) regex passes over the full accumulated message content, resulting in O(n^2) total work over a streaming response. The only consumer of these events is CallOverlay.svelte, which is only mounted when showCallOverlay is true. Without the overlay open, the parsing runs but the dispatched events have no listeners. Wrap all three TTS parsing blocks in an if () guard so the expensive regex work is skipped entirely for the vast majority of users who are not using the voice call feature. --- src/lib/components/chat/Chat.svelte | 110 +++++++++++++++------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 052b44fcd..3d7bba0ab 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1567,27 +1567,29 @@ navigator.vibrate(5); } - // Emit chat event for TTS - const messageContentParts = getMessageContentParts( - removeAllDetails(message.content), - $config?.audio?.tts?.split_on ?? 'punctuation' - ); - messageContentParts.pop(); - - // dispatch only last sentence and make sure it hasn't been dispatched before - if ( - messageContentParts.length > 0 && - messageContentParts[messageContentParts.length - 1] !== message.lastSentence - ) { - message.lastSentence = messageContentParts[messageContentParts.length - 1]; - eventTarget.dispatchEvent( - new CustomEvent('chat', { - detail: { - id: message.id, - content: messageContentParts[messageContentParts.length - 1] - } - }) + // Emit chat event for TTS (only when call overlay is active) + if ($showCallOverlay) { + const messageContentParts = getMessageContentParts( + removeAllDetails(message.content), + $config?.audio?.tts?.split_on ?? 'punctuation' ); + messageContentParts.pop(); + + // dispatch only last sentence and make sure it hasn't been dispatched before + if ( + messageContentParts.length > 0 && + messageContentParts[messageContentParts.length - 1] !== message.lastSentence + ) { + message.lastSentence = messageContentParts[messageContentParts.length - 1]; + eventTarget.dispatchEvent( + new CustomEvent('chat', { + detail: { + id: message.id, + content: messageContentParts[messageContentParts.length - 1] + } + }) + ); + } } } } @@ -1601,27 +1603,29 @@ navigator.vibrate(5); } - // Emit chat event for TTS - const messageContentParts = getMessageContentParts( - removeAllDetails(message.content), - $config?.audio?.tts?.split_on ?? 'punctuation' - ); - messageContentParts.pop(); - - // dispatch only last sentence and make sure it hasn't been dispatched before - if ( - messageContentParts.length > 0 && - messageContentParts[messageContentParts.length - 1] !== message.lastSentence - ) { - message.lastSentence = messageContentParts[messageContentParts.length - 1]; - eventTarget.dispatchEvent( - new CustomEvent('chat', { - detail: { - id: message.id, - content: messageContentParts[messageContentParts.length - 1] - } - }) + // Emit chat event for TTS (only when call overlay is active) + if ($showCallOverlay) { + const messageContentParts = getMessageContentParts( + removeAllDetails(message.content), + $config?.audio?.tts?.split_on ?? 'punctuation' ); + messageContentParts.pop(); + + // dispatch only last sentence and make sure it hasn't been dispatched before + if ( + messageContentParts.length > 0 && + messageContentParts[messageContentParts.length - 1] !== message.lastSentence + ) { + message.lastSentence = messageContentParts[messageContentParts.length - 1]; + eventTarget.dispatchEvent( + new CustomEvent('chat', { + detail: { + id: message.id, + content: messageContentParts[messageContentParts.length - 1] + } + }) + ); + } } } @@ -1648,18 +1652,20 @@ document.getElementById(`speak-button-${message.id}`)?.click(); } - // Emit chat event for TTS - let lastMessageContentPart = - getMessageContentParts( - removeAllDetails(message.content), - $config?.audio?.tts?.split_on ?? 'punctuation' - )?.at(-1) ?? ''; - if (lastMessageContentPart) { - eventTarget.dispatchEvent( - new CustomEvent('chat', { - detail: { id: message.id, content: lastMessageContentPart } - }) - ); + // Emit chat event for TTS (only when call overlay is active) + if ($showCallOverlay) { + let lastMessageContentPart = + getMessageContentParts( + removeAllDetails(message.content), + $config?.audio?.tts?.split_on ?? 'punctuation' + )?.at(-1) ?? ''; + if (lastMessageContentPart) { + eventTarget.dispatchEvent( + new CustomEvent('chat', { + detail: { id: message.id, content: lastMessageContentPart } + }) + ); + } } eventTarget.dispatchEvent( new CustomEvent('chat:finish', {