From 36f129089b3115ebde8a11f7cf82ef4831beadcf Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 06:54:16 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8A=20fix:=20Autoplay=20Latest=20Messa?= =?UTF-8?q?ge=20for=20Browser=20TTS=20and=20short=20responses=20(#15347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ”Š fix: Autoplay Latest Message for Browser TTS and short responses Automatic playback never reached the Web Speech API and dropped audio for short responses. Two independent defects: Autoplay ignored the selected TTS engine. `ChatForm` mounted `StreamAudio` whenever automatic playback was on, and that component always POSTs to `/api/files/speech/tts`, so the Browser engine silently fell through to the external endpoint and `speechSynthesis.speak()` was never called. The engine-aware `useTextToSpeech` hook had no consumers. Autoplay now selects its driver by engine the same way `MessageAudio` does, with the gate shared between both drivers so they trigger on identical terms. `MediaSourceAppender` stranded its queue. `tryAppendNextChunk` only ran from `addData` and `updateend`, while `sourceopen` โ€” which fires only once a media element attaches the object URL โ€” created the `SourceBuffer` without draining what had queued up behind it. A response read in full before that event left every chunk in the queue with no `updateend` to ever restart the drain, so the element sat at `readyState: 0` and never played. Short responses lost the race; longer ones streamed past `sourceopen` and worked. The handler now drains, and `close()` (previously never called) ends the stream once the queue empties. Also in the same path: the error branch had its timeout comparison inverted, so real failures were logged as timeouts and swallowed; and the non-MSE fallback that hands the element a finished blob lived inside the cache-only branch, leaving browsers without MSE support for `audio/mpeg` with no audio at all unless Cache TTS happened to be enabled. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0184N7d9kgpdqmbCb9ujMmNX * ๐Ÿงน style: Sort imports in autoplay files `npm run sort-imports:check` on the changed files. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0184N7d9kgpdqmbCb9ujMmNX * ๐Ÿ”‡ fix: End the media source when no audio ever arrives Codex P2 on #15347. The `hasAppended` guard in `tryEndOfStream` was defensive against a throw that cannot happen: `endOfStream()` only raises `InvalidStateError` while the source is not open or a buffer is updating, both of which the remaining guards already cover. Nothing about an empty buffer throws. The guard's actual effect was to strand the two cases it was meant to protect. A TTS response that completes with zero bytes, or a read timeout that fires before the first byte, both reach `close()` with the MediaSource URL already attached to the element โ€” and left it `open` forever, so the element waited on a source that could never receive data and kept its streaming resources --- .../components/Chat/Input/AutoPlayAudio.tsx | 34 ++++ .../components/Chat/Input/BrowserAudio.tsx | 57 ++++++ client/src/components/Chat/Input/ChatForm.tsx | 4 +- .../src/components/Chat/Input/StreamAudio.tsx | 85 ++++----- .../Input/__tests__/AutoPlayAudio.spec.tsx | 56 ++++++ .../Input/__tests__/BrowserAudio.spec.tsx | 149 ++++++++++++++++ client/src/hooks/Audio/MediaSourceAppender.ts | 53 +++++- .../__tests__/MediaSourceAppender.spec.ts | 164 ++++++++++++++++++ client/src/hooks/Audio/index.ts | 1 + client/src/hooks/Audio/useAutoplayTrigger.ts | 37 ++++ .../src/hooks/Input/useTextToSpeechBrowser.ts | 75 ++++---- 11 files changed, 622 insertions(+), 93 deletions(-) create mode 100644 client/src/components/Chat/Input/AutoPlayAudio.tsx create mode 100644 client/src/components/Chat/Input/BrowserAudio.tsx create mode 100644 client/src/components/Chat/Input/__tests__/AutoPlayAudio.spec.tsx create mode 100644 client/src/components/Chat/Input/__tests__/BrowserAudio.spec.tsx create mode 100644 client/src/hooks/Audio/__tests__/MediaSourceAppender.spec.ts create mode 100644 client/src/hooks/Audio/useAutoplayTrigger.ts diff --git a/client/src/components/Chat/Input/AutoPlayAudio.tsx b/client/src/components/Chat/Input/AutoPlayAudio.tsx new file mode 100644 index 0000000000..7cc5e4a66b --- /dev/null +++ b/client/src/components/Chat/Input/AutoPlayAudio.tsx @@ -0,0 +1,34 @@ +import { memo } from 'react'; +import { useRecoilValue } from 'recoil'; +import BrowserAudio from './BrowserAudio'; +import StreamAudio from './StreamAudio'; +import { TTSEndpoints } from '~/common'; +import store from '~/store'; + +/** + * Mount point for "Autoplay Latest Message". Playback has to honor the selected TTS engine + * the same way the message speaker button does, otherwise the Browser engine silently falls + * through to the external endpoint and never reaches the Web Speech API. + */ +function AutoPlayAudio({ index = 0 }) { + const engineTTS = useRecoilValue(store.engineTTS); + const speechSettingsInitialized = useRecoilValue(store.speechSettingsInitialized); + + if (!speechSettingsInitialized) { + return null; + } + + const AutoPlayComponents = { + [TTSEndpoints.browser]: BrowserAudio, + [TTSEndpoints.external]: StreamAudio, + }; + + const SelectedAutoPlay = AutoPlayComponents[engineTTS]; + if (!SelectedAutoPlay) { + return null; + } + + return ; +} + +export default memo(AutoPlayAudio); diff --git a/client/src/components/Chat/Input/BrowserAudio.tsx b/client/src/components/Chat/Input/BrowserAudio.tsx new file mode 100644 index 0000000000..262310f7b2 --- /dev/null +++ b/client/src/components/Chat/Input/BrowserAudio.tsx @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { useParams } from 'react-router-dom'; +import { parseTextParts } from 'librechat-data-provider'; +import useTextToSpeechBrowser from '~/hooks/Input/useTextToSpeechBrowser'; +import useAutoplayTrigger from '~/hooks/Audio/useAutoplayTrigger'; +import { logger } from '~/utils'; +import store from '~/store'; + +/** + * Autoplay driver for the Browser TTS engine. `StreamAudio` covers the external engine by + * streaming server audio into the global `