Repository navigation
🗣️ feat: First-Class gpt-transcribe and Live gpt-live-transcribe Speech-to-Text - #17190
Open
Nk-rodrigues wants to merge 1 commit into
Open
Nk-rodrigues wants to merge 1 commit into
Nk-rodrigues wants to merge 1 commit into
Conversation
…ch-to-Text Recorded transcription: - Send `prompt`, `keywords[]`, and `languages[]` to gpt-transcribe models and never the singular `language`; older models keep `language` and `prompt`. - The user's speech language narrows the admin `languages` list. - Responses carrying detected-language metadata still resolve to `text`. Live transcription (opt-in via `speech.stt.openai.realtime`): - New "Live" engine under Settings > Speech > Engine, offered only when the admin configures it; `speechTab.speechToText.engineSTT: realtime` defaults it. - POST /api/files/speech/stt/realtime exchanges the browser's WebRTC offer for the provider's answer using a 60s client secret, so no credential reaches the browser. Metered by the existing STT limiters and SSRF-guarded. - Deltas stream into the composer; Stop commits the buffer and the final transcript stays editable. Auto Transcribe, Auto Send, cancel, connecting and reconnecting states, and permission/connection/provider errors are handled. Shared silence detection moves to `hooks/Input/silence.ts`.
This was referenced Oct 16, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Related to #14503 (claimed in #14503 (comment)).
Speech-to-text could only send a finished recording with a singular
language. OpenAI's recommended transcription models don't fit that:gpt-transcribereplaceslanguagewithlanguages[]and addskeywords[], andgpt-live-transcribeonly works over a Realtime session. Setting either name asstt.openai.modeltherefore neither uses the new hints nor gives live dictation.Recorded dictation now builds the request from the model.
gpt-transcribeandgpt-live-transcribe(including snapshots) receive the admin's optionalprompt,keywords, andlanguagesand never the singularlanguage; when the user has picked a speech language,languagesnarrows to it. Older models on theopenaiprovider (Whisper,gpt-4o(-mini)-transcribe) keep the singularlanguageand also receive the optionalprompt; theazureOpenAIprovider is unchanged. A response that carries detected-language metadata still resolves to itstext.Live dictation is opt-in through a new
speech.stt.openai.realtimeblock. When it's configured, Settings > Speech > Engine offers a third option, "Live", andspeechTab.speechToText.engineSTT: 'realtime'can make it the default. A Live take streams transcript deltas into the composer while the user speaks. Stop, or the existing silence auto-stop, commits the audio, and the final transcript stays editable. Auto Send, Cancel, the elapsed timer, and the draft that was in the composer before recording behave as they do for External. The composer shows "Connecting" until audio can reach the provider and "Reconnecting" while WebRTC recovers. Permission, connection, and provider failures each get a toast and keep whatever text was already heard.Out of scope: STT usage accounting (#13308 adds it for all non-chat models) and
stream=truefor recorded clips (optional in the issue; Live already covers progressive text).How it works
The browser never holds a provider credential, not even an ephemeral one. It posts its WebRTC offer to LibreChat. The server uses the configured key to mint a 60-second client secret, spends that secret on the SDP exchange, and returns only the SDP answer. Media and transcript events then flow between the browser and the provider over WebRTC. Azure OpenAI's
/realtime/callsaccepts only ephemeral secrets, so this flow works there too.sequenceDiagram participant Browser participant API as LibreChat API participant Provider as OpenAI / Azure OpenAI Browser->>API: POST /api/files/speech/stt/realtime { sdp, language? } API->>Provider: POST /realtime/client_secrets (API key, transcription session) Provider->>API: ek_... (60 s) API->>Provider: POST /realtime/calls (offer, ephemeral secret) Provider->>API: SDP answer API->>Browser: { sdp } Browser->>Provider: microphone audio (WebRTC) Provider->>Browser: transcription delta / completed events (data channel "oai-events") Browser->>Provider: input_audio_buffer.commit on Stop or silenceThe session is transcription-only with
turn_detection: null.gpt-live-transcribehas no server VAD, and for older models this stops a pause from splitting one dictation into several items. The route sits behind JWT auth and the existing STT IP and user limiters. Both upstream calls go through the proxy config and the SSRF guard, which honorsstt.allowedAddresses. Failures come back as coded errors (stt_realtime_not_configured,stt_realtime_invalid_offer,stt_realtime_provider_error), and provider errors are logged withgetSafeErrorMetadata.Type of change
Testing
Tested environments/configuration:
npm run frontend,npm run backend) with MongoDB 8.0.20 in Docker, Node 24.16.0, macOSgpt-4o-mini-transcribeandgpt-4o-transcribeWhat I ran against the real API:
stt.openai.realtime: { url: 'https://<resource>.openai.azure.com/openai/v1', model: 'gpt-4o-mini-transcribe' }andengineSTT: 'realtime'. Settings listed Browser / External / Live with Live preselected. Each take went Connecting, then Listening once the data channel opened. Stop sent the commit, the transcript landed in the composer, and the mic re-enabled. 6 of 7 takes completed, each within 0.8 s of the commit. In the other one, Azure acknowledged the commit (committed,item.added,item.done) but sent no transcript. The take ended at the 15 s completion timeout with the "stopped unexpectedly" toast, and the composer stayed usable. A standalone WebRTC script outside LibreChat hit the same provider flakiness: 1 of 4client_secretscalls timed out with a 408.openaiprovider, withurlpointing at the Azure deployment's/audio/transcriptionsandmodel: 'gpt-4o-mini-transcribe': 200, and the transcript landed in the composer.azureOpenAIprovider withengineSTT: 'realtime'in the yaml. The speech config reportssttRealtime: false, Live isn't offered, the engine falls back to External, and the take transcribes through the unchanged Azure provider.POST /api/files/speech/stt/realtimereturns 404stt_realtime_not_configuredwhen live isn't configured, and 401 without a token.What I could not verify against the real API:
gpt-transcribeandgpt-live-transcribeweren't deployed on the Azure resource I had access to. Their request shapes (languages,keywords,prompt,delay) are covered by unit tests only. Withgpt-4o-mini-transcribeandturn_detection: null, Azure sent all deltas in a burst right after the commit. So the real-API runs exercise the transport, the commit, and finalization, but not deltas arriving while the user is still speaking; that rendering is covered by the mocked hook tests. Separately, this Azure resource returnedDeploymentNotFoundfor/openai/v1/audio/transcriptions, which is why step 2 used the deployments URL.Automated tests:
packages/api/src/files/transcription.spec.ts,packages/api/src/files/realtime.spec.ts,api/server/routes/files/speech/index.spec.js,client/src/hooks/Input/__tests__/useSpeechToTextRealtime.spec.tsx,client/src/hooks/Input/__tests__/silence.spec.ts,client/src/components/Nav/SettingsTabs/Speech/STT/__tests__/EngineSTTDropdown.spec.tsxSTTService.spec.js,getCustomConfigSpeech.spec.js,config.spec.ts,useDictation.spec.tsx,useSpeechToText.spec.ts,useSpeechSettingsInit.spec.tsxclient12108 of 12112 passed,api6346 of 6365 (18 skipped),packages/apiwith itstest:ciignore patterns 20824 of 20852 (25 skipped), andpackages/data-provider2884 of 2884. None of the failures involve files this PR changes. The 4clientfailures (balance and TokenUsage number formatting) come from this machine's en-IN locale grouping (31,00,000) and pass underLANG=en_US.UTF-8. The 3packages/api/src/agents/hooksprocess-group signal tests also fail ondev(73847f2) here. Theapifailure instreamTenant.spec.jsis a keepalive timing test, and that spec fails intermittently ondevhere too.npx tsc --noEmitpasses inclient,packages/api, andpackages/data-providernpm run static-checks -- --against upstream/dev --fullpasses (the unused npm packages check was skipped because depcheck isn't installed)Screenshots / recordings
Settings > Speech > Engine, before (
dev) and after (this branch, withstt.openai.realtimeconfigured):Live dictation is a new engine, so there is no "before". From left to right: connecting, listening, and the final transcript left in the composer.
Recording of the full flow (Azure
gpt-4o-mini-transcribe, so the text arrives after Stop):live-dictation.webm
Risk / compatibility
engineSTTalso accepts'realtime'. If a user's saved or admin-defaultrealtimeengine isn't offered by the server, the client falls back to External, or to Browser when External isn't available either.openai. Azure works through theopenaiprovider with an Azure/openai/v1realtime URL, as tested above. TheazureOpenAIprovider block doesn't enable Live.filtersrules for thetranscriptfield don't apply to them. Message filters still apply when the turn is sent. This is documented next to the new config inlibrechat.example.yaml.Checklist