Skip to content

fix(room_io): drop the previous participant's track when the transcription output switches participants - #7531

Closed
RaphaelFakhri wants to merge 2 commits into
livekit:mainfrom
RaphaelFakhri:fix/legacy-transcription-track-on-participant-switch
Closed

RaphaelFakhri wants to merge 2 commits into
livekit:mainfrom
RaphaelFakhri:fix/legacy-transcription-track-on-participant-switch

Conversation

@RaphaelFakhri

Copy link
Copy Markdown
Contributor

fix(room_io): drop the previous participant's track when the transcription output switches participants

Description

_ParticipantLegacyTranscriptionOutput.set_participant searches for the new
participant's microphone track only when _track_id is empty. After a switch, the
field still held the previous participant's track, so the search was skipped and
transcriptions were published with the new identity and the old track_sid.

set_participant now clears _track_id first, so the track is always resolved for
the participant that is being linked. The stream-based output already behaves this
way.

Testing

  • Added test_legacy_transcription_output_follows_the_new_participants_track in
    tests/test_room_io.py.
  • Without the change to _output.py the test fails ('TR_a' == 'TR_b'). With it,
    uv run pytest --unit -q tests/test_room_io.py passes (45 tests).
  • ruff check and ruff format --check pass.

Related issues

  • Open PR 7240 edits nearby lines of the same file and adds tests to
    tests/test_room_io.py; the two changes do not overlap in code.

@RaphaelFakhri
RaphaelFakhri requested a review from a team as a code owner September 29, 2026 10:33
@CLAassistant

CLAassistant commented Sep 29, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

devin-ai-integration[bot]

This comment was marked as resolved.

@RaphaelFakhri

Copy link
Copy Markdown
Contributor Author

This is valid. Clearing the track alone leaves the open segment to be finalized with the new participant's identity and track, or skipped when the new participant has no microphone.

Fixed in 34e01c4. set_participant now finalizes the open segment before it replaces the identity and track. flush captures the represented identity and track when it is called and passes them to the publish task, so the final segment goes out as the previous participant on the previous track even though the task runs later. The new tests switch participants mid-segment, with and without a microphone on the new participant, and fail without the change.

@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 new potential issue.

Devin Review

) -> None:
# finalize the open segment for the participant it was captured for, before the
# identity and track are replaced
self.flush()

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.

🔴 Stream final loses the previous speaker

When set_participant switches during an open user transcript, the legacy final targets the old speaker but the stream final targets the new one. The stream output changes identity before flushing and resets its stored text, leaving that final empty.

Learn more

The composite transcription output sends each user transcript through both legacy RTC transcription and a text stream. A participant switch now schedules the legacy final before changing its identity, but the stream implementation assigns the new identity before its own flush and resets _latest_text immediately after scheduling the final task. For non-delta user transcripts, that task reads the reset text and current identity when it runs, so it sends an empty final for the incoming speaker. The None branch also skips flushing the old stream entirely here.

Example: User A has an interim transcript hello. Switching to B finalizes hello for A via the legacy output; the stream output sends a final for B with an empty payload instead of finalizing A's stream.

Recommended fix: Update _ParticipantStreamTranscriptionOutput.set_participant to flush before replacing the identity, including when unsetting it. Snapshot the identity, track, segment ID, and latest text needed by _flush_task before resetting state; pass these snapshots into the asynchronous task.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.

2 participants