Skip to content

fix(room_io): key user transcript segments on the transcription item_id - #6731

Closed
RGB-loop wants to merge 1 commit into
livekit:mainfrom
RGB-loop:fix/user-transcript-segment-item-id
Closed

RGB-loop wants to merge 1 commit into
livekit:mainfrom
RGB-loop:fix/user-transcript-segment-item-id

Conversation

@RGB-loop

@RGB-loop RGB-loop commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

A realtime provider may finalize the same input-audio transcription item more than once, each time with a longer revision of the transcript. RoomIO ignored ev.item_id when forwarding user transcripts, so every final opened a fresh segment id and clients rendered N stacked segments, each containing all of its predecessors β€” while the chat context correctly collapsed the revisions into one message via _upsert_item.

Derive the segment id from the item id when present, so a repeated final for the same item reuses the segment id and clients revise the segment in place. Events without an item_id (the STT path) keep the previous behavior. An open segment with no visible text yet (e.g. the empty interim emitted on input_speech_stopped) is re-keyed in place instead of flushed, so no empty orphan segment is published.

Fixes #6710

A realtime provider may finalize the same input-audio transcription item
more than once, each time with a longer revision of the transcript.
RoomIO ignored ev.item_id when forwarding user transcripts, so every
final opened a fresh segment id and clients rendered N stacked segments,
each containing all of its predecessors β€” while the chat context
correctly collapsed the revisions into one message via _upsert_item.

Derive the segment id from the item id when present, so a repeated final
for the same item reuses the segment id and clients revise the segment
in place. Events without an item_id (the STT path) keep the previous
behavior. An open segment with no visible text yet (e.g. the empty
interim emitted on input_speech_stopped) is re-keyed in place instead of
flushed, so no empty orphan segment is published.

Fixes livekit#6710

Generated with AI

Co-Authored-By: AI <ai@example.com>
@RGB-loop
RGB-loop requested a review from a team as a code owner August 6, 2026 16:30
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


jingyan 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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +270 to +273
else:
# nothing visible was published yet: re-key the open segment
self._current_id = segment_id
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 An empty user caption can be left permanently unfinished on clients using the older captions channel

The still-open caption is silently given a new identity (self._current_id = segment_id at livekit-agents/livekit/agents/voice/room_io/_output.py:272) even though an empty version of it was already sent to clients, so that first empty caption is never closed out and lingers as an unfinished entry.

Impact: Clients using the legacy captions channel can accumulate a stale, never-finalized empty caption for every user turn in realtime sessions.

Why the legacy sink differs from the stream sink: it publishes empty interims

The realtime path emits UserInputTranscribedEvent(transcript="", is_final=False) on input_speech_stopped with no item_id (livekit-agents/livekit/agents/voice/agent_activity.py:1922). _forward_user_transcript forwards it to capture_text("").

  • _ParticipantStreamTranscriptionOutput.capture_text returns early when the cleaned text is empty (livekit-agents/livekit/agents/voice/room_io/_output.py:520-521), so nothing was ever written and re-keying the open segment is safe β€” that's what the new test asserts.
  • _ParticipantLegacyTranscriptionOutput.capture_text has no such guard: it always calls _publish_transcription(self._current_id, "", final=False) (livekit-agents/livekit/agents/voice/room_io/_output.py:301). So by the time the next event carries an item_id, an interim segment with the random SG_... id has already been published, yet _pushed_text is "" so set_segment_id takes the "nothing visible was published yet" branch and swaps _current_id. The originally published segment is never sent again with final=True.

Before this PR the same _current_id was kept for the whole turn, so the empty interim was always finalized.

Prompt for agents
In _ParticipantLegacyTranscriptionOutput (livekit-agents/livekit/agents/voice/room_io/_output.py), set_segment_id uses an empty _pushed_text to decide that "nothing visible was published yet" and re-keys the open segment in place. That inference holds for _ParticipantStreamTranscriptionOutput (its capture_text returns before creating a writer when the cleaned text is empty) but not for the legacy sink: legacy capture_text unconditionally publishes an interim rtc.TranscriptionSegment, including for the empty interim the realtime path emits on input_speech_stopped. Re-keying then abandons that already-published segment, which never receives a final=True update. Consider tracking whether an interim was actually published for the current segment (a flag set in _publish_transcription/capture_text and cleared in _reset_state) and using that flag instead of _pushed_text to choose between re-keying and flushing, or skip publishing legacy interims whose visible text is empty.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

@longcw

longcw commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

closing in favor of #6729

@longcw longcw closed this Aug 7, 2026
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.

room_io: user transcript segments ignore item_id, so an in-place item revision renders as N separate segments

3 participants