Merge pull request #3562 from speechmatics/fix/smx-ttfs-finals

Support TTFS for Speechmatics STT
This commit is contained in:
Mark Backman
2026-01-27 08:35:34 -05:00
committed by GitHub
3 changed files with 27 additions and 6 deletions

View File

@@ -0,0 +1,2 @@
- 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`

View File

@@ -109,7 +109,7 @@ silero = [ "onnxruntime>=1.20.1,<2" ]
simli = [ "simli-ai~=1.0.3"] simli = [ "simli-ai~=1.0.3"]
soniox = [ "pipecat-ai[websockets-base]" ] soniox = [ "pipecat-ai[websockets-base]" ]
soundfile = [ "soundfile~=0.13.1" ] 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" ] strands = [ "strands-agents>=1.9.1,<2" ]
tavus=[] tavus=[]
together = [] together = []

View File

@@ -66,7 +66,7 @@ class TurnDetectionMode(str, Enum):
"""Endpoint and turn detection handling mode. """Endpoint and turn detection handling mode.
How the STT engine handles the endpointing of speech. If using Pipecat's built-in endpointing, 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 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 voice activity detection or `TurnDetectionMode.SMART_TURN` for more advanced ML-based
@@ -106,7 +106,7 @@ class SpeechmaticsSTTService(STTService):
turn_detection_mode: Endpoint handling, one of `TurnDetectionMode.FIXED`, turn_detection_mode: Endpoint handling, one of `TurnDetectionMode.FIXED`,
`TurnDetectionMode.EXTERNAL`, `TurnDetectionMode.ADAPTIVE` and `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 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 the text output for individual speakers and ensures that the context is clear for
@@ -200,6 +200,7 @@ class SpeechmaticsSTTService(STTService):
extra_params: Extra parameters to pass to the STT engine. This is a dictionary of 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. additional parameters that can be used to configure the STT engine.
Default to None. Default to None.
""" """
# Service configuration # Service configuration
@@ -207,7 +208,7 @@ class SpeechmaticsSTTService(STTService):
language: Language | str = Language.EN language: Language | str = Language.EN
# Endpointing mode # Endpointing mode
turn_detection_mode: TurnDetectionMode = TurnDetectionMode.FIXED turn_detection_mode: TurnDetectionMode = TurnDetectionMode.EXTERNAL
# Output formatting # Output formatting
speaker_active_format: str | None = None speaker_active_format: str | None = None
@@ -345,7 +346,7 @@ class SpeechmaticsSTTService(STTService):
params.speaker_passive_format or params.speaker_active_format params.speaker_passive_format or params.speaker_active_format
) )
# Metrics # Model + metrics
self.set_model_name(self._config.operating_point.value) self.set_model_name(self._config.operating_point.value)
# Message queue # Message queue
@@ -691,6 +692,7 @@ class SpeechmaticsSTTService(STTService):
f"{self} VADUserStoppedSpeakingFrame received but internal VAD is being used" f"{self} VADUserStoppedSpeakingFrame received but internal VAD is being used"
) )
elif not self._enable_vad and self._client is not None: elif not self._enable_vad and self._client is not None:
self.request_finalize()
self._client.finalize() self._client.finalize()
async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None: async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None:
@@ -734,16 +736,33 @@ class SpeechmaticsSTTService(STTService):
# If final, then re-parse into TranscriptionFrame # If final, then re-parse into TranscriptionFrame
if finalized: if finalized:
# 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] 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]) 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"]
)
# Log the frames
logger.debug(f"{self} finalized transcript: {[f.text for f in frames]}") logger.debug(f"{self} finalized transcript: {[f.text for f in frames]}")
# Return as interim results (unformatted) # Return as interim results (unformatted)
else: else:
# Add the interim frames
frames += [ frames += [
InterimTranscriptionFrame(**attr_from_segment(segment)) for segment in segments InterimTranscriptionFrame(**attr_from_segment(segment)) for segment in segments
] ]
# Log the frames
logger.debug(f"{self} interim transcript: {[f.text for f in frames]}") logger.debug(f"{self} interim transcript: {[f.text for f in frames]}")
# Send the frames # Send the frames