Skip to content

fix(voice): make STT the user_state source when turn_detection is stt - #6641

Open
biztex wants to merge 3 commits into
livekit:mainfrom
biztex:fix/stt-turn-detection-user-state
Open

fix(voice): make STT the user_state source when turn_detection is stt#6641
biztex wants to merge 3 commits into
livekit:mainfrom
biztex:fix/stt-turn-detection-user-state

Conversation

@biztex

@biztex biztex commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #5580. Related: #5510 (the noisy-telephony pain this gives an escape hatch for).

Problem

With turn_detection="stt", both VAD and STT drive user_state. In noisy environments Silero VAD flips it to "speaking" on background noise the STT never transcribes, breaking everything keyed on user state — user_away_timeout, filler triggering, presence logic. The only workaround is vad=None, which also gives up VAD-based interruption sensing; the two concerns are independent, as the issue lays out.

Change

This is the issue's Option A intent, implemented one layer higher than proposed — and that placement is the important part. The activity's on_start_of_speech / on_end_of_speech hooks receive ev: VADEvent | None, where None means STT-sourced. Only the _update_user_state writes are gated: when the user opted into STT-driven turn detection, VAD-sourced events skip the state update and STT events drive it. Gating the whole VAD branch in audio_recognition (as the earlier stale attempt at this did) would have silently disabled the adaptive-interruption overlap wiring, pause/false-interruption timers, and endpointing that ride on the same hooks — all of which still run on VAD events here, so VAD keeps its interruption-sensing role exactly as the issue requests. No new configuration; "vad"/default modes are byte-identical.

Tests

New hermetic tests/test_stt_user_state_source.py on the fake-session harness: in stt mode, VAD start/end events leave user_state at "listening" (the noise scenario) while STT-sourced events flip it normally; in default mode VAD drives it exactly as before.

With turn_detection=stt, both VAD and STT wrote user_state: in noisy
environments VAD flips it to speaking on background noise the STT never
transcribes, breaking everything keyed on user state (user_away_timeout,
filler triggering). The only workaround, vad=None, also gives up VAD-based
interruption sensing - the two concerns are independent.

Gate only the user_state writes in the on_start/end_of_speech hooks: when the
user has opted into STT-driven turn detection, VAD-sourced events (ev is not
None) skip the state update while STT speech events (ev is None) drive it.
Every other VAD role is untouched - interruption sensing, endpointing,
overlap tracking, pause/false-interruption timers all still run on VAD
events, which is exactly the separation the issue asks for. Default and vad
modes are byte-identical.

Fixes livekit#5580
@biztex
biztex requested a review from a team as a code owner July 31, 2026 07:05
devin-ai-integration[bot]

This comment was marked as resolved.

…ng state

in stt mode, "speaking" can be entered by writers the STT will never clear:
claim_user_turn re-derives it on release from the VAD-driven silence event
(background noise during a text turn), and update_options can switch to stt
mid-speech. Dropping every VAD end-of-speech left the state stuck at
"speaking" with the away timer never re-armed. Track whether an STT-driven
segment is open (paired ev=None hook calls) and let the VAD end clear the
state only when it is not - an STT-authored "speaking" still ends on the
STT end-of-speech alone.
devin-ai-integration[bot]

This comment was marked as resolved.

…-utterance

the stt-driven speaking segment was only closed by an STT end-of-speech, but
two paths destroy the stream with that event still pending: the pump
recreating the node after an APIError, and _clear_user_turn resetting the
stt. The segment now closes on both (mirroring the vad-task teardown), so
user_state cannot stay "speaking" with the away timer unarmed and the
user_speaking span open until a later utterance is transcribed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow decoupling user_state source from VAD when STT emits speech events

1 participant