Repository navigation
fix: report speech end timestamps across STT plugins - #2928
rosetta-livekit-bot[bot] wants to merge 2 commits into
Conversation
🦋 Changeset detectedLatest commit: c253209 The changes in this PR will be included in the next version bump. This PR includes changesets to release 41 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Devin Review found 2 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
There was a problem hiding this comment.
🟡 Soniox timestamps restart after reconnect
When Soniox reconnects, options.startTimeOffset freezes before the next frame establishes the provider clock. Later transcripts and speech ends use the old offset, placing them on the wrong audio timeline.
(Refers to this code)
Learn more
Soniox passes an options object into processMessage, which uses its startTimeOffset for every emitted transcript and speech end. The options object is constructed before #sendAudio calls input.next(). That read is what updates the capture-aware offset in TimedInputQueue, so on a reconnect it retains the previous connection's offset for the entire new session.
Example: A first connection consumes audio at offset 0, then drops. Audio continues for 10 seconds before Soniox reconnects. The second connection's first frame advances stream.startTimeOffset near 10, but the listener still timestamps a word ending at 0.4 as 0.4 instead of 10.4.
Recommended fix: Read this.startTimeOffset when processing each Soniox message, after the input sender consumes the first frame, rather than caching it during #runWS setup. Preserve the existing fallback for messages received before any frame.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const speechEndTime = | ||
| this.inputStartedAt === undefined | ||
| ? undefined | ||
| : this.inputStartedAt + | ||
| this.startTimeOffset * 1000 + | ||
| event.timestamp - | ||
| event.silenceDuration; |
There was a problem hiding this comment.
Ports livekit/agents#7796 to the STT providers and voice pipeline available in agents-js.
The port adds capture-aware provider clocks, preserves verified acoustic speech boundaries on EOS events, and keeps intentionally untimed providers untimed. It also cancels stale input reads across internal reconnects so the next connection cannot lose its first frame.
Validation
pnpm buildpnpm lint(passes with existing warnings)pnpm format:checkpnpm --filter @livekit/agents api:checkcue-clivalidation was unavailable becausecue-cliis not installed in this environmentSource diff coverage
livekit-agents/livekit/agents/inference/stt.py: adapted toagents/src/inference/stt.ts; JS schema/event differences, same empty-final and last-word EOS behavior.livekit-agents/livekit/agents/stt/multi_speaker_adapter.py: not applicable; agents-js has no multi-speaker adapter.livekit-agents/livekit/agents/voice/audio_recognition.py: adapted toagents/src/voice/audio_recognition.ts; preserves an ended turn boundary in the JS state machine.livekit-plugins/livekit-plugins-assemblyai/livekit/plugins/assemblyai/stt.py: ported toplugins/assemblyai/src/stt.ts.livekit-plugins/livekit-plugins-aws/livekit/plugins/aws/stt.py: not applicable; no JS AWS plugin.livekit-plugins/livekit-plugins-azure/livekit/plugins/azure/stt.py: ported toplugins/azure/src/stt.ts.livekit-plugins/livekit-plugins-baseten/livekit/plugins/baseten/stt.py: ported toplugins/baseten/src/stt.ts.livekit-plugins/livekit-plugins-cartesia/livekit/plugins/cartesia/_recognize_streams/legacy_recognize_stream.py: adapted to the shared clock used byplugins/cartesia/src/stt.ts; JS only has the newer untimed turns protocol, so billed duration remains excluded from EOS.livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt.py: ported toplugins/deepgram/src/stt.ts.livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py: ported toplugins/deepgram/src/stt_v2.ts.livekit-plugins/livekit-plugins-elevenlabs/livekit/plugins/elevenlabs/stt.py: ported toplugins/elevenlabs/src/stt.ts.livekit-plugins/livekit-plugins-fireworksai/livekit/plugins/fireworksai/stt.py: not applicable; no JS FireworksAI plugin.livekit-plugins/livekit-plugins-gladia/livekit/plugins/gladia/stt.py: not applicable; no JS Gladia plugin.livekit-plugins/livekit-plugins-google/livekit/plugins/google/stt.py: not applicable; JS has Gemini Live STT, not the changed Cloud Speech protocol.livekit-plugins/livekit-plugins-gradium/livekit/plugins/gradium/stt.py: not applicable; no JS Gradium plugin.livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/stt.py: ported toplugins/inworld/src/stt.ts.livekit-plugins/livekit-plugins-mistralai/livekit/plugins/mistralai/stt.py: ported toplugins/mistralai/src/stt.ts.livekit-plugins/livekit-plugins-nvidia/livekit/plugins/nvidia/stt.py: not applicable; no JS NVIDIA plugin.livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py: ported toplugins/openai/src/stt.ts.livekit-plugins/livekit-plugins-reson8/livekit/plugins/reson8/stt.py: not applicable; no JS Reson8 plugin.livekit-plugins/livekit-plugins-rtzr/livekit/plugins/rtzr/stt.py: not applicable; no JS RTZR plugin.livekit-plugins/livekit-plugins-sarvam/livekit/plugins/sarvam/stt.py: not applicable; source hunk is comment-only.livekit-plugins/livekit-plugins-sarvam/livekit/plugins/sarvam/stt_streaming.py: not applicable; JS lacks Sarvam realtime/manual endpointing.livekit-plugins/livekit-plugins-sarvam/tests/test_speech_timing.py: not applicable; fixture for the absent realtime stream.livekit-plugins/livekit-plugins-simplismart/livekit/plugins/simplismart/stt.py: not applicable; no JS SimpliSmart plugin.livekit-plugins/livekit-plugins-smallestai/livekit/plugins/smallestai/stt.py: not applicable; no JS SmallestAI plugin.livekit-plugins/livekit-plugins-soniox/livekit/plugins/soniox/stt.py: ported toplugins/soniox/src/stt.tsandplugins/soniox/src/_internal.ts.livekit-plugins/livekit-plugins-speechmatics/livekit/plugins/speechmatics/stt.py: not applicable; no JS Speechmatics plugin.livekit-plugins/livekit-plugins-xai/livekit/plugins/xai/stt.py: ported toplugins/xai/src/stt.ts.tests/stt_reconnect.py: adapted into capture-clock tests and provider recovery tests using Vitest/WebSocket infrastructure.tests/test_audio_recognition_turn_detection.py: adapted to existing voice timing tests.tests/test_inference_stt_start_of_speech.py: adapted toagents/src/inference/stt.test.ts.tests/test_meta_stt.py: adapted to existingplugins/meta/src/stt.test.tscoverage; Meta remains explicitly untimed and does not expose processed duration as EOS.tests/test_multi_speaker_adapter.py: not applicable; no JS adapter.tests/test_multi_speaker_primary_speaker.py: not applicable; no JS primary-speaker adapter.tests/test_plugin_assemblyai_stt.py: ported to AssemblyAI STT tests.tests/test_plugin_aws_stt.py: not applicable; absent plugin.tests/test_plugin_azure_stt.py: ported toplugins/azure/src/stt.test.ts.tests/test_plugin_baseten_stt.py: adapted to the shared capture clock and existing Baseten recovery tests.tests/test_plugin_cartesia_stt.py: adapted to existing Cartesia tests; current protocol remains intentionally untimed.tests/test_plugin_deepgram_stt.py: ported to Deepgram v1/v2/recovery tests.tests/test_plugin_elevenlabs_stt.py: ported toplugins/elevenlabs/src/stt.test.ts.tests/test_plugin_fireworksai_stt.py: not applicable; absent plugin.tests/test_plugin_gladia_stt.py: not applicable; absent plugin.tests/test_plugin_gnani_stt.py: not applicable; absent plugin.tests/test_plugin_google_stt.py: not applicable; changed Cloud Speech protocol is absent.tests/test_plugin_gradium_stt.py: not applicable; absent plugin.tests/test_plugin_inworld_stt.py: ported toplugins/inworld/src/stt_recovery.test.ts.tests/test_plugin_mistralai_stt.py: ported to Mistral production behavior and existing Mistral suite; server turn EOS remains untimed.tests/test_plugin_nabrah_stt.py: not applicable; absent plugin.tests/test_plugin_nvidia_stt.py: not applicable; absent plugin.tests/test_plugin_openai_stt_context.py: ported to OpenAI recovery/timing tests.tests/test_plugin_reson8_stt.py: not applicable; absent plugin.tests/test_plugin_rtzr_stt.py: not applicable; absent plugin.tests/test_plugin_sarvam_stt_streaming.py: not applicable; absent realtime stream.tests/test_plugin_simplismart_stt.py: not applicable; absent plugin.tests/test_plugin_smallestai_stt.py: not applicable; absent plugin.tests/test_plugin_soniox_stt.py: ported toplugins/soniox/src/stt.test.ts.tests/test_plugin_speechmatics_stt.py: not applicable; absent plugin.tests/test_plugin_xai_stt.py: ported to xAI recovery/timing tests.tests/test_stt.py: adapted to package-local JS tests; agents-js has no centralized live-provider pytest matrix.tests/test_stt_stream_timing.py: adapted to core capture-clock tests inagents/src/stt/stt.test.ts.tests/test_transcription_delay_anchor.py: adapted toagents/src/voice/audio_recognition_transcription_delay_anchor.test.ts.tests/utils/stt.py: adapted through package-local Vitest helpers and EOS assertions rather than a Python pacing helper.The source PR depends on the capture-aware provider clock from livekit/agents#7795. agents-js lacked that infrastructure, so this PR ports it in
agents/src/stt/stt.tsand wires every target provider's internal reconnect path to it.Source: livekit/agents#7796
Ported from livekit/agents#7796
Original PR description
Report speech-end timestamps for live-tested STT configurations and retain clock fixes across providers. Sarvam realtime keeps untimed EOS with corrected manual ordering. Untimed transcripts that follow EOS retain the current turn’s boundary.
Paced tests follow @Malik1234567891011's proposal. AssemblyAI gateway parameter forwarding is tracked separately in livekit/agent-gateway#1827.
Stacked on livekit/agents#7795. Addresses AGT-3678 (Linear) and livekit/agents#7651 (@AndrazP's diagnosis). Reconnect handling follows @longcw's review.
@davidzhao’s review identified delayed local VAD timestamps; his Gradium and consumer findings were also reproduced. The fixes remove synthetic padding from Gradium timing, use captured audio time for OpenAI and Mistral local VAD, and retain EOS across untimed finals.
Plugin status
Changes are relative to livekit/agents#7795, which supplies the shared clock fix. Only changed production paths are listed. EOS checks use the paced stream tests.
Live checks ran on 2026-10-30; Gradium, OpenAI, and Mistral were rechecked on 2026-11-01. Reconnect checks use real provider connections and verify the original audio timeline after reconnection. OpenAI also reuses a live pooled socket; Sarvam checks nonzero session offsets. RTZR's English-fixture WER remains exempt; its speech timing passed. Reson8’s internal-reconnect probe adds a 1 s delay before reopening to avoid its connection limit; the socket-failure probe uses the normal retry path.
ink-whisper(legacy)last_word_endend_text.stop_svad_flush=TrueInitial prompt and agent context
Model: GPT-6 Astra
Clarification for Azure-related exclusions:
Latest scope:
Final validation scope: