feat(elevenlabs): secondary languages and language detection for realtime STT - #6902
Open
captainbanan wants to merge 2 commits into
Open
feat(elevenlabs): secondary languages and language detection for realtime STT#6902captainbanan wants to merge 2 commits into
captainbanan wants to merge 2 commits into
Conversation
|
captainbanana seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
…time STT Scribe v2 realtime takes a `secondary_languages` list next to `language_code`, so a session can keep its primary language hinted and still recognize a second one — the case where a speaker drops into another language mid-call. The plugin had no way to send it. Codes are normalized to what the API accepts: it takes ISO-639-1/639-3 and rejects everything else, including the region-tagged tags `LanguageCode` produces. Sending it alone would not be observable, because the detected language never reached `SpeechData.language`. The plugin already asks for language detection whenever no `language_code` is pinned, but the detected language rides only on the delayed `committed_transcript_with_timestamps` copy of a commit, which the plugin drops unless `include_timestamps` is set — so every transcript was labelled with the pinned language, or the hardcoded "en" fallback in auto-detect mode. That is the value `MultilingualModel.unlikely_threshold()` keys its per-language EOU threshold on. The final now comes from whichever copy carries what the session asked for, and `include_language_detection` is an explicit option instead of an implicit one. Word timings are still attached only when `include_timestamps` was requested, so enabling detection does not start handing out `TimedString`s the caller never asked for. Verified against the live API: with a language pinned and detection on, an English utterance followed by a Russian one comes back labelled en / ru instead of en / en, and the delayed copy arrives within ~1ms of the plain one, so the detection-only path costs no latency (the ~80ms lag people associate with that copy comes from include_timestamps, which is unchanged here).
captainbanan
force-pushed
the
feat/elevenlabs-secondary-languages
branch
from
August 19, 2026 17:15
a2975f8 to
38f4e3d
Compare
`_wire_language` was applied to `secondary_languages` only, so the primary `language_code` still went out with whatever region `LanguageCode` preserved. The realtime API rejects region-tagged codes for the primary language too, and the rejected session stays connected and silent, so `language_code="en-US"` produced a call that never transcribed. Both params now go through the same normalization, and the primary one is percent-encoded like the rest. This was listed as out of scope in the PR description; the helper the PR adds makes it a one-line fix, so it comes along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What
Two additions to the ElevenLabs realtime STT plugin, and the fix that makes the second one observable:
secondary_languages— passed through to Scribe v2 realtime. Declares extra languages that may appear in the audio while keeping the primary one hinted. Codes are normalized to what the API accepts: it takes ISO-639-1/639-3 and rejects everything else, including the region-tagged tagsLanguageCodeproduces (ru-RU), so the region comes off before the connect URL. The primarylanguage_codegoes through the same normalization — it was sent verbatim before, and the API rejects a region-tagged primary code too, solanguage_code="en-US"produced a session that stayed connected and never transcribed.include_language_detection— now an explicit option. It was implicit: on when nolanguage_codewas set, off otherwise, with no way to change either.SpeechData.language. Detail below.Why
Sessions where the speaker switches language mid-call — a tutor whose learner drops into their native language for a sentence, support lines in bilingual regions. Pinning
language_codebiases recognition toward the expected language, which you want, but there was no way to say "and they might also speak Russian".The detected language matters beyond the transcript text:
MultilingualModel.unlikely_threshold()keys the per-language EOU threshold onSpeechData.language, so a wrong label silently picks the wrong turn-detection threshold (or, for a language outsidelanguages.json,supports_language()returns False and the EOU model is skipped entirely).The bug
The plugin asks for language detection whenever no
language_codeis pinned, but the server puts the detected language only on the delayedcommitted_transcript_with_timestampscopy of the commit — and the plugin ignores that copy unlessinclude_timestamps=True. So the detection it requested was thrown away, and every transcript came out labelled with the pinned language, or the hardcodedLanguageCode("en")fallback in auto-detect mode.Measured against the live API with
scribe_v2_realtime, one English utterance followed by one Russian one:committed_transcriptcommitted_transcript_with_timestampslanguage_code=enlangabsentlangabsentlanguage_code=en&include_language_detection=truelangabsentlang='en', thenlang='ru'language_code(what the plugin sends today)langabsentlang='en', thenlang='ru'After the change, the final comes from whichever copy carries what the session asked for:
include_timestamps or include_language_detection. Word timings are still only attached wheninclude_timestampswas requested, so enabling detection doesn't start handing outTimedStrings the caller never asked for and thealigned_transcriptcapability stays truthful.Latency: none for the detection-only path — the timestamped copy arrives within ~1ms of the plain one (0.855s vs 0.856s, 1.888s vs 1.889s in the run above). The ~80ms lag people associate with that copy comes from
include_timestamps=true, whose behavior is unchanged here.Serialization
secondary_languagesis a repeated query param. Verified against the live API —secondary_languages=ru&secondary_languages=esis echoed back as"secondary_languages": ["ru", "es"]insession_started.config, while a comma-joined or JSON-encoded value is rejected withinvalid_request: Invalid language code received: 'ru,es'.Both new options are realtime-only and warn + no-op on batch models, matching how
server_vadandprevious_textbehave.Verification
End-to-end against the live API with the patched plugin, same two utterances:
uv run pytest --unit→ 1987 passed,make checkclean. 12 new tests intests/test_plugin_elevenlabs_stt.pycover the query params, the normalization of both the primary and the secondary codes, the batch-model warning, and both the pinned-language and auto-detect final-transcript paths —uv run pytest tests/test_plugin_elevenlabs_stt.py→ 37 passed. That module is markedplugin("elevenlabs"), so it runs in thelivekit-plugins-elevenlabsjob rather than under--unit, and that job is gated onhead.repo.fork == false— it is skipped on this PR.Out of scope on purpose
One neighbouring problem this PR deliberately leaves alone, found while testing it and ready to send separately if you want it:
_process_stream_eventnever reaches the caller —recv_taskcatches everything it raises and only logs, so a session the server rejects stays connected and silent for the rest of the call.