From 960d0faea5f39ff2d2d0ede3f6d28950bb783ba0 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 15:48:04 +0000 Subject: [PATCH 01/10] support `is_eou` for final segment in utterance --- src/pipecat/services/speechmatics/stt.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 457388c31..8d45b4e9d 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -738,9 +738,16 @@ class SpeechmaticsSTTService(STTService): # If final, then re-parse into TranscriptionFrame if finalized: - frames += [TranscriptionFrame(**attr_from_segment(segment)) for segment in segments] + frames += [ + TranscriptionFrame( + **attr_from_segment(segment), finalized=segment.get("is_eou", False) + ) + for segment in segments + ] finalized_text = "|".join([s["text"] for s in segments]) - await self._handle_transcription(finalized_text, True, segments[0]["language"]) + await self._handle_transcription( + finalized_text, is_final=True, language=segments[0]["language"] + ) logger.debug(f"{self} finalized transcript: {[f.text for f in frames]}") # Return as interim results (unformatted) From 8b88280bb10a5bc1fb22e00fe39f358669df25b2 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 15:52:42 +0000 Subject: [PATCH 02/10] Default to using `EXTERNAL` mode. --- src/pipecat/services/speechmatics/stt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 8d45b4e9d..ed74c50d4 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -67,7 +67,7 @@ class TurnDetectionMode(str, Enum): """Endpoint and turn detection handling mode. How the STT engine handles the endpointing of speech. If using Pipecat's built-in endpointing, - then use `TurnDetectionMode.FIXED` (default). + then use `TurnDetectionMode.EXTERNAL` (default). To use the STT engine's built-in endpointing, then use `TurnDetectionMode.ADAPTIVE` for simple voice activity detection or `TurnDetectionMode.SMART_TURN` for more advanced ML-based @@ -107,7 +107,7 @@ class SpeechmaticsSTTService(STTService): turn_detection_mode: Endpoint handling, one of `TurnDetectionMode.FIXED`, `TurnDetectionMode.EXTERNAL`, `TurnDetectionMode.ADAPTIVE` and - `TurnDetectionMode.SMART_TURN`. Defaults to `TurnDetectionMode.FIXED`. + `TurnDetectionMode.SMART_TURN`. Defaults to `TurnDetectionMode.EXTERNAL`. speaker_active_format: Formatter for active speaker ID. This formatter is used to format the text output for individual speakers and ensures that the context is clear for @@ -208,7 +208,7 @@ class SpeechmaticsSTTService(STTService): language: Language | str = Language.EN # Endpointing mode - turn_detection_mode: TurnDetectionMode = TurnDetectionMode.FIXED + turn_detection_mode: TurnDetectionMode = TurnDetectionMode.EXTERNAL # Output formatting speaker_active_format: str | None = None From 0c69ae63710d426b3d372dd7a6b5868c0c689ae5 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 16:07:59 +0000 Subject: [PATCH 03/10] Changelog entry. --- changelog/3562.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3562.changed.md diff --git a/changelog/3562.changed.md b/changelog/3562.changed.md new file mode 100644 index 000000000..491e466ac --- /dev/null +++ b/changelog/3562.changed.md @@ -0,0 +1 @@ +- Added support for TTFS `finalize` attribute in `SpeechmaticsSTTServce` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. From ea94939add121c54039c1cba07314a686a959a78 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 16:24:56 +0000 Subject: [PATCH 04/10] update dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e99ab62bc..e2668d65d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ silero = [ "onnxruntime>=1.20.1,<2" ] simli = [ "simli-ai~=1.0.3"] soniox = [ "pipecat-ai[websockets-base]" ] soundfile = [ "soundfile~=0.13.1" ] -speechmatics = [ "speechmatics-voice[smart]>=0.2.6" ] +speechmatics = [ "speechmatics-voice[smart]>=0.2.8" ] strands = [ "strands-agents>=1.9.1,<2" ] tavus=[] together = [] From fc1444c9d628fbcbc492bde1e4642c666ba926b4 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 16:25:37 +0000 Subject: [PATCH 05/10] Updated changelog --- changelog/3562.changed.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog/3562.changed.md b/changelog/3562.changed.md index 491e466ac..09090672b 100644 --- a/changelog/3562.changed.md +++ b/changelog/3562.changed.md @@ -1 +1,2 @@ - Added support for TTFS `finalize` attribute in `SpeechmaticsSTTServce` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. +- Changed dependency to `speechmatics-voice[smart]>=0.2.8` From 3a71865cf4c250b4bbf5c9bc8b3c4e618cfaadeb Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:11:25 +0000 Subject: [PATCH 06/10] removed old metrics --- src/pipecat/services/speechmatics/stt.py | 47 ++++++++---------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index ed74c50d4..d02b4360b 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -598,9 +598,6 @@ class SpeechmaticsSTTService(STTService): if segments: await self._send_frames(segments) - # Update metrics - await self._emit_metrics(message.get("metadata", {}).get("processing_time", 0.0)) - async def _handle_segment(self, message: dict[str, Any]) -> None: """Handle AddSegment events. @@ -738,23 +735,33 @@ class SpeechmaticsSTTService(STTService): # If final, then re-parse into TranscriptionFrame if finalized: - frames += [ - TranscriptionFrame( - **attr_from_segment(segment), finalized=segment.get("is_eou", False) - ) - for segment in segments - ] + # Do any segments have `is_eou` set to True? + if ( + any(segment.get("is_eou", False) for segment in segments) + and self._finalize_requested + ): + self.confirm_finalize() + + # Add the finalized frames + frames += [TranscriptionFrame(**attr_from_segment(segment)) for segment in segments] + + # Handle the text (for metrics reporting) finalized_text = "|".join([s["text"] for s in segments]) await self._handle_transcription( finalized_text, is_final=True, language=segments[0]["language"] ) + + # Log the frames logger.debug(f"{self} finalized transcript: {[f.text for f in frames]}") # Return as interim results (unformatted) else: + # Add the interim frames frames += [ InterimTranscriptionFrame(**attr_from_segment(segment)) for segment in segments ] + + # Log the frames logger.debug(f"{self} interim transcript: {[f.text for f in frames]}") # Send the frames @@ -811,28 +818,6 @@ class SpeechmaticsSTTService(STTService): yield ErrorFrame(f"Speechmatics error: {e}") await self._disconnect() - async def _emit_metrics(self, processing_time: float) -> None: - """Create TTFB metrics. - - The TTFB is the seconds between the person speaking and the STT - engine emitting the first partial. This is only calculated at the - start of an utterance. - """ - # Skip if metrics not available - if not self._metrics or processing_time == 0.0: - return - - # Calculate time as time.time() - ttfb (which is seconds) - start_time = time.time() - processing_time - - # Update internal metrics - self._metrics._start_ttfb_time = start_time - self._metrics._start_processing_time = start_time - - # Stop TTFB metrics - await self.stop_ttfb_metrics() - await self.stop_processing_metrics() - # ============================================================================ # HELPERS # ============================================================================ From 99242c0a937762fd605939d350b604a7eed23f40 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:14:40 +0000 Subject: [PATCH 07/10] linting updates --- src/pipecat/services/speechmatics/stt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index d02b4360b..b194e1b5b 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -201,6 +201,7 @@ class SpeechmaticsSTTService(STTService): extra_params: Extra parameters to pass to the STT engine. This is a dictionary of additional parameters that can be used to configure the STT engine. Default to None. + """ # Service configuration @@ -346,7 +347,7 @@ class SpeechmaticsSTTService(STTService): params.speaker_passive_format or params.speaker_active_format ) - # Metrics + # Model + metrics self.set_model_name(self._config.operating_point.value) # Message queue @@ -693,6 +694,7 @@ class SpeechmaticsSTTService(STTService): ) elif not self._enable_vad and self._client is not None: self._client.finalize() + # self.confirm_finalize() async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None: """Send frames to the pipeline. From 23d7608e5f5895625cb6238db444eb7e164e2b96 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:15:30 +0000 Subject: [PATCH 08/10] changelog update --- changelog/3562.changed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3562.changed.md b/changelog/3562.changed.md index 09090672b..533f37b2f 100644 --- a/changelog/3562.changed.md +++ b/changelog/3562.changed.md @@ -1,2 +1,2 @@ -- Added support for TTFS `finalize` attribute in `SpeechmaticsSTTServce` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. +- Added support for TTFS in `SpeechmaticsSTTService` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. - Changed dependency to `speechmatics-voice[smart]>=0.2.8` From 60168f7f69ea9388f3ea3917a11f67658570cb17 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:16:43 +0000 Subject: [PATCH 09/10] remove comment --- src/pipecat/services/speechmatics/stt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index b194e1b5b..1d25d6af6 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -694,7 +694,6 @@ class SpeechmaticsSTTService(STTService): ) elif not self._enable_vad and self._client is not None: self._client.finalize() - # self.confirm_finalize() async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None: """Send frames to the pipeline. From 91346f5f37655d96e8886af8aa5ee546f49a3e79 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Tue, 27 Jan 2026 10:44:35 +0000 Subject: [PATCH 10/10] Add support for `self.request_finalize()` for Pipecat-based VAD. --- src/pipecat/services/speechmatics/stt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 1d25d6af6..752edd98b 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -693,6 +693,7 @@ class SpeechmaticsSTTService(STTService): f"{self} VADUserStoppedSpeakingFrame received but internal VAD is being used" ) elif not self._enable_vad and self._client is not None: + self.request_finalize() self._client.finalize() async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None: