diff --git a/examples/foundational/07c-interruptible-deepgram-sagemaker.py b/examples/foundational/07c-interruptible-deepgram-sagemaker.py index 406f95445..2f51bda8d 100644 --- a/examples/foundational/07c-interruptible-deepgram-sagemaker.py +++ b/examples/foundational/07c-interruptible-deepgram-sagemaker.py @@ -22,9 +22,12 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.aws.llm import AWSBedrockLLMService +from pipecat.services.aws.llm import AWSBedrockLLMService, AWSBedrockLLMSettings from pipecat.services.deepgram.sagemaker.stt import DeepgramSageMakerSTTService -from pipecat.services.deepgram.sagemaker.tts import DeepgramSageMakerTTSService +from pipecat.services.deepgram.sagemaker.tts import ( + DeepgramSageMakerTTSService, + DeepgramSageMakerTTSSettings, +) from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams @@ -69,14 +72,18 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): tts = DeepgramSageMakerTTSService( endpoint_name=os.getenv("SAGEMAKER_TTS_ENDPOINT_NAME"), region=os.getenv("AWS_REGION"), - voice="aura-2-andromeda-en", + settings=DeepgramSageMakerTTSSettings( + voice="aura-2-andromeda-en", + ), ) llm = AWSBedrockLLMService( aws_region=os.getenv("AWS_REGION"), - model="us.amazon.nova-pro-v1:0", - params=AWSBedrockLLMService.InputParams(temperature=0.8), system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + settings=AWSBedrockLLMSettings( + model="us.amazon.nova-pro-v1:0", + temperature=0.8, + ), ) context = LLMContext() diff --git a/examples/foundational/07g-interruptible-openai-http.py b/examples/foundational/07g-interruptible-openai-http.py index 05c98ca59..721d1795f 100644 --- a/examples/foundational/07g-interruptible-openai-http.py +++ b/examples/foundational/07g-interruptible-openai-http.py @@ -23,8 +23,8 @@ from pipecat.processors.aggregators.llm_response_universal import ( from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.openai.stt import OpenAISTTService -from pipecat.services.openai.tts import OpenAITTSService +from pipecat.services.openai.stt import OpenAISTTService, OpenAISTTSettings +from pipecat.services.openai.tts import OpenAITTSService, OpenAITTSSettings from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams @@ -54,11 +54,18 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): stt = OpenAISTTService( api_key=os.getenv("OPENAI_API_KEY"), - model="gpt-4o-transcribe", - prompt="Expect words related to dogs, such as breed names.", + settings=OpenAISTTSettings( + model="gpt-4o-transcribe", + prompt="Expect words related to dogs, such as breed names.", + ), ) - tts = OpenAITTSService(api_key=os.getenv("OPENAI_API_KEY"), voice="ballad") + tts = OpenAITTSService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAITTSSettings( + voice="ballad", + ), + ) llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), diff --git a/src/pipecat/services/assemblyai/models.py b/src/pipecat/services/assemblyai/models.py index 2c6b3a1dc..efc13d482 100644 --- a/src/pipecat/services/assemblyai/models.py +++ b/src/pipecat/services/assemblyai/models.py @@ -124,6 +124,9 @@ AnyMessage = BeginMessage | TurnMessage | SpeechStartedMessage | TerminationMess class AssemblyAIConnectionParams(BaseModel): """Configuration parameters for AssemblyAI WebSocket connection. + .. deprecated:: 0.0.105 + Use ``settings=AssemblyAISTTSettings(foo=...)`` instead. + Parameters: sample_rate: Audio sample rate in Hz. Defaults to 16000. encoding: Audio encoding format. Defaults to "pcm_s16le". diff --git a/src/pipecat/services/assemblyai/stt.py b/src/pipecat/services/assemblyai/stt.py index e140f7543..ec4130ea5 100644 --- a/src/pipecat/services/assemblyai/stt.py +++ b/src/pipecat/services/assemblyai/stt.py @@ -13,7 +13,7 @@ WebSocket API for streaming audio transcription. import asyncio import json from dataclasses import dataclass, field -from typing import Any, AsyncGenerator, Dict, Optional +from typing import Any, AsyncGenerator, Dict, List, Optional from urllib.parse import urlencode from loguru import logger @@ -83,15 +83,38 @@ def map_language_from_assemblyai(language_code: str) -> Language: class AssemblyAISTTSettings(STTSettings): """Settings for the AssemblyAI STT service. - See :class:`AssemblyAIConnectionParams` for detailed parameter descriptions. - Parameters: - connection_params: Connection configuration parameters. + formatted_finals: Whether to enable transcript formatting. + word_finalization_max_wait_time: Maximum time to wait for word + finalization in milliseconds. + end_of_turn_confidence_threshold: Confidence threshold for + end-of-turn detection. + min_turn_silence: Minimum silence duration when confident about + end-of-turn. + max_turn_silence: Maximum silence duration before forcing + end-of-turn. + keyterms_prompt: List of key terms to guide transcription. + prompt: Optional text prompt to guide the transcription. Only + used when model is "u3-rt-pro". + language_detection: Enable automatic language detection. + format_turns: Whether to format transcript turns. + speaker_labels: Enable speaker diarization. """ - connection_params: AssemblyAIConnectionParams | _NotGiven = field( + formatted_finals: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + word_finalization_max_wait_time: int | None | _NotGiven = field( default_factory=lambda: NOT_GIVEN ) + end_of_turn_confidence_threshold: float | None | _NotGiven = field( + default_factory=lambda: NOT_GIVEN + ) + min_turn_silence: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + max_turn_silence: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + keyterms_prompt: List[str] | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + prompt: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + language_detection: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + format_turns: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + speaker_labels: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) class AssemblyAISTTService(WebsocketSTTService): @@ -110,6 +133,8 @@ class AssemblyAISTTService(WebsocketSTTService): api_key: str, language: Optional[Language] = None, api_endpoint_base_url: str = "wss://streaming.assemblyai.com/v3/ws", + sample_rate: int = 16000, + encoding: str = "pcm_s16le", connection_params: Optional[AssemblyAIConnectionParams] = None, vad_force_turn_endpoint: bool = True, should_interrupt: bool = True, @@ -123,8 +148,18 @@ class AssemblyAISTTService(WebsocketSTTService): Args: api_key: AssemblyAI API key for authentication. language: Language code for transcription. Defaults to English (Language.EN). + + .. deprecated:: 0.0.105 + Use ``settings=AssemblyAISTTSettings(language=...)`` instead. + api_endpoint_base_url: WebSocket endpoint URL. Defaults to AssemblyAI's streaming endpoint. - connection_params: Connection configuration parameters. Defaults to AssemblyAIConnectionParams(). + sample_rate: Audio sample rate in Hz. Defaults to 16000. + encoding: Audio encoding format. Defaults to "pcm_s16le". + connection_params: Connection configuration parameters. + + .. deprecated:: 0.0.105 + Use ``settings=AssemblyAISTTSettings(...)`` instead. + vad_force_turn_endpoint: Controls turn detection mode. When True (Pipecat mode, default): Forces AssemblyAI to return finals ASAP so Pipecat's turn detection (e.g., Smart Turn) decides when the user is done. @@ -135,7 +170,6 @@ class AssemblyAISTTService(WebsocketSTTService): When False (AssemblyAI turn detection mode, u3-rt-pro only): AssemblyAI's model controls turn endings using built-in turn detection. - Uses AssemblyAI API defaults for all parameters (unless user explicitly sets them) - - Respects all user-provided connection_params as-is - Emits UserStarted/StoppedSpeakingFrame from STT - No ForceEndpoint on VAD stop should_interrupt: Whether to interrupt the bot when the user starts speaking @@ -145,39 +179,80 @@ class AssemblyAISTTService(WebsocketSTTService): Use {speaker} for speaker label and {text} for transcript text. Example: "<{speaker}>{text}" or "{speaker}: {text}" If None, transcript text is not modified. Defaults to None. - settings: Runtime-updatable settings. When provided alongside other + settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. ttfs_p99_latency: P99 latency from speech end to final transcript in seconds. Override for your deployment. See https://github.com/pipecat-ai/stt-benchmark **kwargs: Additional arguments passed to parent STTService class. """ - # Resolve connection_params early — needed for validation and turn mode config - _connection_params = connection_params or AssemblyAIConnectionParams() + # 1. Initialize default_settings with hardcoded defaults + default_settings = AssemblyAISTTSettings( + model="u3-rt-pro", + language=Language.EN, + formatted_finals=True, + word_finalization_max_wait_time=None, + end_of_turn_confidence_threshold=None, + min_turn_silence=None, + max_turn_silence=None, + keyterms_prompt=None, + prompt=None, + language_detection=None, + format_turns=True, + speaker_labels=None, + ) - # AssemblyAI turn detection mode (vad_force_turn_endpoint=False) requires the - # SpeechStarted event for reliable barge-in. Only u3-rt-pro supports - # this. Other models must use Pipecat turn detection. - is_u3_pro = _connection_params.speech_model == "u3-rt-pro" + # 2. Apply direct init arg overrides (deprecated) + if language is not None: + _warn_deprecated_param("language", AssemblyAISTTSettings, "language") + default_settings.language = language + + # 3. Apply connection_params overrides (deprecated) — only if settings not provided + if connection_params is not None: + _warn_deprecated_param("connection_params", AssemblyAISTTSettings) + if not settings: + sample_rate = connection_params.sample_rate + encoding = connection_params.encoding + default_settings.model = connection_params.speech_model + default_settings.formatted_finals = connection_params.formatted_finals + default_settings.word_finalization_max_wait_time = ( + connection_params.word_finalization_max_wait_time + ) + default_settings.end_of_turn_confidence_threshold = ( + connection_params.end_of_turn_confidence_threshold + ) + default_settings.min_turn_silence = connection_params.min_turn_silence + default_settings.max_turn_silence = connection_params.max_turn_silence + default_settings.keyterms_prompt = connection_params.keyterms_prompt + default_settings.prompt = connection_params.prompt + default_settings.language_detection = connection_params.language_detection + default_settings.format_turns = connection_params.format_turns + default_settings.speaker_labels = connection_params.speaker_labels + + # 4. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + # 5. Validate final settings + is_u3_pro = default_settings.model == "u3-rt-pro" if not vad_force_turn_endpoint and not is_u3_pro: raise ValueError( f"AssemblyAI turn detection mode (vad_force_turn_endpoint=False) requires " f"u3-rt-pro for SpeechStarted support. Either set " - f"vad_force_turn_endpoint=True for {_connection_params.speech_model}, " - f"or use speech_model='u3-rt-pro'." + f"vad_force_turn_endpoint=True for {default_settings.model}, " + f"or use model='u3-rt-pro'." ) - # Validate that prompt and keyterms_prompt are not both set - if _connection_params.prompt is not None and _connection_params.keyterms_prompt is not None: + if default_settings.prompt is not None and default_settings.keyterms_prompt is not None: raise ValueError( "The prompt and keyterms_prompt parameters cannot be used in the same request. " "Please choose either one or the other based on your use case. When you use " "keyterms_prompt, your boosted words are appended to the default prompt automatically. " - "Or to boost within prompt: + Make sure to boost the words in the audio. " + "Or to boost within prompt: + Make sure to boost the words " + "in the audio. " "For more info go to: https://www.assemblyai.com/docs/streaming/universal-3-pro" ) - # Warn if user sets a custom prompt (recommend testing without one first) - if _connection_params.prompt is not None: + if default_settings.prompt is not None: logger.warning( "Custom prompt detected. Prompting is a beta feature. We recommend testing " "with no prompt first, as this will use our optimized default prompt for " @@ -186,35 +261,12 @@ class AssemblyAISTTService(WebsocketSTTService): "https://www.assemblyai.com/docs/streaming/prompting" ) - # When vad_force_turn_endpoint is enabled, configure connection params - # for Pipecat turn detection mode (fast finals for smart turn analyzer) + # 6. Configure pipecat turn mode (mutates default_settings) if vad_force_turn_endpoint: - _connection_params = self._configure_pipecat_turn_mode(_connection_params, is_u3_pro) - - # 1. Initialize default_settings with hardcoded defaults - default_settings = AssemblyAISTTSettings( - model=None, - language=Language.EN, - connection_params=AssemblyAIConnectionParams(), - ) - - # 2. Apply direct init arg overrides (deprecated) - if language is not None: - _warn_deprecated_param("language", AssemblyAISTTSettings, "language") - default_settings.language = language - - # 3. Apply connection_params overrides — only if settings not provided - if connection_params is not None: - _warn_deprecated_param("connection_params", AssemblyAISTTSettings) - if not settings: - default_settings.connection_params = _connection_params - - # 4. Apply settings delta (canonical API, always wins) - if settings is not None: - default_settings.apply_update(settings) + self._configure_pipecat_turn_mode(default_settings, is_u3_pro) super().__init__( - sample_rate=_connection_params.sample_rate, + sample_rate=sample_rate, ttfs_p99_latency=ttfs_p99_latency, settings=default_settings, **kwargs, @@ -226,6 +278,9 @@ class AssemblyAISTTService(WebsocketSTTService): self._should_interrupt = should_interrupt self._speaker_format = speaker_format + # Init-only audio config (not runtime-updatable) + self._encoding = encoding + self._termination_event = asyncio.Event() self._received_termination = False self._connected = False @@ -238,10 +293,8 @@ class AssemblyAISTTService(WebsocketSTTService): self._user_speaking = False - def _configure_pipecat_turn_mode( - self, connection_params: AssemblyAIConnectionParams, is_u3_pro: bool - ) -> AssemblyAIConnectionParams: - """Configure connection params for Pipecat turn detection mode. + def _configure_pipecat_turn_mode(self, settings: AssemblyAISTTSettings, is_u3_pro: bool): + """Configure settings for Pipecat turn detection mode. When vad_force_turn_endpoint is enabled, force AssemblyAI to return finals as fast as possible so Pipecat's smart turn analyzer can decide @@ -260,46 +313,31 @@ class AssemblyAISTTService(WebsocketSTTService): - max_turn_silence: not set (API default) Args: - connection_params: The user-provided connection parameters. + settings: The settings to configure in place. is_u3_pro: Whether using u3-rt-pro model. - - Returns: - Updated connection parameters configured for Pipecat turn mode. """ - updates = {} - if is_u3_pro: # u3-rt-pro: Synchronize max_turn_silence with min_turn_silence - min_silence = connection_params.min_turn_silence + min_silence = settings.min_turn_silence if min_silence is None: min_silence = 100 # Warn if user set max_turn_silence (will be overridden) - if connection_params.max_turn_silence is not None: + if settings.max_turn_silence is not None: logger.warning( - f"Your max_turn_silence value ({connection_params.max_turn_silence}ms) will be " + f"Your max_turn_silence value ({settings.max_turn_silence}ms) will be " f"OVERRIDDEN in Pipecat mode (vad_force_turn_endpoint=True). It will be set to " f"{min_silence}ms (matching min_turn_silence) and SENT to " f"AssemblyAI to avoid double turn detection. To use your max_turn_silence as-is, " f"switch to AssemblyAI turn detection mode (vad_force_turn_endpoint=False)." ) - updates = { - "min_turn_silence": min_silence, - "max_turn_silence": min_silence, - } + settings.min_turn_silence = min_silence + settings.max_turn_silence = min_silence else: # universal-streaming: Different configuration (works differently) - updates = { - "end_of_turn_confidence_threshold": 1.0, - "min_turn_silence": 160, - } - - # Apply updates if any - if updates: - connection_params = connection_params.model_copy(update=updates) - - return connection_params + settings.end_of_turn_confidence_threshold = 1.0 + settings.min_turn_silence = 160 def can_generate_metrics(self) -> bool: """Check if the service can generate metrics. @@ -309,18 +347,11 @@ class AssemblyAISTTService(WebsocketSTTService): """ return True - async def _update_settings(self, delta: STTSettings) -> dict[str, Any]: - """Apply a settings delta and send UpdateConfiguration if connected. - - Stores settings changes and sends UpdateConfiguration message to AssemblyAI - without reconnecting. Supports updating: - - keyterms_prompt: List of terms to boost (can be empty array to clear) - - prompt: Custom prompt text (u3-rt-pro only) - - max_turn_silence: Maximum silence before forcing turn end - - min_turn_silence: Silence before EOT check + async def _update_settings(self, delta: AssemblyAISTTSettings) -> dict[str, Any]: + """Apply a settings delta and reconnect to apply changes. Args: - delta: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta. + delta: A settings delta with updated values. Returns: Dict mapping changed field names to their previous values. @@ -330,72 +361,9 @@ class AssemblyAISTTService(WebsocketSTTService): if not changed: return changed - # If websocket is connected, send UpdateConfiguration for supported params - if ( - self._websocket - and self._websocket.state is State.OPEN - and "connection_params" in changed - ): - # Build UpdateConfiguration message - update_config = {"type": "UpdateConfiguration"} - conn_params = self._settings.connection_params - - # Get the old connection_params to see what changed - old_conn_params = changed.get("connection_params") - - # Check each potentially changed parameter - if ( - old_conn_params is None - or conn_params.keyterms_prompt != old_conn_params.keyterms_prompt - ): - if conn_params.keyterms_prompt is not None: - update_config["keyterms_prompt"] = conn_params.keyterms_prompt - logger.info(f"Updating keyterms_prompt to: {conn_params.keyterms_prompt}") - - if old_conn_params is None or conn_params.prompt != old_conn_params.prompt: - if conn_params.prompt is not None: - if conn_params.speech_model != "u3-rt-pro": - logger.warning( - f"prompt parameter is only supported with u3-rt-pro model, " - f"current model is {conn_params.speech_model}" - ) - else: - update_config["prompt"] = conn_params.prompt - logger.info(f"Updating prompt") - - if ( - old_conn_params is None - or conn_params.max_turn_silence != old_conn_params.max_turn_silence - ): - if conn_params.max_turn_silence is not None: - update_config["max_turn_silence"] = conn_params.max_turn_silence - logger.info(f"Updating max_turn_silence to: {conn_params.max_turn_silence}ms") - - if ( - old_conn_params is None - or conn_params.min_turn_silence != old_conn_params.min_turn_silence - ): - if conn_params.min_turn_silence is not None: - update_config["min_turn_silence"] = conn_params.min_turn_silence - logger.info(f"Updating min_turn_silence to: {conn_params.min_turn_silence}ms") - - # Send update if we have parameters to update - if len(update_config) > 1: # More than just "type" - try: - await self._websocket.send(json.dumps(update_config)) - logger.info(f"Sent UpdateConfiguration: {update_config}") - except Exception as e: - logger.error(f"Failed to send UpdateConfiguration: {e}") - elif "connection_params" in changed: - logger.warning( - "Connection params changed but WebSocket not connected. " - "Settings will be applied on next connection." - ) - - # Warn about other settings that can't be changed dynamically - other_changes = {k: v for k, v in changed.items() if k not in ["connection_params"]} - if other_changes: - self._warn_unhandled_updated_settings(other_changes) + # Reconnect to apply updated settings (they become WS query params) + await self._disconnect() + await self._connect() return changed @@ -473,19 +441,41 @@ class AssemblyAISTTService(WebsocketSTTService): def _build_ws_url(self) -> str: """Build WebSocket URL with query parameters using urllib.parse.urlencode.""" - params = {} - for k, v in self._settings.connection_params.model_dump().items(): - # Skip deprecated parameter - it's been migrated to min_turn_silence - if k == "min_end_of_turn_silence_when_confident": - continue + s = self._settings + params: dict[str, Any] = {} + + # Init-only audio config + params["sample_rate"] = self.sample_rate + params["encoding"] = self._encoding + + # Map model → speech_model (AssemblyAI API naming) + if s.model is not None: + params["speech_model"] = s.model + + # Settings fields (skip None values) + optional_fields = { + "formatted_finals": s.formatted_finals, + "word_finalization_max_wait_time": s.word_finalization_max_wait_time, + "end_of_turn_confidence_threshold": s.end_of_turn_confidence_threshold, + "min_turn_silence": s.min_turn_silence, + "max_turn_silence": s.max_turn_silence, + "prompt": s.prompt, + "language_detection": s.language_detection, + "format_turns": s.format_turns, + "speaker_labels": s.speaker_labels, + } + + for k, v in optional_fields.items(): if v is not None: - if k == "keyterms_prompt": - params[k] = json.dumps(v) - elif isinstance(v, bool): + if isinstance(v, bool): params[k] = str(v).lower() else: params[k] = v + # Special handling for keyterms_prompt (needs JSON encoding) + if s.keyterms_prompt is not None: + params["keyterms_prompt"] = json.dumps(s.keyterms_prompt) + if params: query_string = urlencode(params) return f"{self._api_endpoint_base_url}?{query_string}" @@ -717,7 +707,7 @@ class AssemblyAISTTService(WebsocketSTTService): # Determine if this is a final turn from AssemblyAI is_final_turn = message.end_of_turn and ( - not self._settings.connection_params.format_turns or message.turn_is_formatted + not self._settings.format_turns or message.turn_is_formatted ) if self._vad_force_turn_endpoint: diff --git a/src/pipecat/services/aws/stt.py b/src/pipecat/services/aws/stt.py index 451c8a695..dd2f2f97b 100644 --- a/src/pipecat/services/aws/stt.py +++ b/src/pipecat/services/aws/stt.py @@ -14,7 +14,7 @@ import json import os import random import string -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -29,7 +29,7 @@ from pipecat.frames.frames import ( TranscriptionFrame, ) from pipecat.services.aws.utils import build_event_message, decode_event, get_presigned_url -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _warn_deprecated_param from pipecat.services.stt_latency import AWS_TRANSCRIBE_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -47,21 +47,9 @@ except ModuleNotFoundError as e: @dataclass class AWSTranscribeSTTSettings(STTSettings): - """Settings for the AWS Transcribe STT service. + """Settings for the AWS Transcribe STT service.""" - Parameters: - sample_rate: Audio sample rate in Hz (8000 or 16000). - media_encoding: Audio encoding format (e.g. "linear16"). - number_of_channels: Number of audio channels. - show_speaker_label: Whether to show speaker labels. - enable_channel_identification: Whether to enable channel identification. - """ - - sample_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - media_encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - number_of_channels: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - show_speaker_label: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - enable_channel_identification: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class AWSTranscribeSTTService(WebsocketSTTService): @@ -94,11 +82,9 @@ class AWSTranscribeSTTService(WebsocketSTTService): aws_access_key_id: AWS access key ID. If None, uses AWS_ACCESS_KEY_ID environment variable. aws_session_token: AWS session token for temporary credentials. If None, uses AWS_SESSION_TOKEN environment variable. region: AWS region for the service. - sample_rate: Audio sample rate in Hz. Must be 8000 or 16000. - - .. deprecated:: 0.0.105 - Use ``settings=AWSTranscribeSTTSettings(sample_rate=...)`` instead. - + sample_rate: Audio sample rate in Hz. If None, uses the pipeline sample rate. + AWS Transcribe only supports 8000 or 16000 Hz; other values are + clamped to 16000 Hz at connect time. language: Language for transcription. .. deprecated:: 0.0.105 @@ -113,17 +99,9 @@ class AWSTranscribeSTTService(WebsocketSTTService): # 1. Initialize default_settings with hardcoded defaults default_settings = AWSTranscribeSTTSettings( language=self.language_to_service_language(Language.EN) or "en-US", - sample_rate=16000, - media_encoding="linear16", - number_of_channels=1, - show_speaker_label=False, - enable_channel_identification=False, ) # 2. Apply direct init arg overrides (deprecated) - if sample_rate is not None: - _warn_deprecated_param("sample_rate", AWSTranscribeSTTSettings, "sample_rate") - default_settings.sample_rate = sample_rate if language is not None: _warn_deprecated_param("language", AWSTranscribeSTTSettings, "language") default_settings.language = self.language_to_service_language(language) or "en-US" @@ -135,17 +113,17 @@ class AWSTranscribeSTTService(WebsocketSTTService): default_settings.apply_update(settings) super().__init__( + sample_rate=sample_rate, ttfs_p99_latency=ttfs_p99_latency, settings=default_settings, **kwargs, ) - # Validate sample rate - AWS Transcribe only supports 8000 Hz or 16000 Hz - if default_settings.sample_rate not in [8000, 16000]: - logger.warning( - f"AWS Transcribe only supports 8000 Hz or 16000 Hz sample rates. Converting from {default_settings.sample_rate} Hz to 16000 Hz." - ) - self._settings.sample_rate = 16000 + # Init-only connection config (not runtime-updatable). + self._media_encoding = "linear16" + self._number_of_channels = 1 + self._show_speaker_label = False + self._enable_channel_identification = False self._credentials = { "aws_access_key_id": aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID"), @@ -293,6 +271,15 @@ class AWSTranscribeSTTService(WebsocketSTTService): if not language_code: raise ValueError(f"Unsupported language: {language_code}") + # Validate sample rate — AWS Transcribe only supports 8000 or 16000 Hz + connect_sample_rate = self.sample_rate + if connect_sample_rate not in (8000, 16000): + logger.warning( + f"AWS Transcribe only supports 8000 Hz or 16000 Hz sample rates. " + f"Converting from {connect_sample_rate} Hz to 16000 Hz." + ) + connect_sample_rate = 16000 + # Generate random websocket key websocket_key = "".join( random.choices( @@ -318,14 +305,14 @@ class AWSTranscribeSTTService(WebsocketSTTService): }, language_code=language_code, media_encoding=self.get_service_encoding( - self._settings.media_encoding + self._media_encoding ), # Convert to AWS format - sample_rate=self._settings.sample_rate, - number_of_channels=self._settings.number_of_channels, + sample_rate=connect_sample_rate, + number_of_channels=self._number_of_channels, enable_partial_results_stabilization=True, partial_results_stability="high", - show_speaker_label=self._settings.show_speaker_label, - enable_channel_identification=self._settings.enable_channel_identification, + show_speaker_label=self._show_speaker_label, + enable_channel_identification=self._enable_channel_identification, ) logger.debug(f"{self} Connecting to WebSocket with URL: {presigned_url[:100]}...") diff --git a/src/pipecat/services/azure/stt.py b/src/pipecat/services/azure/stt.py index 2527f0bb0..8e6204c5e 100644 --- a/src/pipecat/services/azure/stt.py +++ b/src/pipecat/services/azure/stt.py @@ -11,7 +11,7 @@ Speech SDK for real-time audio transcription. """ import asyncio -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -26,7 +26,7 @@ from pipecat.frames.frames import ( TranscriptionFrame, ) from pipecat.services.azure.common import language_to_azure_language -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _warn_deprecated_param from pipecat.services.stt_latency import AZURE_TTFS_P99 from pipecat.services.stt_service import STTService from pipecat.transcriptions.language import Language @@ -53,15 +53,9 @@ except ModuleNotFoundError as e: @dataclass class AzureSTTSettings(STTSettings): - """Settings for the Azure STT service. + """Settings for the Azure STT service.""" - Parameters: - region: Azure region for the Speech service. - sample_rate: Audio sample rate in Hz. - """ - - region: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - sample_rate: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class AzureSTTService(STTService): @@ -110,9 +104,7 @@ class AzureSTTService(STTService): # 1. Initialize default_settings with hardcoded defaults default_settings = AzureSTTSettings( model=None, - region=region, language=language_to_azure_language(Language.EN_US), - sample_rate=sample_rate, ) # 2. Apply direct init arg overrides (deprecated) diff --git a/src/pipecat/services/cartesia/stt.py b/src/pipecat/services/cartesia/stt.py index aa66b102e..67416016e 100644 --- a/src/pipecat/services/cartesia/stt.py +++ b/src/pipecat/services/cartesia/stt.py @@ -12,7 +12,7 @@ the Cartesia Live transcription API for real-time speech recognition. import json import urllib.parse -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -28,7 +28,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _warn_deprecated_param from pipecat.services.stt_latency import CARTESIA_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language @@ -46,20 +46,17 @@ except ModuleNotFoundError as e: @dataclass class CartesiaSTTSettings(STTSettings): - """Settings for the Cartesia STT service. + """Settings for the Cartesia STT service.""" - Parameters: - encoding: Audio encoding format (e.g. ``"pcm_s16le"``). - """ - - encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class CartesiaLiveOptions: """Configuration options for Cartesia Live STT service. - Manages transcription parameters including model selection, language, - audio encoding format, and sample rate settings. + .. deprecated:: 0.0.105 + Use ``settings=CartesiaSTTSettings(...)`` for model/language and + direct ``__init__`` parameters for encoding/sample_rate instead. """ def __init__( @@ -156,7 +153,8 @@ class CartesiaSTTService(WebsocketSTTService): *, api_key: str, base_url: str = "", - sample_rate: int = 16000, + encoding: str = "pcm_s16le", + sample_rate: Optional[int] = None, live_options: Optional[CartesiaLiveOptions] = None, settings: Optional[CartesiaSTTSettings] = None, ttfs_p99_latency: Optional[float] = CARTESIA_TTFS_P99, @@ -167,44 +165,42 @@ class CartesiaSTTService(WebsocketSTTService): Args: api_key: Authentication key for Cartesia API. base_url: Custom API endpoint URL. If empty, uses default. - sample_rate: Audio sample rate in Hz. Defaults to 16000. + encoding: Audio encoding format. Defaults to "pcm_s16le". + sample_rate: Audio sample rate in Hz. If None, uses the pipeline + sample rate. live_options: Configuration options for transcription service. - settings: Runtime-updatable settings. When provided alongside - ``live_options``, ``settings`` values take precedence. + + .. deprecated:: 0.0.105 + Use ``settings=CartesiaSTTSettings(...)`` for model/language + and direct init parameters for encoding/sample_rate instead. + + settings: Runtime-updatable settings. When provided alongside deprecated + parameters, ``settings`` values take precedence. ttfs_p99_latency: P99 latency from speech end to final transcript in seconds. Override for your deployment. See https://github.com/pipecat-ai/stt-benchmark **kwargs: Additional arguments passed to parent STTService. """ - sample_rate = sample_rate or (live_options.sample_rate if live_options else None) - # 1. Initialize default_settings with hardcoded defaults default_settings = CartesiaSTTSettings( model="ink-whisper", language=Language.EN.value, - encoding="pcm_s16le", ) - # 2. (no deprecated direct args for this service) - - # 3. Apply live_options overrides — only if settings not provided + # 2. Apply live_options overrides — only if settings not provided if live_options is not None: _warn_deprecated_param("live_options", CartesiaSTTSettings) if not settings: - lo_dict = live_options.to_dict() - # Filter out "None" string values - lo_dict = { - k: v - for k, v in lo_dict.items() - if (not isinstance(v, str) or v != "None") and k != "sample_rate" - } - if "model" in lo_dict: - default_settings.model = lo_dict["model"] - if "language" in lo_dict: - default_settings.language = lo_dict["language"] - if "encoding" in lo_dict: - default_settings.encoding = lo_dict["encoding"] + if live_options.sample_rate and sample_rate is None: + sample_rate = live_options.sample_rate + if live_options.encoding: + encoding = live_options.encoding + if live_options.model: + default_settings.model = live_options.model + if live_options.language: + lang = live_options.language + default_settings.language = lang.value if isinstance(lang, Language) else lang - # 4. Apply settings delta (canonical API, always wins) + # 3. Apply settings delta (canonical API, always wins) if settings is not None: default_settings.apply_update(settings) @@ -221,6 +217,9 @@ class CartesiaSTTService(WebsocketSTTService): self._base_url = base_url or "api.cartesia.ai" self._receive_task = None + # Init-only audio config (not runtime-updatable). + self._encoding = encoding + def can_generate_metrics(self) -> bool: """Check if the service can generate processing metrics. @@ -339,7 +338,7 @@ class CartesiaSTTService(WebsocketSTTService): params = { "model": self._settings.model, "language": self._settings.language, - "encoding": self._settings.encoding, + "encoding": self._encoding, "sample_rate": str(self.sample_rate), } ws_url = f"wss://{self._base_url}/stt/websocket?{urllib.parse.urlencode(params)}" diff --git a/src/pipecat/services/deepgram/flux/stt.py b/src/pipecat/services/deepgram/flux/stt.py index ae5364f60..1e7f135b2 100644 --- a/src/pipecat/services/deepgram/flux/stt.py +++ b/src/pipecat/services/deepgram/flux/stt.py @@ -81,20 +81,16 @@ class DeepgramFluxSTTSettings(STTSettings): eot_timeout_ms: Time in ms after speech to finish a turn regardless of EOT confidence (default 5000). keyterm: Keyterms to boost recognition accuracy for specialized terminology. - mip_opt_out: Opt out of the Deepgram Model Improvement Program (default False). tag: Tags to label requests for identification during usage reporting. min_confidence: Minimum confidence required to create a TranscriptionFrame. - encoding: Audio encoding format (e.g. ``"linear16"``). """ eager_eot_threshold: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) eot_threshold: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) eot_timeout_ms: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) keyterm: list | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - mip_opt_out: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) tag: list | _NotGiven = field(default_factory=lambda: NOT_GIVEN) min_confidence: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) class DeepgramFluxSTTService(WebsocketSTTService): @@ -158,6 +154,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): api_key: str, url: str = "wss://api.deepgram.com/v2/listen", sample_rate: Optional[int] = None, + mip_opt_out: Optional[bool] = None, model: Optional[str] = None, flux_encoding: str = "linear16", params: Optional[InputParams] = None, @@ -170,7 +167,9 @@ class DeepgramFluxSTTService(WebsocketSTTService): Args: api_key: Deepgram API key for authentication. Required for API access. url: WebSocket URL for the Deepgram Flux API. Defaults to the preview endpoint. - sample_rate: Audio sample rate in Hz. If None, uses the rate from params or 16000. + sample_rate: Audio sample rate in Hz. If None, uses the pipeline + sample rate. + mip_opt_out: Opt out of the Deepgram Model Improvement Program. model: Deepgram Flux model to use for transcription. .. deprecated:: 0.0.105 @@ -221,12 +220,10 @@ class DeepgramFluxSTTService(WebsocketSTTService): default_settings = DeepgramFluxSTTSettings( model="flux-general-en", language=Language.EN, - encoding=flux_encoding, eager_eot_threshold=None, eot_threshold=None, eot_timeout_ms=None, keyterm=[], - mip_opt_out=None, tag=[], min_confidence=None, ) @@ -244,9 +241,10 @@ class DeepgramFluxSTTService(WebsocketSTTService): default_settings.eot_threshold = params.eot_threshold default_settings.eot_timeout_ms = params.eot_timeout_ms default_settings.keyterm = params.keyterm or [] - default_settings.mip_opt_out = params.mip_opt_out default_settings.tag = params.tag or [] default_settings.min_confidence = params.min_confidence + if params.mip_opt_out is not None: + mip_opt_out = params.mip_opt_out # 4. Apply settings delta (canonical API, always wins) if settings is not None: @@ -261,8 +259,11 @@ class DeepgramFluxSTTService(WebsocketSTTService): self._api_key = api_key self._url = url self._should_interrupt = should_interrupt + self._encoding = flux_encoding + self._mip_opt_out = mip_opt_out self._websocket_url = None self._receive_task = None + # Flux event handlers self._register_event_handler("on_start_of_turn") self._register_event_handler("on_turn_resumed") @@ -448,7 +449,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): url_params = [ f"model={self._settings.model}", f"sample_rate={self.sample_rate}", - f"encoding={self._settings.encoding}", + f"encoding={self._encoding}", ] if self._settings.eager_eot_threshold is not None: @@ -460,8 +461,8 @@ class DeepgramFluxSTTService(WebsocketSTTService): if self._settings.eot_timeout_ms is not None: url_params.append(f"eot_timeout_ms={self._settings.eot_timeout_ms}") - if self._settings.mip_opt_out is not None: - url_params.append(f"mip_opt_out={str(self._settings.mip_opt_out).lower()}") + if self._mip_opt_out is not None: + url_params.append(f"mip_opt_out={str(self._mip_opt_out).lower()}") # Add keyterm parameters (can have multiple) for keyterm in self._settings.keyterm: diff --git a/src/pipecat/services/deepgram/sagemaker/stt.py b/src/pipecat/services/deepgram/sagemaker/stt.py index d7a867e38..4f29b227f 100644 --- a/src/pipecat/services/deepgram/sagemaker/stt.py +++ b/src/pipecat/services/deepgram/sagemaker/stt.py @@ -14,7 +14,7 @@ languages, and various Deepgram features. import asyncio import json -from dataclasses import dataclass, field +from dataclasses import dataclass, fields from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -32,32 +32,23 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.aws.sagemaker.bidi_client import SageMakerBidiClient -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.deepgram.stt import DeepgramSTTSettings, LiveOptions +from pipecat.services.settings import STTSettings, _warn_deprecated_param, is_given from pipecat.services.stt_latency import DEEPGRAM_SAGEMAKER_TTFS_P99 from pipecat.services.stt_service import STTService from pipecat.transcriptions.language import Language from pipecat.utils.time import time_now_iso8601 from pipecat.utils.tracing.service_decorators import traced_stt -try: - from deepgram import LiveOptions -except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error( - "In order to use DeepgramSageMakerSTTService, you need to `pip install pipecat-ai[deepgram,sagemaker]`." - ) - raise Exception(f"Missing module: {e}") - @dataclass -class DeepgramSageMakerSTTSettings(STTSettings): +class DeepgramSageMakerSTTSettings(DeepgramSTTSettings): """Settings for the Deepgram SageMaker STT service. - Parameters: - live_options: Deepgram LiveOptions for the SageMaker connection. + Inherits all fields from :class:`DeepgramSTTSettings`. """ - live_options: Any | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class DeepgramSageMakerSTTService(STTService): @@ -72,14 +63,13 @@ class DeepgramSageMakerSTTService(STTService): - AWS credentials configured (via environment variables, AWS CLI, or instance metadata) - A deployed SageMaker endpoint with Deepgram model: https://developers.deepgram.com/docs/deploy-amazon-sagemaker - - Deepgram SDK for LiveOptions configuration Example:: stt = DeepgramSageMakerSTTService( endpoint_name="my-deepgram-endpoint", region="us-east-2", - live_options=LiveOptions( + settings=DeepgramSageMakerSTTSettings( model="nova-3", language="en", interim_results=True, @@ -95,7 +85,11 @@ class DeepgramSageMakerSTTService(STTService): *, endpoint_name: str, region: str, + encoding: str = "linear16", + channels: int = 1, + multichannel: bool = False, sample_rate: Optional[int] = None, + mip_opt_out: Optional[bool] = None, live_options: Optional[LiveOptions] = None, settings: Optional[DeepgramSageMakerSTTSettings] = None, ttfs_p99_latency: Optional[float] = DEEPGRAM_SAGEMAKER_TTFS_P99, @@ -107,11 +101,20 @@ class DeepgramSageMakerSTTService(STTService): endpoint_name: Name of the SageMaker endpoint with Deepgram model deployed (e.g., "my-deepgram-nova-3-endpoint"). region: AWS region where the endpoint is deployed (e.g., "us-east-2"). - sample_rate: Audio sample rate in Hz. If None, uses value from - live_options or defaults to the value from StartFrame. - live_options: Deepgram LiveOptions configuration. Treated as a - delta from a set of sensible defaults — only the fields you - set are overridden; all others keep their default values. + encoding: Audio encoding format. Defaults to "linear16". + channels: Number of audio channels. Defaults to 1. + multichannel: Transcribe each audio channel independently. + Defaults to False. + sample_rate: Audio sample rate in Hz. If None, uses the pipeline + sample rate. + mip_opt_out: Opt out of Deepgram model improvement program. + live_options: Legacy configuration options. + + .. deprecated:: 0.0.105 + Use ``settings=DeepgramSageMakerSTTSettings(...)`` for + runtime-updatable fields and direct init parameters for + connection-level config. + settings: Runtime-updatable settings. When provided alongside ``live_options``, ``settings`` values take precedence (applied after the ``live_options`` merge). @@ -119,43 +122,63 @@ class DeepgramSageMakerSTTService(STTService): Override for your deployment. See https://github.com/pipecat-ai/stt-benchmark **kwargs: Additional arguments passed to the parent STTService. """ - sample_rate = sample_rate or (live_options.sample_rate if live_options else None) - - default_options = LiveOptions( - encoding="linear16", - language=Language.EN, - model="nova-3", - channels=1, - interim_results=True, - punctuate=True, - ) - # 1. Initialize default_settings with hardcoded defaults default_settings = DeepgramSageMakerSTTSettings( model="nova-3", language=Language.EN, - live_options=default_options, + detect_entities=False, + diarize=False, + dictation=False, + endpointing=None, + interim_results=True, + keyterm=None, + keywords=None, + numerals=False, + profanity_filter=True, + punctuate=True, + redact=None, + replace=None, + search=None, + smart_format=False, + utterance_end_ms=None, + vad_events=False, ) - # 2. (no deprecated direct args like model= for this service) - - # 3. Apply live_options overrides — only if settings not provided + # 2. Apply live_options overrides — only if settings not provided if live_options is not None: _warn_deprecated_param("live_options", DeepgramSageMakerSTTSettings) if not settings: - # Merge user live_options onto defaults - merged_dict = {**default_options.to_dict(), **live_options.to_dict()} - merged_live_options = LiveOptions(**merged_dict) - default_settings.live_options = merged_live_options - if hasattr(live_options, "model") and live_options.model is not None: - default_settings.model = live_options.model - if hasattr(live_options, "language") and live_options.language is not None: - default_settings.language = live_options.language + # Extract init-only fields from live_options + if live_options.sample_rate is not None and sample_rate is None: + sample_rate = live_options.sample_rate + if live_options.encoding is not None: + encoding = live_options.encoding + if live_options.channels is not None: + channels = live_options.channels + if live_options.multichannel is not None: + multichannel = live_options.multichannel + if live_options.mip_opt_out is not None: + mip_opt_out = live_options.mip_opt_out - # 4. Apply settings delta (canonical API, always wins) + # Build settings delta from remaining fields + init_only = { + "sample_rate", + "encoding", + "channels", + "multichannel", + "mip_opt_out", + } + lo_dict = {k: v for k, v in live_options.to_dict().items() if k not in init_only} + delta = DeepgramSageMakerSTTSettings.from_mapping(lo_dict) + default_settings.apply_update(delta) + + # 3. Apply settings delta (canonical API, always wins) if settings is not None: default_settings.apply_update(settings) + # Sync extra to top-level fields so self._settings is unambiguous + default_settings._sync_extra_to_fields() + super().__init__( sample_rate=sample_rate, ttfs_p99_latency=ttfs_p99_latency, @@ -166,6 +189,12 @@ class DeepgramSageMakerSTTService(STTService): self._endpoint_name = endpoint_name self._region = region + # Init-only connection config (not runtime-updatable). + self._encoding = encoding + self._channels = channels + self._multichannel = multichannel + self._mip_opt_out = mip_opt_out + self._client: Optional[SageMakerBidiClient] = None self._response_task: Optional[asyncio.Task] = None self._keepalive_task: Optional[asyncio.Task] = None @@ -185,6 +214,10 @@ class DeepgramSageMakerSTTService(STTService): if not changed: return changed + # Sync extra to fields after the update so self._settings stays unambiguous + if isinstance(self._settings, DeepgramSTTSettings): + self._settings._sync_extra_to_fields() + # TODO: someday we could reconnect here to apply updated settings. # Code might look something like the below: # await self._disconnect() @@ -237,6 +270,43 @@ class DeepgramSageMakerSTTService(STTService): yield ErrorFrame(error=f"Unknown error occurred: {e}") yield None + def _build_query_string(self) -> str: + """Build query string from current settings and init-only connection config.""" + params = {} + s = self._settings + + # Declared Deepgram-specific fields from settings + for f in fields(s): + if f.name in ("model", "language", "extra") or f.name.startswith("_"): + continue + value = getattr(s, f.name) + if not is_given(value) or value is None: + continue + params[f.name] = str(value).lower() if isinstance(value, bool) else str(value) + + # model and language + if is_given(s.model) and s.model is not None: + params["model"] = str(s.model) + if is_given(s.language) and s.language is not None: + params["language"] = str(s.language) + + # Init-only connection config + params["encoding"] = self._encoding + params["channels"] = str(self._channels) + params["multichannel"] = str(self._multichannel).lower() + params["sample_rate"] = str(self.sample_rate) + + if self._mip_opt_out is not None: + params["mip_opt_out"] = str(self._mip_opt_out).lower() + + # Any remaining values in extra + if s.extra: + for key, value in s.extra.items(): + if value is not None: + params[key] = str(value).lower() if isinstance(value, bool) else str(value) + + return "&".join(f"{k}={v}" for k, v in params.items()) + async def _connect(self): """Connect to the SageMaker endpoint and start the BiDi session. @@ -246,21 +316,7 @@ class DeepgramSageMakerSTTService(STTService): """ logger.debug("Connecting to Deepgram on SageMaker...") - live_options = LiveOptions( - **{**self._settings.live_options.to_dict(), "sample_rate": self.sample_rate} - ) - - # Build query string from live_options, converting booleans to strings - query_params = {} - for key, value in live_options.to_dict().items(): - if value is not None: - # Convert boolean values to lowercase strings for Deepgram API - if isinstance(value, bool): - query_params[key] = str(value).lower() - else: - query_params[key] = str(value) - - query_string = "&".join(f"{k}={v}" for k, v in query_params.items()) + query_string = self._build_query_string() # Create BiDi client self._client = SageMakerBidiClient( diff --git a/src/pipecat/services/deepgram/sagemaker/tts.py b/src/pipecat/services/deepgram/sagemaker/tts.py index 0129abedb..3693178a1 100644 --- a/src/pipecat/services/deepgram/sagemaker/tts.py +++ b/src/pipecat/services/deepgram/sagemaker/tts.py @@ -187,8 +187,7 @@ class DeepgramSageMakerTTSService(TTSService): logger.debug("Connecting to Deepgram TTS on SageMaker...") query_string = ( - f"model={self._settings.voice}&encoding={self._settings.encoding}" - f"&sample_rate={self.sample_rate}" + f"model={self._settings.voice}&encoding={self._encoding}&sample_rate={self.sample_rate}" ) self._client = SageMakerBidiClient( diff --git a/src/pipecat/services/deepgram/stt.py b/src/pipecat/services/deepgram/stt.py index f311567aa..d377fe69a 100644 --- a/src/pipecat/services/deepgram/stt.py +++ b/src/pipecat/services/deepgram/stt.py @@ -8,7 +8,7 @@ import asyncio from dataclasses import dataclass, field, fields -from typing import Any, AsyncGenerator, Dict, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -26,7 +26,6 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.settings import ( - _S, NOT_GIVEN, STTSettings, _NotGiven, @@ -57,8 +56,11 @@ class LiveOptions: """Deepgram live transcription options. Compatibility wrapper that mirrors the ``LiveOptions`` class removed in - deepgram-sdk v6. Pass this to :class:`DeepgramSTTService` via the - ``live_options`` constructor argument. + deepgram-sdk v6. + + .. deprecated:: 0.0.105 + Use ``settings=DeepgramSTTSettings(...)`` for runtime-updatable fields + and direct ``__init__`` parameters for connection-level config instead. """ def __init__( @@ -179,29 +181,42 @@ class DeepgramSTTSettings(STTSettings): ``model`` and ``language`` are inherited from ``STTSettings`` / ``ServiceSettings``. Additional Deepgram connection params may - be passed in through extra ``extra`` (also inherited). + be passed in through ``extra`` (also inherited). Parameters: - channels: Number of audio channels. + detect_entities: Enable named entity detection. diarize: Enable speaker diarization. - encoding: Audio encoding (e.g. ``"linear16"``). + dictation: Enable dictation mode (converts commands to punctuation). endpointing: Endpointing sensitivity in ms, or ``False`` to disable. interim_results: Whether to emit interim transcriptions. + keyterm: Keyterms to boost (str or list of str). + keywords: Keywords to boost (str or list of str). + numerals: Convert spoken numbers to numerals. profanity_filter: Filter profanity from transcripts. punctuate: Add punctuation to transcripts. + redact: Redact sensitive information (str or list of redaction types). + replace: Word replacement rules (str or list). + search: Search terms to highlight (str or list of str). smart_format: Apply smart formatting to transcripts. + utterance_end_ms: Silence duration in ms before an utterance-end event. vad_events: Enable Deepgram VAD speech-started / utterance-end events. - extra: Additional Deepgram query parameters not covered by the fields above. """ - channels: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + detect_entities: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) diarize: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + dictation: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) endpointing: Any | _NotGiven = field(default_factory=lambda: NOT_GIVEN) interim_results: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + keyterm: Any | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + keywords: Any | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + numerals: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) profanity_filter: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) punctuate: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + redact: Any | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + replace: Any | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + search: Any | _NotGiven = field(default_factory=lambda: NOT_GIVEN) smart_format: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + utterance_end_ms: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) vad_events: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) def _sync_extra_to_fields(self) -> None: @@ -259,9 +274,16 @@ class DeepgramSTTService(STTService): api_key: str, url: str = "", base_url: str = "", + encoding: str = "linear16", + channels: int = 1, + multichannel: bool = False, sample_rate: Optional[int] = None, + callback: Optional[str] = None, + callback_method: Optional[str] = None, + tag: Optional[Any] = None, + mip_opt_out: Optional[bool] = None, live_options: Optional[LiveOptions] = None, - addons: Optional[Dict] = None, + addons: Optional[dict] = None, should_interrupt: bool = True, settings: Optional[DeepgramSTTSettings] = None, ttfs_p99_latency: Optional[float] = DEEPGRAM_TTFS_P99, @@ -277,12 +299,25 @@ class DeepgramSTTService(STTService): Parameter `url` is deprecated, use `base_url` instead. base_url: Custom Deepgram API base URL. - sample_rate: Audio sample rate. If None, uses default or live_options value. - live_options: :class: LiveOptions configuration. Treated as a - delta from a set of sensible defaults — only the fields you - set are overridden; all others keep their default values. + encoding: Audio encoding format. Defaults to "linear16". + channels: Number of audio channels. Defaults to 1. + multichannel: Transcribe each audio channel independently. + Defaults to False. + sample_rate: Audio sample rate in Hz. If None, uses the pipeline + sample rate. + callback: Callback URL for async transcription delivery. + callback_method: HTTP method for the callback (``"GET"`` or ``"POST"``). + tag: Custom billing tag. + mip_opt_out: Opt out of Deepgram model improvement program. + live_options: Legacy configuration options. + + .. deprecated:: 0.0.105 + Use ``settings=DeepgramSTTSettings(...)`` for runtime-updatable + fields and direct init parameters for connection-level config. + addons: Additional Deepgram features to enable. - should_interrupt: Determine whether the bot should be interrupted when Deepgram VAD events are enabled and the system detects that the user is speaking. + should_interrupt: Whether to interrupt the bot when Deepgram VAD + detects the user is speaking. .. deprecated:: 0.0.99 This parameter will be removed along with `vad_events` support. @@ -297,8 +332,6 @@ class DeepgramSTTService(STTService): Note: The `vad_events` option in LiveOptions is deprecated as of version 0.0.99 and will be removed in a future version. Please use the Silero VAD instead. """ - sample_rate = sample_rate or (live_options.sample_rate if live_options else None) - if url: import warnings @@ -314,30 +347,62 @@ class DeepgramSTTService(STTService): default_settings = DeepgramSTTSettings( model="nova-3-general", language=Language.EN, - encoding="linear16", - channels=1, - interim_results=True, - smart_format=False, - punctuate=True, - profanity_filter=True, - vad_events=False, + detect_entities=False, diarize=False, + dictation=False, endpointing=None, + interim_results=True, + keyterm=None, + keywords=None, + numerals=False, + profanity_filter=True, + punctuate=True, + redact=None, + replace=None, + search=None, + smart_format=False, + utterance_end_ms=None, + vad_events=False, ) - # 2. (no deprecated direct args like model= for this service) - - # 3. Apply live_options overrides — only if settings not provided + # 2. Apply live_options overrides — only if settings not provided if live_options is not None: _warn_deprecated_param("live_options", DeepgramSTTSettings) if not settings: - lo_dict = live_options.to_dict() - delta = DeepgramSTTSettings.from_mapping( - {k: v for k, v in lo_dict.items() if k != "sample_rate"} - ) + # Extract init-only fields from live_options + if live_options.sample_rate is not None and sample_rate is None: + sample_rate = live_options.sample_rate + if live_options.encoding is not None: + encoding = live_options.encoding + if live_options.channels is not None: + channels = live_options.channels + if live_options.callback is not None: + callback = live_options.callback + if live_options.callback_method is not None: + callback_method = live_options.callback_method + if live_options.tag is not None: + tag = live_options.tag + if live_options.mip_opt_out is not None: + mip_opt_out = live_options.mip_opt_out + if live_options.multichannel is not None: + multichannel = live_options.multichannel + + # Build settings delta from remaining fields + init_only = { + "sample_rate", + "encoding", + "channels", + "multichannel", + "callback", + "callback_method", + "tag", + "mip_opt_out", + } + lo_dict = {k: v for k, v in live_options.to_dict().items() if k not in init_only} + delta = DeepgramSTTSettings.from_mapping(lo_dict) default_settings.apply_update(delta) - # 4. Apply settings delta (canonical API, always wins) + # 3. Apply settings delta (canonical API, always wins) if settings is not None: default_settings.apply_update(settings) @@ -353,6 +418,13 @@ class DeepgramSTTService(STTService): self._addons = addons self._should_interrupt = should_interrupt + self._encoding = encoding + self._channels = channels + self._multichannel = multichannel + self._callback = callback + self._callback_method = callback_method + self._tag = tag + self._mip_opt_out = mip_opt_out if self._settings.vad_events: import warnings @@ -487,14 +559,26 @@ class DeepgramSTTService(STTService): if is_given(s.language) and s.language is not None: kwargs["language"] = str(s.language) + # Init-only connection config + kwargs["encoding"] = self._encoding + kwargs["channels"] = str(self._channels) + kwargs["multichannel"] = str(self._multichannel).lower() + kwargs["sample_rate"] = str(self.sample_rate) + + if self._callback is not None: + kwargs["callback"] = self._callback + if self._callback_method is not None: + kwargs["callback_method"] = self._callback_method + if self._tag is not None: + kwargs["tag"] = str(self._tag) + if self._mip_opt_out is not None: + kwargs["mip_opt_out"] = str(self._mip_opt_out).lower() + # Any remaining values in extra (that didn't map to declared fields) for key, value in s.extra.items(): if value is not None: kwargs[key] = str(value).lower() if isinstance(value, bool) else str(value) - # Always inject sample_rate from service level. - kwargs["sample_rate"] = str(self.sample_rate) - if self._addons: for key, value in self._addons.items(): kwargs[key] = str(value) diff --git a/src/pipecat/services/elevenlabs/stt.py b/src/pipecat/services/elevenlabs/stt.py index 2d75abff9..f9899f7f1 100644 --- a/src/pipecat/services/elevenlabs/stt.py +++ b/src/pipecat/services/elevenlabs/stt.py @@ -182,7 +182,8 @@ class ElevenLabsSTTSettings(STTSettings): """Settings for the ElevenLabs file-based STT service. Parameters: - tag_audio_events: Whether to include audio event tags in transcription. + tag_audio_events: Whether to include audio events like (laughter), + (coughing) in the transcription. """ tag_audio_events: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -195,7 +196,6 @@ class ElevenLabsRealtimeSTTSettings(STTSettings): See ``ElevenLabsRealtimeSTTService.InputParams`` for detailed descriptions. Parameters: - commit_strategy: How to segment speech - manual (Pipecat VAD) or vad (ElevenLabs VAD). vad_silence_threshold_secs: Seconds of silence before VAD commits (0.3-3.0). vad_threshold: VAD sensitivity (0.1-0.9, lower is more sensitive). min_speech_duration_ms: Minimum speech duration for VAD (50-2000ms). @@ -205,7 +205,6 @@ class ElevenLabsRealtimeSTTSettings(STTSettings): include_language_detection: Whether to include language detection in transcripts. """ - commit_strategy: CommitStrategy | _NotGiven = field(default_factory=lambda: NOT_GIVEN) vad_silence_threshold_secs: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) vad_threshold: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) min_speech_duration_ms: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -495,6 +494,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): *, api_key: str, base_url: str = "api.elevenlabs.io", + commit_strategy: CommitStrategy = CommitStrategy.MANUAL, model: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, @@ -507,6 +507,9 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): Args: api_key: ElevenLabs API key for authentication. base_url: Base URL for ElevenLabs WebSocket API. + commit_strategy: How to segment speech — ``CommitStrategy.MANUAL`` + (Pipecat VAD) or ``CommitStrategy.VAD`` (ElevenLabs VAD). + Defaults to ``CommitStrategy.MANUAL``. model: Model ID for transcription. .. deprecated:: 0.0.105 @@ -528,7 +531,6 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): default_settings = ElevenLabsRealtimeSTTSettings( model="scribe_v2_realtime", language=None, - commit_strategy=CommitStrategy.MANUAL, vad_silence_threshold_secs=None, vad_threshold=None, min_speech_duration_ms=None, @@ -548,7 +550,8 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): _warn_deprecated_param("params", ElevenLabsRealtimeSTTSettings) if not settings: default_settings.language = params.language_code - default_settings.commit_strategy = params.commit_strategy + if params.commit_strategy != CommitStrategy.MANUAL: + commit_strategy = params.commit_strategy default_settings.vad_silence_threshold_secs = params.vad_silence_threshold_secs default_settings.vad_threshold = params.vad_threshold default_settings.min_speech_duration_ms = params.min_speech_duration_ms @@ -575,6 +578,9 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): self._audio_format = "" # initialized in start() self._receive_task = None + # Init-only config (not runtime-updatable). + self._commit_strategy = commit_strategy + self._connected_event = asyncio.Event() self._connected_event.set() @@ -651,7 +657,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): await self._start_metrics() elif isinstance(frame, VADUserStoppedSpeakingFrame): # Send commit when user stops speaking (manual commit mode) - if self._settings.commit_strategy == CommitStrategy.MANUAL: + if self._commit_strategy == CommitStrategy.MANUAL: if self._websocket and self._websocket.state is State.OPEN: try: commit_message = { @@ -754,7 +760,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): params.append(f"language_code={self._settings.language}") params.append(f"audio_format={self._audio_format}") - params.append(f"commit_strategy={self._settings.commit_strategy.value}") + params.append(f"commit_strategy={self._commit_strategy.value}") # Add optional parameters if self._settings.include_timestamps: @@ -771,7 +777,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): ) # Add VAD parameters if using VAD commit strategy and values are specified - if self._settings.commit_strategy == CommitStrategy.VAD: + if self._commit_strategy == CommitStrategy.VAD: if self._settings.vad_silence_threshold_secs is not None: params.append( f"vad_silence_threshold_secs={self._settings.vad_silence_threshold_secs}" @@ -931,7 +937,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): await self._handle_transcription(text, True, language) - finalized = self._settings.commit_strategy == CommitStrategy.MANUAL + finalized = self._commit_strategy == CommitStrategy.MANUAL await self.push_frame( TranscriptionFrame( @@ -975,7 +981,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): await self._handle_transcription(text, True, language) - finalized = self._settings.commit_strategy == CommitStrategy.MANUAL + finalized = self._commit_strategy == CommitStrategy.MANUAL # This message is sent after committed_transcript when include_timestamps=true. # It contains the full transcript data including text and word-level timestamps. diff --git a/src/pipecat/services/fal/stt.py b/src/pipecat/services/fal/stt.py index 4428599b1..18b8ed85c 100644 --- a/src/pipecat/services/fal/stt.py +++ b/src/pipecat/services/fal/stt.py @@ -12,7 +12,7 @@ transcription using segmented audio processing. import base64 import os -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import AsyncGenerator, Optional import aiohttp @@ -20,7 +20,7 @@ from loguru import logger from pydantic import BaseModel from pipecat.frames.frames import ErrorFrame, Frame, TranscriptionFrame -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _warn_deprecated_param from pipecat.services.stt_latency import FAL_TTFS_P99 from pipecat.services.stt_service import SegmentedSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -143,18 +143,9 @@ def language_to_fal_language(language: Language) -> Optional[str]: @dataclass class FalSTTSettings(STTSettings): - """Settings for the Fal Wizper STT service. + """Settings for the Fal Wizper STT service.""" - Parameters: - task: Task to perform ('transcribe' or 'translate'). Defaults to - 'transcribe'. - chunk_level: Level of chunking ('segment'). Defaults to 'segment'. - version: Version of Wizper model to use. Defaults to '3'. - """ - - task: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - chunk_level: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - version: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class FalSTTService(SegmentedSTTService): @@ -189,6 +180,9 @@ class FalSTTService(SegmentedSTTService): *, api_key: Optional[str] = None, aiohttp_session: Optional[aiohttp.ClientSession] = None, + task: str = "transcribe", + chunk_level: str = "segment", + version: str = "3", sample_rate: Optional[int] = None, params: Optional[InputParams] = None, settings: Optional[FalSTTSettings] = None, @@ -201,11 +195,16 @@ class FalSTTService(SegmentedSTTService): api_key: Fal API key. If not provided, will check FAL_KEY environment variable. aiohttp_session: Optional aiohttp ClientSession for HTTP requests. If not provided, a session will be created and managed internally. + task: Task to perform (``"transcribe"`` or ``"translate"``). + Defaults to ``"transcribe"``. + chunk_level: Level of chunking (``"segment"``). Defaults to ``"segment"``. + version: Version of Wizper model to use. Defaults to ``"3"``. sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate. params: Configuration parameters for the Wizper API. .. deprecated:: 0.0.105 - Use ``settings=FalSTTSettings(...)`` instead. + Use ``settings=FalSTTSettings(...)`` for model/language and + direct init parameters for task/chunk_level/version instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -217,9 +216,6 @@ class FalSTTService(SegmentedSTTService): default_settings = FalSTTSettings( model=None, language=language_to_fal_language(Language.EN) or "en", - task="transcribe", - chunk_level="segment", - version="3", ) # 2. (no deprecated direct args for this service) @@ -231,9 +227,12 @@ class FalSTTService(SegmentedSTTService): default_settings.language = ( language_to_fal_language(params.language) if params.language else "en" ) - default_settings.task = params.task - default_settings.chunk_level = params.chunk_level - default_settings.version = params.version + if params.task != "transcribe": + task = params.task + if params.chunk_level != "segment": + chunk_level = params.chunk_level + if params.version != "3": + version = params.version # 4. Apply settings delta (canonical API, always wins) if settings is not None: @@ -246,6 +245,10 @@ class FalSTTService(SegmentedSTTService): **kwargs, ) + self._task = task + self._chunk_level = chunk_level + self._version = version + self._api_key = api_key or os.getenv("FAL_KEY", "") if not self._api_key: raise ValueError( @@ -301,7 +304,15 @@ class FalSTTService(SegmentedSTTService): self._session = aiohttp.ClientSession() data_uri = f"data:audio/x-wav;base64,{base64.b64encode(audio).decode()}" - payload = {"audio_url": data_uri, **self._settings.given_fields()} + payload: dict = {"audio_url": data_uri} + if self._settings.language is not None: + payload["language"] = self._settings.language + if self._task is not None: + payload["task"] = self._task + if self._chunk_level is not None: + payload["chunk_level"] = self._chunk_level + if self._version is not None: + payload["version"] = self._version headers = { "Authorization": f"Key {self._api_key}", "Content-Type": "application/json", diff --git a/src/pipecat/services/gladia/config.py b/src/pipecat/services/gladia/config.py index 5fd5bfdac..ec8997d7c 100644 --- a/src/pipecat/services/gladia/config.py +++ b/src/pipecat/services/gladia/config.py @@ -152,6 +152,10 @@ class MessagesConfig(BaseModel): class GladiaInputParams(BaseModel): """Configuration parameters for the Gladia STT service. + .. deprecated:: 0.0.105 + Use ``settings=GladiaSTTSettings(...)`` for runtime-updatable + fields and direct init parameters for encoding/bit_depth/channels. + Parameters: encoding: Audio encoding format bit_depth: Audio bit depth diff --git a/src/pipecat/services/gladia/stt.py b/src/pipecat/services/gladia/stt.py index 44e5bbd9c..f1eca2dc2 100644 --- a/src/pipecat/services/gladia/stt.py +++ b/src/pipecat/services/gladia/stt.py @@ -15,7 +15,7 @@ import base64 import json import warnings from dataclasses import dataclass, field -from typing import Any, AsyncGenerator, Dict, Literal, Optional +from typing import Any, AsyncGenerator, Literal, Optional import aiohttp from loguru import logger @@ -191,28 +191,22 @@ class GladiaSTTSettings(STTSettings): """Settings for Gladia STT service. Parameters: - encoding: Audio encoding format. - bit_depth: Audio bit depth. - channels: Number of audio channels. + language_config: Language detection and handling configuration. custom_metadata: Additional metadata to include with requests. endpointing: Silence duration in seconds to mark end of speech. maximum_duration_without_endpointing: Maximum utterance duration without silence. - language_config: Detailed language configuration. pre_processing: Audio pre-processing options. realtime_processing: Real-time processing features. messages_config: WebSocket message filtering options. enable_vad: Enable VAD to trigger end of utterance detection. """ - encoding: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - bit_depth: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - channels: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - custom_metadata: Dict[str, Any] | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + language_config: LanguageConfig | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + custom_metadata: dict[str, Any] | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) endpointing: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) maximum_duration_without_endpointing: int | None | _NotGiven = field( default_factory=lambda: NOT_GIVEN ) - language_config: LanguageConfig | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) pre_processing: PreProcessingConfig | None | _NotGiven = field( default_factory=lambda: NOT_GIVEN ) @@ -247,6 +241,9 @@ class GladiaSTTService(WebsocketSTTService): api_key: str, region: Literal["us-west", "eu-west"] | None = None, url: str = "https://api.gladia.io/v2/live", + encoding: str = "wav/pcm", + bit_depth: int = 16, + channels: int = 1, confidence: Optional[float] = None, sample_rate: Optional[int] = None, model: Optional[str] = None, @@ -263,6 +260,9 @@ class GladiaSTTService(WebsocketSTTService): api_key: Gladia API key for authentication. region: Region used to process audio. eu-west or us-west. Defaults to eu-west. url: Gladia API URL. Defaults to "https://api.gladia.io/v2/live". + encoding: Audio encoding format. Defaults to ``"wav/pcm"``. + bit_depth: Audio bit depth. Defaults to 16. + channels: Number of audio channels. Defaults to 1. confidence: Minimum confidence threshold for transcriptions (0.0-1.0). .. deprecated:: 0.0.86 @@ -278,7 +278,8 @@ class GladiaSTTService(WebsocketSTTService): params: Additional configuration parameters for Gladia service. .. deprecated:: 0.0.105 - Use ``settings=GladiaSTTSettings(...)`` instead. + Use ``settings=GladiaSTTSettings(...)`` for runtime-updatable + fields and direct init parameters for encoding/bit_depth/channels. max_buffer_size: Maximum size of audio buffer in bytes. Defaults to 20MB. should_interrupt: Determine whether the bot should be interrupted when @@ -303,13 +304,10 @@ class GladiaSTTService(WebsocketSTTService): default_settings = GladiaSTTSettings( model="solaria-1", language=None, - encoding="wav/pcm", - bit_depth=16, - channels=1, + language_config=None, custom_metadata=None, endpointing=None, maximum_duration_without_endpointing=5, - language_config=None, pre_processing=None, realtime_processing=None, messages_config=None, @@ -334,9 +332,13 @@ class GladiaSTTService(WebsocketSTTService): stacklevel=2, ) if not settings: - default_settings.encoding = params.encoding - default_settings.bit_depth = params.bit_depth - default_settings.channels = params.channels + # Extract init-only fields from params + if params.encoding is not None: + encoding = params.encoding + if params.bit_depth is not None: + bit_depth = params.bit_depth + if params.channels is not None: + channels = params.channels default_settings.custom_metadata = params.custom_metadata default_settings.endpointing = params.endpointing default_settings.maximum_duration_without_endpointing = ( @@ -347,14 +349,14 @@ class GladiaSTTService(WebsocketSTTService): default_settings.messages_config = params.messages_config default_settings.enable_vad = params.enable_vad # Resolve deprecated language → language_config at init time - language_config = params.language_config - if not language_config and params.language: + if params.language_config: + default_settings.language_config = params.language_config + elif params.language: language_code = self.language_to_service_language(params.language) if language_code: - language_config = LanguageConfig( + default_settings.language_config = LanguageConfig( languages=[language_code], code_switching=False ) - default_settings.language_config = language_config # 4. Apply settings delta (canonical API, always wins) if settings is not None: @@ -374,6 +376,11 @@ class GladiaSTTService(WebsocketSTTService): self._url = url self._receive_task = None + # Init-only connection config + self._encoding = encoding + self._bit_depth = bit_depth + self._channels = channels + # Session management self._session_url = None self._session_id = None @@ -411,14 +418,14 @@ class GladiaSTTService(WebsocketSTTService): """ return language_to_gladia_language(language) - def _prepare_settings(self) -> Dict[str, Any]: + def _prepare_settings(self) -> dict[str, Any]: s = self._settings settings = { - "encoding": s.encoding or "wav/pcm", - "bit_depth": s.bit_depth or 16, + "encoding": self._encoding or "wav/pcm", + "bit_depth": self._bit_depth or 16, "sample_rate": self.sample_rate, - "channels": s.channels or 1, + "channels": self._channels or 1, "model": s.model, } @@ -610,7 +617,7 @@ class GladiaSTTService(WebsocketSTTService): self._websocket = None await self._call_event_handler("on_disconnected") - async def _setup_gladia(self, settings: Dict[str, Any]): + async def _setup_gladia(self, settings: dict[str, Any]): async with aiohttp.ClientSession() as session: params = {} if self._region: diff --git a/src/pipecat/services/gradium/stt.py b/src/pipecat/services/gradium/stt.py index fff58d87a..e8ab072d0 100644 --- a/src/pipecat/services/gradium/stt.py +++ b/src/pipecat/services/gradium/stt.py @@ -12,7 +12,7 @@ WebSocket API for streaming audio transcription. import base64 import json -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -28,7 +28,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _warn_deprecated_param from pipecat.services.stt_latency import GRADIUM_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -68,14 +68,9 @@ def language_to_gradium_language(language: Language) -> Optional[str]: @dataclass class GradiumSTTSettings(STTSettings): - """Settings for the Gradium STT service. + """Settings for the Gradium STT service.""" - Parameters: - delay_in_frames: Delay in audio frames (80ms each) before text is - generated. Higher delays allow more context but increase latency. - """ - - delay_in_frames: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class GradiumSTTService(WebsocketSTTService): @@ -112,6 +107,7 @@ class GradiumSTTService(WebsocketSTTService): *, api_key: str, api_endpoint_base_url: str = "wss://eu.api.gradium.ai/api/speech/asr", + delay_in_frames: Optional[int] = None, params: Optional[InputParams] = None, json_config: Optional[str] = None, settings: Optional[GradiumSTTSettings] = None, @@ -123,6 +119,9 @@ class GradiumSTTService(WebsocketSTTService): Args: api_key: Gradium API key for authentication. api_endpoint_base_url: WebSocket endpoint URL. Defaults to Gradium's streaming endpoint. + delay_in_frames: Delay in audio frames (80ms each) before text is + generated. Higher delays allow more context but increase latency. + Allowed values: 7, 8, 10, 12, 14, 16, 20, 24, 36, 48. params: Configuration parameters for language and delay settings. .. deprecated:: 0.0.105 @@ -152,7 +151,6 @@ class GradiumSTTService(WebsocketSTTService): default_settings = GradiumSTTSettings( model=None, language=None, - delay_in_frames=None, ) # 2. (no deprecated direct args for this service) @@ -162,7 +160,8 @@ class GradiumSTTService(WebsocketSTTService): _warn_deprecated_param("params", GradiumSTTSettings) if not settings: default_settings.language = params.language - default_settings.delay_in_frames = params.delay_in_frames + if params.delay_in_frames is not None: + delay_in_frames = params.delay_in_frames # 4. Apply settings delta (canonical API, always wins) if settings is not None: @@ -179,6 +178,7 @@ class GradiumSTTService(WebsocketSTTService): self._api_endpoint_base_url = api_endpoint_base_url self._websocket = None self._json_config = json_config + self._config_delay_in_frames = delay_in_frames self._receive_task = None @@ -358,8 +358,8 @@ class GradiumSTTService(WebsocketSTTService): gradium_language = language_to_gradium_language(self._settings.language) if gradium_language: json_config["language"] = gradium_language - if self._settings.delay_in_frames: - json_config["delay_in_frames"] = self._settings.delay_in_frames + if self._config_delay_in_frames: + json_config["delay_in_frames"] = self._config_delay_in_frames if json_config: setup_msg["json_config"] = json_config await self._websocket.send(json.dumps(setup_msg)) diff --git a/src/pipecat/services/groq/stt.py b/src/pipecat/services/groq/stt.py index b875f63d4..1733831c8 100644 --- a/src/pipecat/services/groq/stt.py +++ b/src/pipecat/services/groq/stt.py @@ -6,6 +6,7 @@ """Groq speech-to-text service implementation using Whisper models.""" +from dataclasses import dataclass from typing import Optional from pipecat.services.settings import _warn_deprecated_param @@ -18,6 +19,17 @@ from pipecat.services.whisper.base_stt import ( from pipecat.transcriptions.language import Language +@dataclass +class GroqSTTSettings(BaseWhisperSTTSettings): + """Settings for the Groq STT service. + + Parameters: + prompt: Optional prompt text to guide transcription style. + """ + + pass + + class GroqSTTService(BaseWhisperSTTService): """Groq Whisper speech-to-text service. @@ -25,6 +37,8 @@ class GroqSTTService(BaseWhisperSTTService): set via the api_key parameter or GROQ_API_KEY environment variable. """ + _settings: GroqSTTSettings + def __init__( self, *, @@ -34,7 +48,7 @@ class GroqSTTService(BaseWhisperSTTService): language: Optional[Language] = None, prompt: Optional[str] = None, temperature: Optional[float] = None, - settings: Optional[BaseWhisperSTTSettings] = None, + settings: Optional[GroqSTTSettings] = None, ttfs_p99_latency: Optional[float] = GROQ_TTFS_P99, **kwargs, ): @@ -44,24 +58,24 @@ class GroqSTTService(BaseWhisperSTTService): model: Whisper model to use. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(model=...)`` instead. + Use ``settings=GroqSTTSettings(model=...)`` instead. api_key: Groq API key. Defaults to None. base_url: API base URL. Defaults to "https://api.groq.com/openai/v1". language: Language of the audio input. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(language=...)`` instead. + Use ``settings=GroqSTTSettings(language=...)`` instead. prompt: Optional text to guide the model's style or continue a previous segment. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(prompt=...)`` instead. + Use ``settings=GroqSTTSettings(prompt=...)`` instead. temperature: Optional sampling temperature between 0 and 1. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(temperature=...)`` instead. + Use ``settings=GroqSTTSettings(temperature=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -70,24 +84,25 @@ class GroqSTTService(BaseWhisperSTTService): **kwargs: Additional arguments passed to BaseWhisperSTTService. """ # --- 1. Hardcoded defaults --- - default_settings = BaseWhisperSTTSettings( + default_settings = GroqSTTSettings( model="whisper-large-v3-turbo", language=self.language_to_service_language(Language.EN), - base_url=base_url, + prompt=None, + temperature=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", BaseWhisperSTTSettings, "model") + _warn_deprecated_param("model", GroqSTTSettings, "model") default_settings.model = model if language is not None: - _warn_deprecated_param("language", BaseWhisperSTTSettings, "language") + _warn_deprecated_param("language", GroqSTTSettings, "language") default_settings.language = self.language_to_service_language(language) if prompt is not None: - _warn_deprecated_param("prompt", BaseWhisperSTTSettings, "prompt") + _warn_deprecated_param("prompt", GroqSTTSettings, "prompt") default_settings.prompt = prompt if temperature is not None: - _warn_deprecated_param("temperature", BaseWhisperSTTSettings, "temperature") + _warn_deprecated_param("temperature", GroqSTTSettings, "temperature") default_settings.temperature = temperature # --- 3. (no params object for this service) --- @@ -105,7 +120,7 @@ class GroqSTTService(BaseWhisperSTTService): ) async def _transcribe(self, audio: bytes) -> Transcription: - assert self._language is not None # Assigned in the BaseWhisperSTTService class + assert self._settings.language is not None # Build kwargs dict with only set parameters kwargs = { @@ -113,13 +128,13 @@ class GroqSTTService(BaseWhisperSTTService): "model": self._settings.model, # Use verbose_json to get probability metrics "response_format": "verbose_json" if self._include_prob_metrics else "json", - "language": self._language, + "language": self._settings.language, } - if self._prompt is not None: - kwargs["prompt"] = self._prompt + if self._settings.prompt is not None: + kwargs["prompt"] = self._settings.prompt - if self._temperature is not None: - kwargs["temperature"] = self._temperature + if self._settings.temperature is not None: + kwargs["temperature"] = self._settings.temperature return await self._client.audio.transcriptions.create(**kwargs) diff --git a/src/pipecat/services/nvidia/stt.py b/src/pipecat/services/nvidia/stt.py index df785e25c..9b9b2bcf9 100644 --- a/src/pipecat/services/nvidia/stt.py +++ b/src/pipecat/services/nvidia/stt.py @@ -8,7 +8,7 @@ import asyncio from concurrent.futures import CancelledError as FuturesCancelledError -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, List, Mapping, Optional from loguru import logger @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( StartFrame, TranscriptionFrame, ) -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _warn_deprecated_param from pipecat.services.stt_latency import NVIDIA_TTFS_P99 from pipecat.services.stt_service import SegmentedSTTService, STTService from pipecat.transcriptions.language import Language, resolve_language @@ -110,11 +110,11 @@ class NvidiaSegmentedSTTSettings(STTSettings): boosted_lm_score: Score boost for specified words. """ - profanity_filter: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - automatic_punctuation: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - verbatim_transcripts: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - boosted_lm_words: List[str] | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - boosted_lm_score: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + profanity_filter: bool = False + automatic_punctuation: bool = True + verbatim_transcripts: bool = False + boosted_lm_words: Optional[List[str]] = None + boosted_lm_score: float = 4.0 class NvidiaSTTService(STTService): @@ -586,19 +586,18 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): def _create_recognition_config(self): """Create the NVIDIA Riva ASR recognition configuration.""" # Create base configuration + s = self._settings config = riva.client.RecognitionConfig( language_code=self._get_language_code(), max_alternatives=1, - profanity_filter=self._settings.profanity_filter, - enable_automatic_punctuation=self._settings.automatic_punctuation, - verbatim_transcripts=self._settings.verbatim_transcripts, + profanity_filter=s.profanity_filter, + enable_automatic_punctuation=s.automatic_punctuation, + verbatim_transcripts=s.verbatim_transcripts, ) # Add word boosting if specified - if self._settings.boosted_lm_words: - riva.client.add_word_boosting_to_config( - config, self._settings.boosted_lm_words, self._settings.boosted_lm_score - ) + if s.boosted_lm_words: + riva.client.add_word_boosting_to_config(config, s.boosted_lm_words, s.boosted_lm_score) # Add voice activity detection parameters riva.client.add_endpoint_parameters_to_config( diff --git a/src/pipecat/services/openai/stt.py b/src/pipecat/services/openai/stt.py index 7692deee5..fbd372523 100644 --- a/src/pipecat/services/openai/stt.py +++ b/src/pipecat/services/openai/stt.py @@ -16,7 +16,7 @@ Provides two STT services: import base64 import json -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Literal, Optional, Union from loguru import logger @@ -35,7 +35,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _NotGiven, _warn_deprecated_param from pipecat.services.stt_latency import OPENAI_REALTIME_TTFS_P99, OPENAI_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.services.whisper.base_stt import ( @@ -55,6 +55,13 @@ except ModuleNotFoundError: State = None +@dataclass +class OpenAISTTSettings(BaseWhisperSTTSettings): + """Settings for the OpenAI STT service.""" + + pass + + class OpenAISTTService(BaseWhisperSTTService): """OpenAI Speech-to-Text service that generates text from audio. @@ -62,6 +69,8 @@ class OpenAISTTService(BaseWhisperSTTService): set via the api_key parameter or OPENAI_API_KEY environment variable. """ + _settings: OpenAISTTSettings + def __init__( self, *, @@ -71,7 +80,7 @@ class OpenAISTTService(BaseWhisperSTTService): language: Optional[Language] = Language.EN, prompt: Optional[str] = None, temperature: Optional[float] = None, - settings: Optional[BaseWhisperSTTSettings] = None, + settings: Optional[OpenAISTTSettings] = None, ttfs_p99_latency: Optional[float] = OPENAI_TTFS_P99, **kwargs, ): @@ -81,13 +90,25 @@ class OpenAISTTService(BaseWhisperSTTService): model: Model to use — either gpt-4o or Whisper. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(model=...)`` instead. + Use ``settings=OpenAISTTSettings(model=...)`` instead. api_key: OpenAI API key. Defaults to None. base_url: API base URL. Defaults to None. language: Language of the audio input. Defaults to English. + + .. deprecated:: 0.0.105 + Use ``settings=OpenAISTTSettings(language=...)`` instead. + prompt: Optional text to guide the model's style or continue a previous segment. + + .. deprecated:: 0.0.105 + Use ``settings=OpenAISTTSettings(prompt=...)`` instead. + temperature: Optional sampling temperature between 0 and 1. Defaults to 0.0. + + .. deprecated:: 0.0.105 + Use ``settings=OpenAISTTSettings(temperature=...)`` instead. + settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. ttfs_p99_latency: P99 latency from speech end to final transcript in seconds. @@ -96,18 +117,23 @@ class OpenAISTTService(BaseWhisperSTTService): """ # --- 1. Hardcoded defaults --- _language = language or Language.EN - default_settings = BaseWhisperSTTSettings( + default_settings = OpenAISTTSettings( model="gpt-4o-transcribe", language=self.language_to_service_language(_language), - base_url=base_url, - prompt=prompt, - temperature=temperature, + prompt=None, + temperature=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", BaseWhisperSTTSettings, "model") + _warn_deprecated_param("model", OpenAISTTSettings, "model") default_settings.model = model + if prompt is not None: + _warn_deprecated_param("prompt", OpenAISTTSettings, "prompt") + default_settings.prompt = prompt + if temperature is not None: + _warn_deprecated_param("temperature", OpenAISTTSettings, "temperature") + default_settings.temperature = temperature # --- 3. (no params object for this service) --- @@ -124,7 +150,7 @@ class OpenAISTTService(BaseWhisperSTTService): ) async def _transcribe(self, audio: bytes) -> Transcription: - assert self._language is not None # Assigned in the BaseWhisperSTTService class + assert self._settings.language is not None # Build kwargs dict with only set parameters kwargs = { @@ -162,7 +188,7 @@ class OpenAIRealtimeSTTSettings(STTSettings): prompt: Optional prompt text to guide transcription style. """ - prompt: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + prompt: str | None | _NotGiven = None class OpenAIRealtimeSTTService(WebsocketSTTService): @@ -228,8 +254,16 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): base_url: WebSocket base URL for the Realtime API. Defaults to ``"wss://api.openai.com/v1/realtime"``. language: Language of the audio input. Defaults to English. + + .. deprecated:: 0.0.105 + Use ``settings=OpenAIRealtimeSTTSettings(language=...)`` instead. + prompt: Optional prompt text to guide transcription style or provide keyword hints. + + .. deprecated:: 0.0.105 + Use ``settings=OpenAIRealtimeSTTSettings(prompt=...)`` instead. + turn_detection: Server-side VAD configuration. Defaults to ``False`` (disabled), which relies on a local VAD processor in the pipeline. Pass ``None`` to use server @@ -257,14 +291,20 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): # --- 1. Hardcoded defaults --- default_settings = OpenAIRealtimeSTTSettings( model="gpt-4o-transcribe", - language=language, - prompt=prompt, + language=Language.EN, + prompt=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: _warn_deprecated_param("model", OpenAIRealtimeSTTSettings, "model") default_settings.model = model + if language is not None and language != Language.EN: + _warn_deprecated_param("language", OpenAIRealtimeSTTSettings, "language") + default_settings.language = language + if prompt is not None: + _warn_deprecated_param("prompt", OpenAIRealtimeSTTSettings, "prompt") + default_settings.prompt = prompt # --- 3. (no params object for this service) --- @@ -281,7 +321,6 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): self._api_key = api_key self._base_url = base_url - self._prompt = self._settings.prompt self._turn_detection = turn_detection self._noise_reduction = noise_reduction self._should_interrupt = should_interrupt @@ -318,8 +357,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): async def _update_settings(self, delta: STTSettings) -> dict[str, Any]: """Apply a settings delta and send session update if needed. - Keeps ``_language_code`` and ``_prompt`` in sync with settings - and sends a ``session.update`` to the server when the session is active. + Sends a ``session.update`` to the server when the session is active. Args: delta: A :class:`STTSettings` (or ``OpenAIRealtimeSTTSettings``) delta. @@ -329,13 +367,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): """ changed = await super()._update_settings(delta) - if not changed: - return changed - - if "prompt" in changed and isinstance(self._settings, OpenAIRealtimeSTTSettings): - self._prompt = self._settings.prompt - - if self._session_ready: + if changed and self._session_ready: await self._send_session_update() return changed @@ -492,8 +524,8 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): if language_code: transcription["language"] = language_code - if self._prompt: - transcription["prompt"] = self._prompt + if self._settings.prompt: + transcription["prompt"] = self._settings.prompt input_audio: dict = { "format": { diff --git a/src/pipecat/services/openai/tts.py b/src/pipecat/services/openai/tts.py index feb6453c7..b4933ae9f 100644 --- a/src/pipecat/services/openai/tts.py +++ b/src/pipecat/services/openai/tts.py @@ -161,6 +161,8 @@ class OpenAITTSService(TTSService): model="gpt-4o-mini-tts", voice="alloy", language=None, + instructions=None, + speed=None, ) # 2. Apply direct init arg overrides (deprecated) diff --git a/src/pipecat/services/sambanova/stt.py b/src/pipecat/services/sambanova/stt.py index 60f638897..822c44da9 100644 --- a/src/pipecat/services/sambanova/stt.py +++ b/src/pipecat/services/sambanova/stt.py @@ -6,6 +6,7 @@ """SambaNova's Speech-to-Text service implementation for real-time transcription.""" +from dataclasses import dataclass from typing import Any, Optional from loguru import logger @@ -20,6 +21,13 @@ from pipecat.services.whisper.base_stt import ( from pipecat.transcriptions.language import Language +@dataclass +class SambaNovaSTTSettings(BaseWhisperSTTSettings): + """Settings for the SambaNova STT service.""" + + pass + + class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore """SambaNova Whisper speech-to-text service. @@ -36,7 +44,7 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore language: Optional[Language] = None, prompt: Optional[str] = None, temperature: Optional[float] = None, - settings: Optional[BaseWhisperSTTSettings] = None, + settings: Optional[SambaNovaSTTSettings] = None, ttfs_p99_latency: Optional[float] = SAMBANOVA_TTFS_P99, **kwargs: Any, ) -> None: @@ -46,24 +54,24 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore model: Whisper model to use. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(model=...)`` instead. + Use ``settings=SambaNovaSTTSettings(model=...)`` instead. api_key: SambaNova API key. Defaults to None. base_url: API base URL. Defaults to "https://api.sambanova.ai/v1". language: Language of the audio input. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(language=...)`` instead. + Use ``settings=SambaNovaSTTSettings(language=...)`` instead. prompt: Optional text to guide the model's style or continue a previous segment. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(prompt=...)`` instead. + Use ``settings=SambaNovaSTTSettings(prompt=...)`` instead. temperature: Optional sampling temperature between 0 and 1. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(temperature=...)`` instead. + Use ``settings=SambaNovaSTTSettings(temperature=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -72,24 +80,25 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore **kwargs: Additional arguments passed to `pipecat.services.whisper.base_stt.BaseWhisperSTTService`. """ # --- 1. Hardcoded defaults --- - default_settings = BaseWhisperSTTSettings( + default_settings = SambaNovaSTTSettings( model="Whisper-Large-v3", language=self.language_to_service_language(Language.EN), - base_url=base_url, + prompt=None, + temperature=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", BaseWhisperSTTSettings, "model") + _warn_deprecated_param("model", SambaNovaSTTSettings, "model") default_settings.model = model if language is not None: - _warn_deprecated_param("language", BaseWhisperSTTSettings, "language") + _warn_deprecated_param("language", SambaNovaSTTSettings, "language") default_settings.language = self.language_to_service_language(language) if prompt is not None: - _warn_deprecated_param("prompt", BaseWhisperSTTSettings, "prompt") + _warn_deprecated_param("prompt", SambaNovaSTTSettings, "prompt") default_settings.prompt = prompt if temperature is not None: - _warn_deprecated_param("temperature", BaseWhisperSTTSettings, "temperature") + _warn_deprecated_param("temperature", SambaNovaSTTSettings, "temperature") default_settings.temperature = temperature # --- 3. (no params object for this service) --- @@ -107,7 +116,7 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore ) async def _transcribe(self, audio: bytes) -> Transcription: - assert self._language is not None # Assigned in the BaseWhisperSTTService class + assert self._settings.language is not None if self._include_prob_metrics: # https://docs.sambanova.ai/docs/en/features/audio#request-parameters @@ -122,13 +131,13 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore "file": ("audio.wav", audio, "audio/wav"), "model": self._settings.model, "response_format": "json", - "language": self._language, + "language": self._settings.language, } - if self._prompt is not None: - kwargs["prompt"] = self._prompt + if self._settings.prompt is not None: + kwargs["prompt"] = self._settings.prompt - if self._temperature is not None: - kwargs["temperature"] = self._temperature + if self._settings.temperature is not None: + kwargs["temperature"] = self._settings.temperature return await self._client.audio.transcriptions.create(**kwargs) diff --git a/src/pipecat/services/sarvam/stt.py b/src/pipecat/services/sarvam/stt.py index f255619bb..d8659a6e7 100644 --- a/src/pipecat/services/sarvam/stt.py +++ b/src/pipecat/services/sarvam/stt.py @@ -142,14 +142,13 @@ class SarvamSTTSettings(STTSettings): """Settings for the Sarvam STT service. Parameters: - prompt: Optional prompt to guide transcription/translation style. - mode: Mode of operation (transcribe, translate, verbatim, etc.). + prompt: Optional prompt to guide transcription/translation style/context. + Only applicable to models that support prompts (e.g., saaras:v2.5). vad_signals: Enable VAD signals in response. high_vad_sensitivity: Enable high VAD sensitivity. """ prompt: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - mode: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) vad_signals: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) high_vad_sensitivity: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -204,6 +203,9 @@ class SarvamSTTService(STTService): *, api_key: str, model: Optional[str] = None, + mode: Optional[ + Literal["transcribe", "translate", "verbatim", "translit", "codemix"] + ] = None, sample_rate: Optional[int] = None, input_audio_codec: str = "wav", params: Optional[InputParams] = None, @@ -222,6 +224,9 @@ class SarvamSTTService(STTService): .. deprecated:: 0.0.105 Use ``settings=SarvamSTTSettings(model=...)`` instead. + mode: Mode of operation. Options: transcribe, translate, verbatim, + translit, codemix. Only applicable to models that support it + (e.g., saaras:v3). Defaults to the model's default mode. sample_rate: Audio sample rate. Defaults to 16000 if not specified. input_audio_codec: Audio codec/format of the input file. Defaults to "wav". params: Configuration parameters for Sarvam STT service. @@ -238,32 +243,32 @@ class SarvamSTTService(STTService): keepalive_interval: Seconds between idle checks when keepalive is enabled. **kwargs: Additional arguments passed to the parent STTService. """ - # 1. Initialize default_settings with hardcoded defaults + # --- 1. Hardcoded defaults --- default_settings = SarvamSTTSettings( model="saarika:v2.5", language=None, prompt=None, - mode=None, vad_signals=None, high_vad_sensitivity=None, ) - # 2. Apply direct init arg overrides (deprecated) + # --- 2. Deprecated direct-arg overrides --- if model is not None: _warn_deprecated_param("model", SarvamSTTSettings, "model") default_settings.model = model - # 3. Apply params overrides — only if settings not provided + # --- 3. Deprecated params overrides --- if params is not None: _warn_deprecated_param("params", SarvamSTTSettings) if not settings: default_settings.language = params.language default_settings.prompt = params.prompt - default_settings.mode = params.mode + if params.mode is not None: + mode = params.mode default_settings.vad_signals = params.vad_signals default_settings.high_vad_sensitivity = params.high_vad_sensitivity - # 4. Apply settings delta (canonical API, always wins) + # --- 4. Settings delta (canonical API, always wins) --- if settings is not None: default_settings.apply_update(settings) @@ -278,7 +283,7 @@ class SarvamSTTService(STTService): # Validate parameters against model capabilities if default_settings.prompt is not None and not self._config.supports_prompt: raise ValueError(f"Model '{resolved_model}' does not support prompt parameter.") - if default_settings.mode is not None and not self._config.supports_mode: + if mode is not None and not self._config.supports_mode: raise ValueError(f"Model '{resolved_model}' does not support mode parameter.") if default_settings.language is not None and not self._config.supports_language: raise ValueError( @@ -286,8 +291,8 @@ class SarvamSTTService(STTService): ) # Resolve mode default from model config - if default_settings.mode is None: - default_settings.mode = self._config.default_mode + if mode is None: + mode = self._config.default_mode super().__init__( sample_rate=sample_rate, @@ -300,6 +305,9 @@ class SarvamSTTService(STTService): self._api_key = api_key + # Init-only connection config (not runtime-updatable) + self._mode = mode + # Store connection parameters self._input_audio_codec = input_audio_codec @@ -380,30 +388,26 @@ class SarvamSTTService(STTService): f"Model '{self._settings.model}' does not support language parameter " "(auto-detects language)." ) - - if isinstance(delta, SarvamSTTSettings): - if is_given(delta.prompt) and delta.prompt is not None: - if not self._config.supports_prompt: - raise ValueError( - f"Model '{self._settings.model}' does not support prompt parameter." - ) - if is_given(delta.mode) and delta.mode is not None: - if not self._config.supports_mode: - raise ValueError( - f"Model '{self._settings.model}' does not support mode parameter." - ) + if ( + isinstance(delta, SarvamSTTSettings) + and is_given(delta.prompt) + and delta.prompt is not None + ): + if not self._config.supports_prompt: + raise ValueError( + f"Model '{self._settings.model}' does not support prompt parameter." + ) changed = await super()._update_settings(delta) - # TODO: someday we could reconnect here to apply updated settings. - # Code might look something like the below: - # if not changed: - # return changed + # Prompt is a WebSocket connect-time parameter; reconnect to apply. + if "prompt" in changed: + await self._disconnect() + await self._connect() - # await self._disconnect() - # await self._connect() - - self._warn_unhandled_updated_settings(changed) + unhandled = {k: v for k, v in changed.items() if k != "prompt"} + if unhandled: + self._warn_unhandled_updated_settings(unhandled) return changed @@ -542,8 +546,8 @@ class SarvamSTTService(STTService): connect_kwargs["language_code"] = language_string # Add mode for models that support it - if self._config.supports_mode and self._settings.mode is not None: - connect_kwargs["mode"] = self._settings.mode + if self._config.supports_mode and self._mode is not None: + connect_kwargs["mode"] = self._mode # Prompt support differs across sarvamai versions. Prefer connect-time prompt # when available and gracefully degrade if the SDK doesn't accept it. diff --git a/src/pipecat/services/soniox/stt.py b/src/pipecat/services/soniox/stt.py index beadabf1b..483cad062 100644 --- a/src/pipecat/services/soniox/stt.py +++ b/src/pipecat/services/soniox/stt.py @@ -144,8 +144,6 @@ class SonioxSTTSettings(STTSettings): """Settings for Soniox STT service. Parameters: - audio_format: Audio format to use for transcription. - num_channels: Number of channels to use for transcription. language_hints: List of language hints to use for transcription. language_hints_strict: If true, strictly enforce language hints. context: Customization for transcription. String for models with @@ -156,8 +154,6 @@ class SonioxSTTSettings(STTSettings): client_reference_id: Client reference ID to use for transcription. """ - audio_format: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - num_channels: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) language_hints: List[Language] | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) language_hints_strict: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) context: SonioxContextObject | str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -187,6 +183,8 @@ class SonioxSTTService(WebsocketSTTService): url: str = "wss://stt-rt.soniox.com/transcribe-websocket", sample_rate: Optional[int] = None, model: Optional[str] = None, + audio_format: str = "pcm_s16le", + num_channels: int = 1, params: Optional[SonioxInputParams] = None, vad_force_turn_endpoint: bool = True, settings: Optional[SonioxSTTSettings] = None, @@ -204,6 +202,8 @@ class SonioxSTTService(WebsocketSTTService): .. deprecated:: 0.0.105 Use ``settings=SonioxSTTSettings(model=...)`` instead. + audio_format: Audio format for transcription. Defaults to ``"pcm_s16le"``. + num_channels: Number of audio channels. Defaults to 1. params: Additional configuration parameters, such as language hints, context and speaker diarization. @@ -218,12 +218,10 @@ class SonioxSTTService(WebsocketSTTService): Override for your deployment. See https://github.com/pipecat-ai/stt-benchmark **kwargs: Additional arguments passed to the STTService. """ - # 1. Initialize default_settings with hardcoded defaults + # --- 1. Hardcoded defaults --- default_settings = SonioxSTTSettings( model="stt-rt-v4", language=None, - audio_format="pcm_s16le", - num_channels=1, language_hints=None, language_hints_strict=None, context=None, @@ -232,18 +230,20 @@ class SonioxSTTService(WebsocketSTTService): client_reference_id=None, ) - # 2. Apply direct init arg overrides (deprecated) + # --- 2. Deprecated direct-arg overrides --- if model is not None: _warn_deprecated_param("model", SonioxSTTSettings, "model") default_settings.model = model - # 3. Apply params overrides — only if settings not provided + # --- 3. Deprecated params overrides --- if params is not None: _warn_deprecated_param("params", SonioxSTTSettings) if not settings: default_settings.model = params.model - default_settings.audio_format = params.audio_format - default_settings.num_channels = params.num_channels + if params.audio_format is not None: + audio_format = params.audio_format + if params.num_channels is not None: + num_channels = params.num_channels default_settings.language_hints = params.language_hints default_settings.language_hints_strict = params.language_hints_strict default_settings.context = params.context @@ -253,7 +253,7 @@ class SonioxSTTService(WebsocketSTTService): ) default_settings.client_reference_id = params.client_reference_id - # 4. Apply settings delta (canonical API, always wins) + # --- 4. Settings delta (canonical API, always wins) --- if settings is not None: default_settings.apply_update(settings) @@ -270,6 +270,10 @@ class SonioxSTTService(WebsocketSTTService): self._url = url self._vad_force_turn_endpoint = vad_force_turn_endpoint + # Init-only audio config + self._audio_format = audio_format + self._num_channels = num_channels + self._final_transcription_buffer = [] self._last_tokens_received: Optional[float] = None @@ -438,8 +442,8 @@ class SonioxSTTService(WebsocketSTTService): config = { "api_key": self._api_key, "model": s.model, - "audio_format": s.audio_format, - "num_channels": s.num_channels or 1, + "audio_format": self._audio_format, + "num_channels": self._num_channels, "enable_endpoint_detection": enable_endpoint_detection, "sample_rate": self.sample_rate, "language_hints": _prepare_language_hints(s.language_hints), diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index a0a5c8b64..b3ded9255 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -100,7 +100,6 @@ class SpeechmaticsSTTSettings(STTSettings): focus_mode: Speaker focus mode for diarization. known_speakers: List of known speaker labels and identifiers. additional_vocab: List of additional vocabulary entries. - audio_encoding: Audio encoding format. operating_point: Operating point for accuracy vs. latency. max_delay: Maximum delay in seconds for transcription. end_of_utterance_silence_trigger: Maximum delay for end of utterance trigger. @@ -126,7 +125,6 @@ class SpeechmaticsSTTSettings(STTSettings): additional_vocab: list[AdditionalVocabEntry] | _NotGiven = field( default_factory=lambda: NOT_GIVEN ) - audio_encoding: AudioEncoding | _NotGiven = field(default_factory=lambda: NOT_GIVEN) operating_point: OperatingPoint | _NotGiven = field(default_factory=lambda: NOT_GIVEN) max_delay: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) end_of_utterance_silence_trigger: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -344,8 +342,8 @@ class SpeechmaticsSTTService(STTService): class UpdateParams(BaseModel): """Update parameters for Speechmatics STT service. - These are the only parameters that can be changed once a session has started. If you need to - change the language, etc., then you must create a new instance of the service. + .. deprecated:: 0.0.104 + Use ``SpeechmaticsSTTSettings`` with ``STTUpdateSettingsFrame`` instead. Parameters: focus_speakers: List of speaker IDs to focus on. When enabled, only these speakers are @@ -379,6 +377,7 @@ class SpeechmaticsSTTService(STTService): api_key: str | None = None, base_url: str | None = None, sample_rate: int | None = None, + encoding: AudioEncoding = AudioEncoding.PCM_S16LE, params: InputParams | None = None, should_interrupt: bool = True, settings: SpeechmaticsSTTSettings | None = None, @@ -393,6 +392,7 @@ class SpeechmaticsSTTService(STTService): base_url: Base URL for Speechmatics API. Uses environment variable `SPEECHMATICS_RT_URL` or defaults to `wss://eu2.rt.speechmatics.com/v2`. sample_rate: Optional audio sample rate in Hz. + encoding: Audio encoding format. Defaults to ``AudioEncoding.PCM_S16LE``. params: Input parameters for the service. .. deprecated:: 0.0.105 @@ -423,7 +423,7 @@ class SpeechmaticsSTTService(STTService): _params = params or SpeechmaticsSTTService.InputParams() self._check_deprecated_args(kwargs, _params) - # 1. Initialize default_settings with hardcoded defaults + # --- 1. Hardcoded defaults --- default_settings = SpeechmaticsSTTSettings( model=None, # Will be resolved from operating_point after config is built language=Language.EN, @@ -436,7 +436,6 @@ class SpeechmaticsSTTService(STTService): focus_mode=SpeakerFocusMode.RETAIN, known_speakers=[], additional_vocab=[], - audio_encoding=AudioEncoding.PCM_S16LE, operating_point=None, max_delay=None, end_of_utterance_silence_trigger=None, @@ -451,9 +450,9 @@ class SpeechmaticsSTTService(STTService): extra_params=None, ) - # 2. No direct init arg overrides + # --- 2. No direct init arg overrides --- - # 3. Apply params overrides — only if settings not provided + # --- 3. Deprecated params overrides --- if params is not None: _warn_deprecated_param("params", SpeechmaticsSTTSettings) if not settings: @@ -475,7 +474,7 @@ class SpeechmaticsSTTService(STTService): default_settings.focus_mode = _params.focus_mode default_settings.known_speakers = _params.known_speakers default_settings.additional_vocab = _params.additional_vocab - default_settings.audio_encoding = _params.audio_encoding + encoding = _params.audio_encoding default_settings.operating_point = _params.operating_point default_settings.max_delay = _params.max_delay default_settings.end_of_utterance_silence_trigger = ( @@ -493,10 +492,11 @@ class SpeechmaticsSTTService(STTService): # Build SDK config from settings, then resolve model from operating_point self._client: VoiceAgentClient | None = None + self._audio_encoding = encoding self._config: VoiceAgentConfig = self._build_config(default_settings) default_settings.model = self._config.operating_point.value - # 4. Apply settings delta (canonical API, always wins) + # --- 4. Settings delta (canonical API, always wins) --- if settings is not None: default_settings.apply_update(settings) @@ -720,6 +720,9 @@ class SpeechmaticsSTTService(STTService): # Preset from turn detection mode config = VoiceAgentConfigPreset.load(s.turn_detection_mode.value) + # Audio encoding (init-only, stored as instance attribute) + config.audio_encoding = self._audio_encoding + # Language + domain language = s.language config.language = self._language_to_speechmatics_language(language) @@ -773,7 +776,7 @@ class SpeechmaticsSTTService(STTService): ) -> None: """Updates the speaker configuration. - .. deprecated:: + .. deprecated:: 0.0.104 Use ``STTUpdateSettingsFrame`` with ``SpeechmaticsSTTSettings(...)`` instead. diff --git a/src/pipecat/services/whisper/base_stt.py b/src/pipecat/services/whisper/base_stt.py index c2dde8acc..f89d7f5d0 100644 --- a/src/pipecat/services/whisper/base_stt.py +++ b/src/pipecat/services/whisper/base_stt.py @@ -10,15 +10,15 @@ This module provides common functionality for services implementing the Whisper interface, including language mapping, metrics generation, and error handling. """ -from dataclasses import dataclass, field -from typing import Any, AsyncGenerator, Optional +from dataclasses import dataclass +from typing import AsyncGenerator, Optional from loguru import logger from openai import AsyncOpenAI from openai.types.audio import Transcription from pipecat.frames.frames import ErrorFrame, Frame, TranscriptionFrame -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import STTSettings, _NotGiven, _warn_deprecated_param from pipecat.services.stt_latency import WHISPER_TTFS_P99 from pipecat.services.stt_service import SegmentedSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -31,15 +31,13 @@ class BaseWhisperSTTSettings(STTSettings): """Settings for Whisper API-based STT services. Parameters: - base_url: API base URL. prompt: Optional text to guide the model's style or continue a previous segment. temperature: Sampling temperature between 0 and 1. """ - base_url: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - prompt: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - temperature: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + prompt: str | None | _NotGiven = None + temperature: float | None | _NotGiven = None def language_to_whisper_language(language: Language) -> Optional[str]: @@ -185,7 +183,6 @@ class BaseWhisperSTTService(SegmentedSTTService): default_settings = BaseWhisperSTTSettings( model=None, language=None, - base_url=base_url, ) # --- 2. Deprecated direct-arg overrides --- @@ -214,32 +211,12 @@ class BaseWhisperSTTService(SegmentedSTTService): **kwargs, ) self._client = self._create_client(api_key, base_url) - self._language = self._settings.language - self._prompt = self._settings.prompt - self._temperature = self._settings.temperature self._include_prob_metrics = include_prob_metrics self._push_empty_transcripts = push_empty_transcripts def _create_client(self, api_key: Optional[str], base_url: Optional[str]): return AsyncOpenAI(api_key=api_key, base_url=base_url) - async def _update_settings(self, delta: STTSettings) -> dict[str, Any]: - """Apply a settings delta, syncing instance variables. - - Keeps ``_language``, ``_prompt``, and ``_temperature`` in sync with - the settings fields. - """ - changed = await super()._update_settings(delta) - - if "language" in changed: - self._language = self._settings.language - if "prompt" in changed: - self._prompt = self._settings.prompt - if "temperature" in changed: - self._temperature = self._settings.temperature - - return changed - def can_generate_metrics(self) -> bool: """Whether this service can generate processing metrics. @@ -289,7 +266,7 @@ class BaseWhisperSTTService(SegmentedSTTService): logger.warning("Received empty transcription from API") if text or self._push_empty_transcripts: - await self._handle_transcription(text, True, self._language) + await self._handle_transcription(text, True, self._settings.language) logger.debug(f"Transcription: [{text}]") yield TranscriptionFrame( text, diff --git a/src/pipecat/services/whisper/stt.py b/src/pipecat/services/whisper/stt.py index f3ac0c948..af96f92c0 100644 --- a/src/pipecat/services/whisper/stt.py +++ b/src/pipecat/services/whisper/stt.py @@ -179,13 +179,9 @@ class WhisperSTTSettings(STTSettings): """Settings for the local Whisper (Faster Whisper) STT service. Parameters: - device: Inference device ('cpu', 'cuda', or 'auto'). - compute_type: Compute type for inference ('default', 'int8', etc.). no_speech_prob: Probability threshold for filtering non-speech segments. """ - device: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - compute_type: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) no_speech_prob: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -217,8 +213,8 @@ class WhisperSTTService(SegmentedSTTService): self, *, model: Optional[str | Model] = None, - device: Optional[str] = None, - compute_type: Optional[str] = None, + device: str = "auto", + compute_type: str = "default", no_speech_prob: Optional[float] = None, language: Optional[Language] = None, settings: Optional[WhisperSTTSettings] = None, @@ -233,15 +229,9 @@ class WhisperSTTService(SegmentedSTTService): Use ``settings=WhisperSTTSettings(model=...)`` instead. device: The device to run inference on ('cpu', 'cuda', or 'auto'). - - .. deprecated:: 0.0.105 - Use ``settings=WhisperSTTSettings(device=...)`` instead. - - compute_type: The compute type for inference ('default', 'int8', 'int8_float16', etc.). - - .. deprecated:: 0.0.105 - Use ``settings=WhisperSTTSettings(compute_type=...)`` instead. - + Defaults to ``"auto"``. + compute_type: The compute type for inference ('default', 'int8', + 'int8_float16', etc.). Defaults to ``"default"``. no_speech_prob: Probability threshold for filtering out non-speech segments. .. deprecated:: 0.0.105 @@ -260,8 +250,6 @@ class WhisperSTTService(SegmentedSTTService): default_settings = WhisperSTTSettings( model=Model.DISTIL_MEDIUM_EN.value, language=Language.EN, - device="auto", - compute_type="default", no_speech_prob=0.4, ) @@ -269,12 +257,6 @@ class WhisperSTTService(SegmentedSTTService): if model is not None: _warn_deprecated_param("model", WhisperSTTSettings, "model") default_settings.model = model if isinstance(model, str) else model.value - if device is not None: - _warn_deprecated_param("device", WhisperSTTSettings, "device") - default_settings.device = device - if compute_type is not None: - _warn_deprecated_param("compute_type", WhisperSTTSettings, "compute_type") - default_settings.compute_type = compute_type if no_speech_prob is not None: _warn_deprecated_param("no_speech_prob", WhisperSTTSettings, "no_speech_prob") default_settings.no_speech_prob = no_speech_prob @@ -292,9 +274,11 @@ class WhisperSTTService(SegmentedSTTService): settings=default_settings, **kwargs, ) - self._device: str = self._settings.device - self._compute_type = self._settings.compute_type - self._no_speech_prob = self._settings.no_speech_prob + + # Init-only inference config + self._device = device + self._compute_type = compute_type + self._model: Optional[WhisperModel] = None self._load() @@ -373,7 +357,7 @@ class WhisperSTTService(SegmentedSTTService): ) text: str = "" for segment in segments: - if segment.no_speech_prob < self._no_speech_prob: + if segment.no_speech_prob < self._settings.no_speech_prob: text += f"{segment.text} " await self.stop_processing_metrics() @@ -471,9 +455,6 @@ class WhisperSTTServiceMLX(WhisperSTTService): **kwargs, ) - self._no_speech_prob = self._settings.no_speech_prob - self._temperature = self._settings.temperature - # No need to call _load() as MLX Whisper loads models on demand @override @@ -514,7 +495,7 @@ class WhisperSTTServiceMLX(WhisperSTTService): mlx_whisper.transcribe, audio_float, path_or_hf_repo=self._settings.model, - temperature=self._temperature, + temperature=self._settings.temperature, language=self._settings.language, ) text: str = "" @@ -523,7 +504,7 @@ class WhisperSTTServiceMLX(WhisperSTTService): if segment.get("compression_ratio", None) == 0.5555555555555556: continue - if segment.get("no_speech_prob", 0.0) < self._no_speech_prob: + if segment.get("no_speech_prob", 0.0) < self._settings.no_speech_prob: text += f"{segment.get('text', '')} " if len(text.strip()) == 0: diff --git a/tests/test_settings.py b/tests/test_settings.py index ce3e35af0..201e85745 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -328,8 +328,6 @@ class TestDeepgramSTTSettingsApplyUpdate: defaults = dict( model="nova-3-general", language="en", - encoding="linear16", - channels=1, interim_results=True, smart_format=False, punctuate=True, @@ -350,8 +348,8 @@ class TestDeepgramSTTSettingsApplyUpdate: assert current.punctuate is False assert "punctuate" in changed # Other fields are untouched - assert current.encoding == "linear16" - assert current.channels == 1 + assert current.model == "nova-3-general" + assert current.language == "en" def test_apply_update_model(self): """model field is updated directly.""" @@ -427,8 +425,6 @@ class TestDeepgramSTTSettingsFromMapping: current = DeepgramSTTSettings( model="nova-3-general", language="en", - encoding="linear16", - channels=1, interim_results=True, punctuate=True, profanity_filter=True, @@ -442,7 +438,6 @@ class TestDeepgramSTTSettingsFromMapping: assert current.punctuate is False assert current.diarize is True # Unchanged fields stay put - assert current.encoding == "linear16" assert current.model == "nova-3-general" assert "punctuate" in changed @@ -451,8 +446,6 @@ class TestDeepgramSTTSettingsFromMapping: current = DeepgramSTTSettings( model="nova-3-general", language="en", - encoding="linear16", - channels=1, ) raw = {"model": "nova-2"} @@ -474,16 +467,13 @@ class TestDeepgramSageMakerSTTSettings: store = DeepgramSageMakerSTTSettings( model="nova-3", language="en", - encoding="linear16", - channels=1, - punctuate=True, ) - delta = DeepgramSageMakerSTTSettings(punctuate=False) + delta = DeepgramSageMakerSTTSettings(model="nova-2") changed = store.apply_update(delta) - assert store.punctuate is False - assert store.encoding == "linear16" - assert "punctuate" in changed + assert store.model == "nova-2" + assert store.language == "en" + assert "model" in changed # --------------------------------------------------------------------------- @@ -499,17 +489,17 @@ class TestDeepgramSTTSettingsExtraSync: return DeepgramSTTService(api_key="test-key", sample_rate=16000, **kwargs) def test_extra_synced_to_declared_field_at_init(self): - """If LiveOptions has unknown params in _extra, they can be synced if they match fields.""" + """LiveOptions params that match declared fields are synced at init.""" from pipecat.services.deepgram.stt import LiveOptions - # Use **kwargs to pass undeclared params - live_options = LiveOptions(numerals=True) # 'numerals' goes into _extra + live_options = LiveOptions(numerals=True) svc = self._make_service(live_options=live_options) - # 'numerals' doesn't match a declared DeepgramSTTSettings field, - # so it should stay in extra - assert svc._settings.extra["numerals"] is True + # 'numerals' is a declared DeepgramSTTSettings field, + # so it should be promoted from extra to the declared field + assert svc._settings.numerals is True + assert "numerals" not in svc._settings.extra def test_declared_field_from_live_options(self): """LiveOptions fields that match DeepgramSTTSettings fields are applied.""" @@ -532,7 +522,7 @@ class TestDeepgramSTTSettingsExtraSync: raw_dict = { "diarize": True, # matches declared field "punctuate": False, # matches declared field - "numerals": True, # doesn't match - stays in extra + "custom_param": "value", # doesn't match - stays in extra } delta = DeepgramSTTSettings.from_mapping(raw_dict) @@ -541,7 +531,7 @@ class TestDeepgramSTTSettingsExtraSync: assert delta.diarize is True assert delta.punctuate is False # Unknown stays in extra - assert delta.extra["numerals"] is True + assert delta.extra["custom_param"] == "value" # Now simulate syncing (though from_mapping already routes correctly) delta._sync_extra_to_fields() @@ -549,7 +539,7 @@ class TestDeepgramSTTSettingsExtraSync: # Still the same - from_mapping already put them in the right place assert delta.diarize is True assert delta.punctuate is False - assert delta.extra["numerals"] is True + assert delta.extra["custom_param"] == "value" def test_sync_promotes_extra_to_field_when_not_given(self): """_sync_extra_to_fields promotes extra dict entries to declared fields.""" @@ -611,16 +601,17 @@ class TestDeepgramSTTSettingsExtraSync: """Unknown params (not matching fields) stay in extra and get forwarded.""" from pipecat.services.deepgram.stt import LiveOptions - # numerals isn't a declared field in DeepgramSTTSettings + # 'numerals' is now a declared field; 'custom_param' is not live_options = LiveOptions(numerals=True, custom_param="test") svc = self._make_service(live_options=live_options) - # Should be in extra - assert svc._settings.extra["numerals"] is True + # 'numerals' is a declared field, so it should be promoted + assert svc._settings.numerals is True + # 'custom_param' is unknown, so it stays in extra assert svc._settings.extra["custom_param"] == "test" - # And forwarded to kwargs + # Both forwarded to kwargs kwargs = svc._build_connect_kwargs() assert kwargs["numerals"] == "true" assert kwargs["custom_param"] == "test"