Transcribe a Stage, gated to whoever holds the floor (LC-597) #568
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc597-stage-transcription"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes LC-597, with one acceptance criterion explicitly not met and explained at the bottom.
The gap
The Stage carries audio over the LiveKit SFU rather than the WebRTC mesh, so a speaker never appears in
Hub::voice_room_usersandrequire_participantrefused them. Transcription reached DMs, voice channels, and huddles; the one broadcast surface, where an accessible transcript matters most, had none.Gate
require_participantnow falls through to a newis_stage_speakerfor non-DM rooms. The mesh check runs first: it is in-memory and already covers every other surface, so only a caller it rejects pays for the stage lookup.Only the floor is captured. A listener is audience and publishes no track, so capturing them would record a room rather than a call. Keeping the capture set equal to the set of published tracks is also what lets the whole existing server path be reused unchanged:
/call/{room}/transcript/start|segment|audio|end, the segment store, the live caption drawer, and the archive all work as-is.is_stage_speakerre-readsrooms.stage_enabledinstead of trusting the roster. This is not redundant with the join path. The roster lives inHub::stagesand is only mutated by the WS stage frames, so turning the stage off in room settings clears neither the participant set nor the speaker set. Without the re-check, every user holding the floor at the moment a host disabled the stage would keep opening sessions and posting segments indefinitely.disabling_the_stage_revokes_the_floorasserts the roster is deliberately left stale and the request is still refused.Controls
The panel offers the transcribe toggle and the drawer toggle to speakers only, alongside the existing step-down button.
data-lc-roomis whattranscribe.jsalready resolves a session from, so starting one needed no stage-specific JS.The hot mic
This is the part that was not obvious from the ticket. A host demoting a speaker, or a speaker stepping down, re-renders the panel with
data-lc-stage-speaker="0". The server refuses their segments from that moment (proven instage_speaker_transcribes_listener_forbidden), but the microphone would have kept recording into a session that no longer accepts it - a hot mic with nowhere to go, invisible to the user because the toggle had already vanished from the re-rendered panel.transcribe.jsnow reconciles against the panel onhtmx:afterSettleand stops local capture. It deliberately does not call/end: the session belongs to the stage and the remaining speakers are still on it. Same reasoning as the existinglc:voice-lefthandler. The same hook re-runs the browser-support gate, since the panel arrives over the WebSocket after load and its toggle would otherwise never be disabled on an unsupported browser.Tests
Five, in
server/tests/transcripts.rsandserver/tests/stage_control.rs:data-lc-roomEach was confirmed failing without its fix: the gate tests by deleting the
is_stage_speakerbranch (both fail on the start call), the render test by deleting the template markup. The render test is also the first render-level coverage the stage panel has had.Full suite green, 181 test binaries, 0 failures.
just checkclean. No new Fluent keys, so en/es symmetry is untouched.Acceptance criterion not met
There is no consent notice on any surface.
transcript-banner = This call is being transcribedis an orphaned Fluent key with zero template or JS references; the floating pill was deleted in LC-416 (cc1f8b7). Several doc comments still describe it as a "consent banner" and are stale.Stage listeners do get the drawer opening and the record dot, via the existing
TranscriptStartedbus broadcast, which is the same awareness every other surface has today. But that is not the notice the ticket asks for, and restoring it is a cross-surface change that would land on 1:1 calls, voice channels, and huddles too. Filing it separately rather than reintroducing a global UI element inside a Stage ticket.Noted, not changed
recipients()keys onroom.is_voice, so for a stage room (notis_voice) live transcript events fan out to all room members rather than to stage participants. Listeners therefore do receive captions, which is what the acceptance criteria want, but so does a room member who never opened the Stage. This is pre-existing and identical for huddles, and it discloses nothing new -require_accessalready lets any room member read the saved transcript. Left alone rather than changed silently under this ticket.🤖 Generated with Claude Code
https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
The Stage was the one real-time surface with no transcription. Its audio rides the LiveKit SFU rather than the WebRTC mesh, so a Stage speaker never appears in `Hub::voice_room_users` and `require_participant` refused them - which is why this is the surface where captions matter most and the only one without them. `require_participant` now falls through to `is_stage_speaker` for non-DM rooms. The mesh check runs first because it is in-memory and already covers DMs, voice channels, and huddles; only a caller it rejects pays for the stage lookup. Only the floor is captured. A Stage listener is audience and publishes no track, so capturing them would record a room rather than a call. That keeps the capture set equal to the set of published tracks, and it means the existing server path (`/call/{room}/transcript/*`, the segment store, the archive) is reused unchanged. `is_stage_speaker` re-reads `rooms.stage_enabled` rather than trusting the roster. The roster lives in the hub and is only mutated by the WS stage frames, so turning the stage off in room settings clears nothing; without the re-check, everyone holding the floor at that moment would keep their grant indefinitely. The panel offers the transcribe and drawer toggles to speakers only. `data-lc-room` is what transcribe.js already resolves a session from, so starting one needs no stage-specific JS. Losing the floor now stops the microphone. A host demoting a speaker (or the speaker stepping down) re-renders the panel with `data-lc-stage-speaker="0"`; the server would refuse their segments from that moment, but the mic would have kept recording into a session that no longer accepts it. transcribe.js reconciles against the panel on `htmx:afterSettle` and stops local capture without calling `/end`, since the session belongs to the stage and the remaining speakers are still on it. Same reasoning as `lc:voice-left`. Tests: a listener is refused and a speaker is not, segments persist for a stage room, a demoted speaker is refused mid-session, disabling the stage revokes a live floor, and the panel renders the toggle for a speaker but not a listener. Each was confirmed failing without its fix - the gate tests by deleting the `is_stage_speaker` branch, the render test by deleting the markup. Not covered: the in-call consent notice named in the ticket does not exist on any surface. `transcript-banner` is an orphaned Fluent key; the pill was deleted in LC-416. Stage listeners do get the drawer and the record dot via the existing bus broadcast, the same awareness every other surface has, but that is not the notice the ticket asks for and it is not a Stage-specific gap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5