Skip to content

fix(stt): apply speech timestamp fixes onto the connection refactor - #8333

Draft
chenghao-mou wants to merge 1 commit into
dz/consistent-ws-error-handlingfrom
chenghao/merge-main-into-8147
Draft

chenghao-mou wants to merge 1 commit into
dz/consistent-ws-error-handlingfrom
chenghao/merge-main-into-8147

Conversation

@chenghao-mou

@chenghao-mou chenghao-mou commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Ports #7795 and #7796 onto #8147, plus #7739, which they build on. Nothing else from main is included. Your coordinator stays; each provider connection restarts the input clock.

Where ours wins:

  • RTZR detects idle in the send loop (no watchdog), so no audio is read while a segment drains.
  • Gladia and Reson8 re-anchor per session instead of summing audio or shifting start_time_offset; Gladia's queued-audio test now expects capture time.
Initial prompt and agent context

Model: Claude Opus 5.5

I am going to merge my two PRs, but DZ has a huge PR for STT refactoring too, can we open a PR for the merge conflicts targeting his PR once I merged mine?

we should take his approach whenever possible

so we included all commits in main? how about we just limit the changes to from my PRs

Chenghao chose our approach for Gladia session timing and RTZR idle handling after reviewing the tradeoff.

🤖 Generated with Claude Code

@chenghao-mou chenghao-mou added the review_effort:high Needs broad context and substantial validation label Nov 2, 2026
@CLAassistant

CLAassistant commented Nov 2, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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>
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

STT Test Results

Status: ✗ Some tests failed

Metric Count
✓ Passed 42
✗ Failed 12
× Errors 0
→ Skipped 46
▣ Total 100
⏱ Duration 479.8s
Failed Tests
  • tests.test_stt::test_stream[livekit.plugins.fireworksai]
    livekit.agents._exceptions.APIStatusError: message='Invalid response status', status_code=503, retryable=True
    
    The above exception was the direct cause of the following exception:
    
    stt_factory = <function parameter_factory.<locals>.<lambda> at 0x7126803ce8e0>
    request = <FixtureRequest for <Coroutine test_stream[livekit.plugins.fireworksai]>>
    
        @pytest.mark.usefixtures("job_process")
        @pytest.mark.parametrize("stt_factory", STTs + STREAM_ONLY_STTs)
        async def test_stream(stt_factory: Callable[[], STT], request):
            plugin_id = request.node.callspec.id
            frames, transcript, _ = await make_test_speech(
                chunk_duration_ms=STREAM_CHUNK_DURATION_MS, sample_rate=SAMPLE_RATE
            )
            silence = rtc.AudioFrame.create(
                sample_rate=SAMPLE_RATE,
                num_channels=1,
                samples_per_channel=SAMPLE_RATE * STREAM_CHUNK_DURATION_MS // 1000,
            )
            frames.extend([silence] * (TRAILING_SILENCE_DURATION_MS // STREAM_CHUNK_DURATION_MS))
      
            # TODO: differentiate missing key vs other errors
            try:
                stt_instance: STT = stt_factory()
            except ValueError as e:
                pytest.skip(f"{plugin_id}: {e}")
      
            async with stt_instance as stt:
                label = f"{stt.model}@{stt.provider}"
                if not stt.capabilities.streaming:
                    pytest.skip(f"{label} does not support streaming")
      
                for attempt in range(MAX_RETRIES):
                    try:
                        started_at = time.monotonic()
                        async with stt.stream() as stream:
                            async with asyncio.timeout(120):
    >                           events, audio_started_at = await collect_stream_events(stream, frames)
                                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    tests/test_stt.py:294: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
    
    stream = <livekit.plugins.fireworksai.stt.Spe
    
  • tests.test_stt::test_stream[livekit.plugins.mistralai.realtime_v3]
    self = <mistralai.extra.realtime.transcription.RealtimeTranscription object at 0x7b306745af00>
    model = 'voxtral-mini-transcribe-realtime-3', audio_format = None
    target_streaming_delay_ms = None, server_url = None, timeout_ms = None
    http_headers = {'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'python-httpx2/2.13.1'}
    
        async def connect(
            self,
            model: str,
            audio_format: Optional[AudioFormat] = None,
            target_streaming_delay_ms: Optional[int] = None,
            server_url: Optional[str] = None,
            timeout_ms: Optional[int] = None,
            http_headers: Optional[Mapping[str, str]] = None,
        ) -> RealtimeConnection:
            if timeout_ms is None:
                timeout_ms = self._sdk_config.timeout_ms
      
            security = self._sdk_config.security
            if security is not None and callable(security):
                security = security()
      
            resolved_security = get_security_from_env(security, models.Security)
      
            headers: dict[str, str] = {}
            query_params: dict[str, str] = {}
      
            if resolved_security is not None:
                security_headers, security_query = get_security(resolved_security)
                headers |= security_headers
                for key, values in security_query.items():
                    if values:
                        query_params[key] = values[-1]
      
            # The websocket handshake bypasses the SDK's before-request hooks, so service-account
            # auth has to be applied here too, with the same precedence.
            caller_authorization = any(
                name.lower() == "authorization" for name in (http_headers or {})
            )
            if self._sdk_config.security is None and not caller_authorization:
                token = read_service_account_token()
                if token is not None:
                    headers["Authorization"] = bearer_header(token)
      
            if http_headers is not None:
                headers |= dict(http_headers)
    
  • tests.test_stt::test_stream[livekit.plugins.nvidia]
    self = <livekit.plugins.nvidia.stt.SpeechStream object at 0x71268ec92ff0>
    config = config {
      encoding: LINEAR_PCM
      sample_rate_hertz: 16000
      language_code: "en-US"
      max_alternatives: 1
      audio_chan...automatic_punctuation: true
      model: "parakeet-1.1b-en-US-asr-streaming-silero-vad-sortformer"
    }
    interim_results: true
    
    audio_queue = <queue.Queue object at 0x71268e74f1d0>
    done = <Future finished result=<_MultiThread...tion failed"
    >>
    service = <riva.client.asr.ASRService object at 0x71268e74ef00>
    drain_started = <asyncio.locks.Event object at 0x71268e74f290 [unset]>
    
        def _recognition_worker(self, config, audio_queue, done, service, drain_started) -> None:
            def audio():
                yield from iter(audio_queue.get, None)
                self._event_loop.call_soon_threadsafe(drain_started.set)
      
            error = None
            try:
                responses = service.streaming_response_generator(audio(), config)
    >           for response in responses:
                                ^^^^^^^^^
    
    livekit-plugins/livekit-plugins-nvidia/livekit/plugins/nvidia/stt.py:241: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
    
    self = <riva.client.asr.ASRService object at 0x71268e74ef00>
    audio_chunks = <generator object SpeechStream._recognition_worker.<locals>.audio at 0x71268e768c80>
    streaming_config = config {
      encoding: LINEAR_PCM
      sample_rate_hertz: 16000
      language_code: "en-US"
      max_alternatives: 1
      audio_chan...automatic_punctuation: true
      model: "parakeet-1.1b-en-US-asr-streaming-silero-vad-sortformer"
    }
    interim_results: true
    
    
        def streaming_response_generator(
            self, audio_chunks: Iterable[bytes], streaming_config: rasr.StreamingRecognitionConfig
        ) -> Generator[rasr.StreamingRecognizeResponse, None, None]:
            """
            Generates speech recognition responses for fragments of speech audio in :param:`audio_chunks`.
            The purpose of the method is to perform speech recognition "online" - as soon as
            audio
    
  • tests.test_stt::test_stream[livekit.plugins.mistralai.realtime]
    self = <mistralai.extra.realtime.transcription.RealtimeTranscription object at 0x7d27366703e0>
    model = 'voxtral-mini-transcribe-realtime-2602', audio_format = None
    target_streaming_delay_ms = None, server_url = None, timeout_ms = None
    http_headers = {'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'python-httpx2/2.13.1'}
    
        async def connect(
            self,
            model: str,
            audio_format: Optional[AudioFormat] = None,
            target_streaming_delay_ms: Optional[int] = None,
            server_url: Optional[str] = None,
            timeout_ms: Optional[int] = None,
            http_headers: Optional[Mapping[str, str]] = None,
        ) -> RealtimeConnection:
            if timeout_ms is None:
                timeout_ms = self._sdk_config.timeout_ms
      
            security = self._sdk_config.security
            if security is not None and callable(security):
                security = security()
      
            resolved_security = get_security_from_env(security, models.Security)
      
            headers: dict[str, str] = {}
            query_params: dict[str, str] = {}
      
            if resolved_security is not None:
                security_headers, security_query = get_security(resolved_security)
                headers |= security_headers
                for key, values in security_query.items():
                    if values:
                        query_params[key] = values[-1]
      
            # The websocket handshake bypasses the SDK's before-request hooks, so service-account
            # auth has to be applied here too, with the same precedence.
            caller_authorization = any(
                name.lower() == "authorization" for name in (http_headers or {})
            )
            if self._sdk_config.security is None and not caller_authorization:
                token = read_service_account_token()
                if token is not None:
                    headers["Authorization"] = bearer_header(token)
      
            if http_headers is not None:
                headers |= dict(http_headers)
    
  • tests.test_stt::test_stream[livekit.plugins.mistralai.realtime_v3_vad]
    self = <mistralai.extra.realtime.transcription.RealtimeTranscription object at 0x7d27361ac740>
    model = 'voxtral-mini-transcribe-realtime-3', audio_format = None
    target_streaming_delay_ms = None, server_url = None, timeout_ms = None
    http_headers = {'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'python-httpx2/2.13.1'}
    
        async def connect(
            self,
            model: str,
            audio_format: Optional[AudioFormat] = None,
            target_streaming_delay_ms: Optional[int] = None,
            server_url: Optional[str] = None,
            timeout_ms: Optional[int] = None,
            http_headers: Optional[Mapping[str, str]] = None,
        ) -> RealtimeConnection:
            if timeout_ms is None:
                timeout_ms = self._sdk_config.timeout_ms
      
            security = self._sdk_config.security
            if security is not None and callable(security):
                security = security()
      
            resolved_security = get_security_from_env(security, models.Security)
      
            headers: dict[str, str] = {}
            query_params: dict[str, str] = {}
      
            if resolved_security is not None:
                security_headers, security_query = get_security(resolved_security)
                headers |= security_headers
                for key, values in security_query.items():
                    if values:
                        query_params[key] = values[-1]
      
            # The websocket handshake bypasses the SDK's before-request hooks, so service-account
            # auth has to be applied here too, with the same precedence.
            caller_authorization = any(
                name.lower() == "authorization" for name in (http_headers or {})
            )
            if self._sdk_config.security is None and not caller_authorization:
                token = read_service_account_token()
                if token is not None:
                    headers["Authorization"] = bearer_header(token)
      
            if http_headers is not None:
                headers |= dict(http_headers)
    
  • tests.test_stt_stream_live::test_live_stream_flush_idle_and_completion[fireworksai]
    provider_config = Provider(live_constructor='livekit.plugins.fireworksai.stt:STT', required_env=('FIREWORKS_API_KEY',), live_options={},...otstrap=None, tls=False, expected_attempts_after_audio=3, progress_requires_final=False, reconnect_on_idle_close=False)
    
        @pytest.mark.usefixtures("job_process")
        @pytest.mark.parametrize(
            "provider_config", providers(), ids=lambda provider_config: provider_config.name
        )
        async def test_live_stream_flush_idle_and_completion(provider_config):
            if not provider_config.live_constructor:
                pytest.skip("live driver is not configured for this variant")
            load_dotenv()
            missing = [name for name in provider_config.required_env if not os.environ.get(name)]
            if missing:
                pytest.skip("Missing live test configuration: " + ", ".join(missing))
      
            async with provider_config.create_live_stt() as provider:
                stream = provider.stream(
                    conn_options=APIConnectOptions(
                        max_retry=0,
                        timeout=15,
                        liveness_timeout=6,
                    )
                )
                finals = asyncio.Queue()
                received = []
      
                async def collect():
                    async for event in stream:
                        if event.type == stt.SpeechEventType.FINAL_TRANSCRIPT:
                            text = event.alternatives[0].text
                            received.append(text)
                            finals.put_nowait(text)
      
                collector = asyncio.create_task(collect())
                try:
                    for index, (name, expected) in enumerate(
                        [
                            ("hello_world", {"hello", "world"}),
                            ("weather_question", {"weather", "paris"}),
                        ]
                    ):
                        await _send_utterance(stream, name)
                        stream.flush()
      
                        async def wait_for_words(expected=expected):
    
  • tests.test_stt_stream_live::test_live_stream_flush_idle_and_completion[mistralai]
    self = <mistralai.extra.realtime.transcription.RealtimeTranscription object at 0x7b3066f9e150>
    model = 'voxtral-mini-transcribe-realtime-2602', audio_format = None
    target_streaming_delay_ms = None, server_url = None, timeout_ms = None
    http_headers = {'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'python-httpx2/2.13.1'}
    
        async def connect(
            self,
            model: str,
            audio_format: Optional[AudioFormat] = None,
            target_streaming_delay_ms: Optional[int] = None,
            server_url: Optional[str] = None,
            timeout_ms: Optional[int] = None,
            http_headers: Optional[Mapping[str, str]] = None,
        ) -> RealtimeConnection:
            if timeout_ms is None:
                timeout_ms = self._sdk_config.timeout_ms
      
            security = self._sdk_config.security
            if security is not None and callable(security):
                security = security()
      
            resolved_security = get_security_from_env(security, models.Security)
      
            headers: dict[str, str] = {}
            query_params: dict[str, str] = {}
      
            if resolved_security is not None:
                security_headers, security_query = get_security(resolved_security)
                headers |= security_headers
                for key, values in security_query.items():
                    if values:
                        query_params[key] = values[-1]
      
            # The websocket handshake bypasses the SDK's before-request hooks, so service-account
            # auth has to be applied here too, with the same precedence.
            caller_authorization = any(
                name.lower() == "authorization" for name in (http_headers or {})
            )
            if self._sdk_config.security is None and not caller_authorization:
                token = read_service_account_token()
                if token is not None:
                    headers["Authorization"] = bearer_header(token)
      
            if http_headers is not None:
                headers |= dict(http_headers)
    
  • tests.test_stt_stream_live::test_live_stream_flush_idle_and_completion[mistralai_voxtral3]
    provider_config = Provider(live_constructor='livekit.plugins.mistralai.stt:STT', required_env=('MISTRAL_API_KEY',), live_options={'model...otstrap=None, tls=False, expected_attempts_after_audio=3, progress_requires_final=False, reconnect_on_idle_close=False)
    
        @pytest.mark.usefixtures("job_process")
        @pytest.mark.parametrize(
            "provider_config", providers(), ids=lambda provider_config: provider_config.name
        )
        async def test_live_stream_flush_idle_and_completion(provider_config):
            if not provider_config.live_constructor:
                pytest.skip("live driver is not configured for this variant")
            load_dotenv()
            missing = [name for name in provider_config.required_env if not os.environ.get(name)]
            if missing:
                pytest.skip("Missing live test configuration: " + ", ".join(missing))
      
            async with provider_config.create_live_stt() as provider:
                stream = provider.stream(
                    conn_options=APIConnectOptions(
                        max_retry=0,
                        timeout=15,
                        liveness_timeout=6,
                    )
                )
                finals = asyncio.Queue()
                received = []
      
                async def collect():
                    async for event in stream:
                        if event.type == stt.SpeechEventType.FINAL_TRANSCRIPT:
                            text = event.alternatives[0].text
                            received.append(text)
                            finals.put_nowait(text)
      
                collector = asyncio.create_task(collect())
                try:
                    for index, (name, expected) in enumerate(
                        [
                            ("hello_world", {"hello", "world"}),
                            ("weather_question", {"weather", "paris"}),
                        ]
                    ):
    >                   await _send_utterance(stream, name)
    
    tests/test_stt_stream_live.py:78: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    
  • tests.test_stt_stream_live::test_live_stream_flush_idle_and_completion[mistralai_voxtral3_diarized]
    self = <mistralai.extra.realtime.transcription.RealtimeTranscription object at 0x7b3066f94710>
    model = 'voxtral-mini-transcribe-realtime-3', audio_format = None
    target_streaming_delay_ms = None, server_url = None, timeout_ms = None
    http_headers = {'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'python-httpx2/2.13.1'}
    
        async def connect(
            self,
            model: str,
            audio_format: Optional[AudioFormat] = None,
            target_streaming_delay_ms: Optional[int] = None,
            server_url: Optional[str] = None,
            timeout_ms: Optional[int] = None,
            http_headers: Optional[Mapping[str, str]] = None,
        ) -> RealtimeConnection:
            if timeout_ms is None:
                timeout_ms = self._sdk_config.timeout_ms
      
            security = self._sdk_config.security
            if security is not None and callable(security):
                security = security()
      
            resolved_security = get_security_from_env(security, models.Security)
      
            headers: dict[str, str] = {}
            query_params: dict[str, str] = {}
      
            if resolved_security is not None:
                security_headers, security_query = get_security(resolved_security)
                headers |= security_headers
                for key, values in security_query.items():
                    if values:
                        query_params[key] = values[-1]
      
            # The websocket handshake bypasses the SDK's before-request hooks, so service-account
            # auth has to be applied here too, with the same precedence.
            caller_authorization = any(
                name.lower() == "authorization" for name in (http_headers or {})
            )
            if self._sdk_config.security is None and not caller_authorization:
                token = read_service_account_token()
                if token is not None:
                    headers["Authorization"] = bearer_header(token)
      
            if http_headers is not None:
                headers |= dict(http_headers)
    
  • tests.test_stt_stream_live::test_live_stream_flush_idle_and_completion[mistralai_voxtral3_turn_detection]
    self = <mistralai.extra.realtime.transcription.RealtimeTranscription object at 0x7b3066ffe9c0>
    model = 'voxtral-mini-transcribe-realtime-3', audio_format = None
    target_streaming_delay_ms = None, server_url = None, timeout_ms = None
    http_headers = {'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'python-httpx2/2.13.1'}
    
        async def connect(
            self,
            model: str,
            audio_format: Optional[AudioFormat] = None,
            target_streaming_delay_ms: Optional[int] = None,
            server_url: Optional[str] = None,
            timeout_ms: Optional[int] = None,
            http_headers: Optional[Mapping[str, str]] = None,
        ) -> RealtimeConnection:
            if timeout_ms is None:
                timeout_ms = self._sdk_config.timeout_ms
      
            security = self._sdk_config.security
            if security is not None and callable(security):
                security = security()
      
            resolved_security = get_security_from_env(security, models.Security)
      
            headers: dict[str, str] = {}
            query_params: dict[str, str] = {}
      
            if resolved_security is not None:
                security_headers, security_query = get_security(resolved_security)
                headers |= security_headers
                for key, values in security_query.items():
                    if values:
                        query_params[key] = values[-1]
      
            # The websocket handshake bypasses the SDK's before-request hooks, so service-account
            # auth has to be applied here too, with the same precedence.
            caller_authorization = any(
                name.lower() == "authorization" for name in (http_headers or {})
            )
            if self._sdk_config.security is None and not caller_authorization:
                token = read_service_account_token()
                if token is not None:
                    headers["Authorization"] = bearer_header(token)
      
            if http_headers is not None:
                headers |= dict(http_headers)
    
  • tests.test_stt_stream_live::test_live_stream_flush_idle_and_completion[soniox]
    provider_config = Provider(live_constructor='livekit.plugins.soniox.stt:STT', required_env=('SONIOX_API_KEY',), live_options={}, live_op...otstrap=None, tls=False, expected_attempts_after_audio=3, progress_requires_final=False, reconnect_on_idle_close=False)
    
        @pytest.mark.usefixtures("job_process")
        @pytest.mark.parametrize(
            "provider_config", providers(), ids=lambda provider_config: provider_config.name
        )
        async def test_live_stream_flush_idle_and_completion(provider_config):
            if not provider_config.live_constructor:
                pytest.skip("live driver is not configured for this variant")
            load_dotenv()
            missing = [name for name in provider_config.required_env if not os.environ.get(name)]
            if missing:
                pytest.skip("Missing live test configuration: " + ", ".join(missing))
      
            async with provider_config.create_live_stt() as provider:
                stream = provider.stream(
                    conn_options=APIConnectOptions(
                        max_retry=0,
                        timeout=15,
                        liveness_timeout=6,
                    )
                )
                finals = asyncio.Queue()
                received = []
      
                async def collect():
                    async for event in stream:
                        if event.type == stt.SpeechEventType.FINAL_TRANSCRIPT:
                            text = event.alternatives[0].text
                            received.append(text)
                            finals.put_nowait(text)
      
                collector = asyncio.create_task(collect())
                try:
                    for index, (name, expected) in enumerate(
                        [
                            ("hello_world", {"hello", "world"}),
                            ("weather_question", {"weather", "paris"}),
                        ]
                    ):
                        await _send_utterance(stream, name)
                        stream.flush()
      
                        async def wait_for_words(expected=expected):
    
  • tests.test_stt_stream_live::test_live_stream_flush_idle_and_completion[assemblyai_universal36]
    provider_config = Provider(live_constructor='livekit.plugins.assemblyai.stt:STT', required_env=('ASSEMBLYAI_API_KEY',), live_options={'m...otstrap=None, tls=False, expected_attempts_after_audio=3, progress_requires_final=False, reconnect_on_idle_close=False)
    
        @pytest.mark.usefixtures("job_process")
        @pytest.mark.parametrize(
            "provider_config", providers(), ids=lambda provider_config: provider_config.name
        )
        async def test_live_stream_flush_idle_and_completion(provider_config):
            if not provider_config.live_constructor:
                pytest.skip("live driver is not configured for this variant")
            load_dotenv()
            missing = [name for name in provider_config.required_env if not os.environ.get(name)]
            if missing:
                pytest.skip("Missing live test configuration: " + ", ".join(missing))
      
            async with provider_config.create_live_stt() as provider:
                stream = provider.stream(
                    conn_options=APIConnectOptions(
                        max_retry=0,
                        timeout=15,
                        liveness_timeout=6,
                    )
                )
                finals = asyncio.Queue()
                received = []
      
                async def collect():
                    async for event in stream:
                        if event.type == stt.SpeechEventType.FINAL_TRANSCRIPT:
                            text = event.alternatives[0].text
                            received.append(text)
                            finals.put_nowait(text)
      
                collector = asyncio.create_task(collect())
                try:
                    for index, (name, expected) in enumerate(
                        [
                            ("hello_world", {"hello", "world"}),
                            ("weather_question", {"weather", "paris"}),
                        ]
                    ):
    >                   await _send_utterance(stream, name)
    
    tests/test_stt_stream_live.py:78: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    
Skipped Tests
Test Reason
tests.test_stt::test_recognize[livekit.plugins.assemblyai] universal-3-6-pro@AssemblyAI does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.speechmatics] linden-1@Speechmatics does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.soniox] stt-rt-v5@Soniox does not support batch recognition
tests.test_stt::test_recognize[livekit.agents.inference] unknown@livekit does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.azure] unknown@Azure STT does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.aws] unknown@Amazon Transcribe does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.reson8] livekit.plugins.reson8: Reson8 API key is required, either as argument or RESON8_API_KEY env var
tests.test_stt::test_recognize[livekit.plugins.mistralai.realtime_v3] voxtral-mini-transcribe-realtime-3@MistralAI does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.fireworksai] unknown@FireworksAI does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.cartesia] ink-2@Cartesia does not support batch recognition
tests.test_stt::test_stream[livekit.plugins.elevenlabs] scribe_v1@ElevenLabs does not support streaming
tests.test_stt::test_recognize[livekit.plugins.cartesia._legacy] ink-whisper@Cartesia does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.deepgram.STTv2] flux-general-en@Deepgram does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.gradium.STT] default@Gradium does not support batch recognition
tests.test_stt::test_recognize[livekit.plugins.nvidia] unknown@unknown does not support batch recognition
tests.test_stt::test_stream[livekit.plugins.fal] Wizper@Fal does not support streaming
tests.test_stt::test_stream[livekit.plugins.openai] gpt-4o-mini-transcribe@api.openai.com does not support streaming
tests.test_stt::test_stream[livekit.plugins.rtzr] livekit.plugins.rtzr: RTZR_CLIENT_ID and RTZR_CLIENT_SECRET must be set
tests.test_stt::test_stream[livekit.plugins.reson8] livekit.plugins.reson8: Reson8 API key is required, either as argument or RESON8_API_KEY env var
tests.test_stt::test_stream[livekit.plugins.xai] livekit.plugins.xai: xAI API key is required
... ... and 26 more

Triggered by workflow run #5488

@chenghao-mou
chenghao-mou force-pushed the chenghao/merge-main-into-8147 branch from 7f7337e to bab5a95 Compare November 2, 2026 21:20
@chenghao-mou chenghao-mou changed the title chore: merge main into the stt connection refactor fix(stt): apply speech timestamp fixes onto the connection refactor Nov 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review_effort:high Needs broad context and substantial validation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants