Skip to content

fix(voice): end a turn on a late STT final after a later END_OF_SPEECH - #7739

Merged
chenghao-mou merged 4 commits into
livekit:mainfrom
acharan-tech-200037:fix/stt-late-final-ends-turn
Oct 30, 2026
Merged

chenghao-mou merged 4 commits into
livekit:mainfrom
acharan-tech-200037:fix/stt-late-final-ends-turn

Conversation

@acharan-tech-200037

Copy link
Copy Markdown
Contributor

Fixes #7699

With turn_detection="stt", a final transcript only runs end-of-turn detection while _user_turn_committed is set, and every commit clears it. If the user resumes between an END_OF_SPEECH and its FINAL_TRANSCRIPT (Sarvam realtime sends END a few hundred ms before the final), the next END_OF_SPEECH commits the earlier text. That utterance's final then arrives after the commit, finds the latch cleared and waits for another END_OF_SPEECH, so every later turn is committed one utterance late.

Change: track the end of a turn in STT mode with a separate flag, _stt_turn_ended. It is set when a turn ends, cleared on START_OF_SPEECH, and lets a final that arrives in between run end-of-turn detection. _user_turn_committed is unchanged, so the VAD end-of-speech path and the other turn detection modes behave as before. The flag is only read in STT mode, so switching to manual turn detection right after a turn doesn't let a final end the next one.

Tests (events sent through _on_stt_event, which every STT stream event passes through):

  • test_stt_final_after_a_later_end_of_speech_still_ends_a_turn replays the sequence from the issue. With no endpointing delay, the late final commits as its own turn; on main it is stranded. With a 0.5 s delay it joins the pending turn. In both cases the next turn commits on its own END_OF_SPEECH.
  • test_stt_pending_end_of_speech_survives_a_resume: an END_OF_SPEECH not yet committed still lets a delayed VAD end commit its final when the user resumes (same as main).
  • test_stt_turn_is_not_ended_by_vad_after_the_user_resumes: after the user resumes, a final that follows a VAD pause doesn't end the turn early.
  • test_stt_turn_end_does_not_carry_into_manual_turn_detection: after switching to manual turn detection, a final doesn't end the turn.

Removing any one part of the change fails one of these tests. test_transcription_delay_anchor.py builds AudioRecognition by hand, so its helper sets the new attribute.

With turn_detection="stt", a final transcript only ran end-of-turn detection
while _user_turn_committed was set, and every commit cleared it. When the user
resumed between an END_OF_SPEECH and its final, the next END_OF_SPEECH
committed the earlier text, and that utterance's final, arriving after the
commit, waited for another END_OF_SPEECH, leaving every later turn one
utterance late.

Track the end of a turn in STT mode separately (_stt_turn_ended, set when a
turn ends and cleared on START_OF_SPEECH) and let a final run end-of-turn
detection while it is set. _user_turn_committed is unchanged, so the VAD
end-of-speech path and the other modes behave as before.

Fixes livekit#7699
@acharan-tech-200037
acharan-tech-200037 requested a review from a team as a code owner October 11, 2026 12:27
devin-ai-integration[bot]

This comment was marked as resolved.

_stt_turn_ended was set whenever a turn ended, so a VAD-driven commit armed it
for an utterance the user had already resumed, and clear_user_turn left it set.
Set it on the STT's END_OF_SPEECH instead, and clear it on START_OF_SPEECH,
on clear_user_turn and when the turn detection mode changes.
@acharan-tech-200037

Copy link
Copy Markdown
Contributor Author

Hi @longcw , @chenghao-mou this PR is ready for review and all checks are passing. Could you please review it when you have a chance? Thank you!

@chenghao-mou chenghao-mou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks for the PR!

@chenghao-mou
chenghao-mou merged commit 11c8ad8 into livekit:main Oct 30, 2026
15 checks passed
iamdgarcia added a commit to iamdgarcia/agents that referenced this pull request Oct 31, 2026
Rebased onto main (post livekit#7285, livekit#7739). Un-stacked from the silero VAD
branch (livekit#7735) so this PR carries only its own changes. Pipelines with
manual_flush keep the livekit#7285 flush-first path; the reuse shortcut applies
to plain streaming pipelines where a second final will never arrive.
chenghao-mou added a commit that referenced this pull request Nov 2, 2026
Port #7795 and #7796 onto the shared STT
connection coordinator, with #7739 that they build on. Each
provider connection restarts the input clock. RTZR detects idle in the send
loop, and Gladia and Reson8 re-anchor each session on its first frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

STT turn detection: a final transcript that arrives after a later END_OF_SPEECH leaves every later turn committed one utterance late

2 participants