From fb12bf9b4c6ba80757a44bdff857add5d9538f98 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 15:57:43 -0400 Subject: [PATCH 01/18] Update LLMService docstrings --- src/pipecat/services/llm_service.py | 174 ++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 33 deletions(-) diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 5619cd35e..11a8eded7 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Base classes for Large Language Model services with function calling support.""" + import asyncio import inspect from dataclasses import dataclass @@ -41,9 +43,21 @@ FunctionCallHandler = Callable[["FunctionCallParams"], Awaitable[None]] # Type alias for a callback function that handles the result of an LLM function call. class FunctionCallResultCallback(Protocol): + """Protocol for function call result callbacks. + + Handles the result of an LLM function call execution. + """ + async def __call__( self, result: Any, *, properties: Optional[FunctionCallResultProperties] = None - ) -> None: ... + ) -> None: + """Call the result callback. + + Args: + result: The result of the function call. + properties: Optional properties for the result. + """ + ... @dataclass @@ -51,13 +65,12 @@ class FunctionCallParams: """Parameters for a function call. Attributes: - function_name (str): The name of the function being called. - arguments (Mapping[str, Any]): The arguments for the function. - tool_call_id (str): A unique identifier for the function call. - llm (LLMService): The LLMService instance being used. - context (OpenAILLMContext): The LLM context. - result_callback (FunctionCallResultCallback): Callback to handle the result of the function call. - + function_name: The name of the function being called. + tool_call_id: A unique identifier for the function call. + arguments: The arguments for the function. + llm: The LLMService instance being used. + context: The LLM context. + result_callback: Callback to handle the result of the function call. """ function_name: str @@ -70,14 +83,14 @@ class FunctionCallParams: @dataclass class FunctionCallRegistryItem: - """Represents an entry in our function call registry. This is what the user - registers. + """Represents an entry in the function call registry. + + This is what the user registers when calling register_function. Attributes: - function_name (Optional[str]): The name of the function. - handler (FunctionCallHandler): The handler for processing function call parameters. - cancel_on_interruption (bool): Flag indicating whether to cancel the call on interruption. - + function_name: The name of the function (None for catch-all handler). + handler: The handler for processing function call parameters. + cancel_on_interruption: Whether to cancel the call on interruption. """ function_name: Optional[str] @@ -87,16 +100,17 @@ class FunctionCallRegistryItem: @dataclass class FunctionCallRunnerItem: - """Represents an internal function call entry to our function call - runner. The runner executes function calls in order. + """Internal function call entry for the function call runner. + + The runner executes function calls in order. Attributes: - registry_name (Optional[str]): The function call name registration (could be None). - function_name (str): The name of the function. - tool_call_id (str): A unique identifier for the function call. - arguments (Mapping[str, Any]): The arguments for the function. - context (OpenAILLMContext): The LLM context. - + registry_item: The registry item containing handler information. + function_name: The name of the function. + tool_call_id: A unique identifier for the function call. + arguments: The arguments for the function. + context: The LLM context. + run_llm: Optional flag to control LLM execution after function call. """ registry_item: FunctionCallRegistryItem @@ -108,22 +122,32 @@ class FunctionCallRunnerItem: class LLMService(AIService): - """This is the base class for all LLM services. It handles function calling - registration and execution. The class also provides event handlers. + """Base class for all LLM services. - An event to know when an LLM service completion timeout occurs: + Handles function calling registration and execution with support for both + parallel and sequential execution modes. Provides event handlers for + completion timeouts and function call lifecycle events. - @task.event_handler("on_completion_timeout") - async def on_completion_timeout(service): - ... + Args: + run_in_parallel: Whether to run function calls in parallel or sequentially. + Defaults to True. + **kwargs: Additional arguments passed to the parent AIService. - And an event to know that function calls have been received from the LLM - service and that we are going to start executing them: + Event handlers: + on_completion_timeout: Called when an LLM completion timeout occurs. + on_function_calls_started: Called when function calls are received and + execution is about to start. - @task.event_handler("on_function_calls_started") - async def on_function_calls_started(service, function_calls: Sequence[FunctionCallFromLLM]): - ... + Example: + ```python + @task.event_handler("on_completion_timeout") + async def on_completion_timeout(service): + logger.warning("LLM completion timed out") + @task.event_handler("on_function_calls_started") + async def on_function_calls_started(service, function_calls): + logger.info(f"Starting {len(function_calls)} function calls") + ``` """ # OpenAILLMAdapter is used as the default adapter since it aligns with most LLM implementations. @@ -143,6 +167,11 @@ class LLMService(AIService): self._register_event_handler("on_completion_timeout") def get_llm_adapter(self) -> BaseLLMAdapter: + """Get the LLM adapter instance. + + Returns: + The adapter instance used for LLM communication. + """ return self._adapter def create_context_aggregator( @@ -152,24 +181,57 @@ class LLMService(AIService): user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(), assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(), ) -> Any: + """Create a context aggregator for managing LLM conversation context. + + Must be implemented by subclasses. + + Args: + context: The LLM context to create an aggregator for. + user_params: Parameters for user message aggregation. + assistant_params: Parameters for assistant message aggregation. + + Returns: + A context aggregator instance. + """ pass async def start(self, frame: StartFrame): + """Start the LLM service. + + Args: + frame: The start frame. + """ await super().start(frame) if not self._run_in_parallel: await self._create_sequential_runner_task() async def stop(self, frame: EndFrame): + """Stop the LLM service. + + Args: + frame: The end frame. + """ await super().stop(frame) if not self._run_in_parallel: await self._cancel_sequential_runner_task() async def cancel(self, frame: CancelFrame): + """Cancel the LLM service. + + Args: + frame: The cancel frame. + """ await super().cancel(frame) if not self._run_in_parallel: await self._cancel_sequential_runner_task() async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process a frame. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, StartInterruptionFrame): @@ -188,6 +250,18 @@ class LLMService(AIService): *, cancel_on_interruption: bool = True, ): + """Register a function handler for LLM function calls. + + Args: + function_name: The name of the function to handle. Use None to handle + all function calls with a catch-all handler. + handler: The function handler. Should accept a single FunctionCallParams + parameter. + start_callback: Legacy callback function (deprecated). Put initialization + code at the top of your handler instead. + cancel_on_interruption: Whether to cancel this function call when an + interruption occurs. Defaults to True. + """ # Registering a function with the function_name set to None will run # that handler for all functions self._functions[function_name] = FunctionCallRegistryItem( @@ -210,16 +284,38 @@ class LLMService(AIService): self._start_callbacks[function_name] = start_callback def unregister_function(self, function_name: Optional[str]): + """Remove a registered function handler. + + Args: + function_name: The name of the function handler to remove. + """ del self._functions[function_name] if self._start_callbacks[function_name]: del self._start_callbacks[function_name] def has_function(self, function_name: str): + """Check if a function handler is registered. + + Args: + function_name: The name of the function to check. + + Returns: + True if the function is registered or if a catch-all handler (None) + is registered. + """ if None in self._functions.keys(): return True return function_name in self._functions.keys() async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]): + """Execute a sequence of function calls from the LLM. + + Triggers the on_function_calls_started event and executes functions + either in parallel or sequentially based on the run_in_parallel setting. + + Args: + function_calls: The function calls to execute. + """ if len(function_calls) == 0: return @@ -272,6 +368,18 @@ class LLMService(AIService): text_content: Optional[str] = None, video_source: Optional[str] = None, ): + """Request an image from a user. + + Pushes a UserImageRequestFrame upstream to request an image from the + specified user. + + Args: + user_id: The ID of the user to request an image from. + function_name: Optional function name associated with the request. + tool_call_id: Optional tool call ID associated with the request. + text_content: Optional text content/context for the image request. + video_source: Optional video source identifier. + """ await self.push_frame( UserImageRequestFrame( user_id=user_id, From f622b281d0af13d3b0250aca8f546548ad1fa913 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 15:58:19 -0400 Subject: [PATCH 02/18] Make call_start_function a private function in llm_service --- src/pipecat/services/llm_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 11a8eded7..1a1ac3783 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -353,7 +353,7 @@ class LLMService(AIService): else: await self._sequential_runner_queue.put(runner_item) - async def call_start_function(self, context: OpenAILLMContext, function_name: str): + async def _call_start_function(self, context: OpenAILLMContext, function_name: str): if function_name in self._start_callbacks.keys(): await self._start_callbacks[function_name](function_name, self, context) elif None in self._start_callbacks.keys(): @@ -424,7 +424,7 @@ class LLMService(AIService): ) # NOTE(aleix): This needs to be removed after we remove the deprecation. - await self.call_start_function(runner_item.context, runner_item.function_name) + await self._call_start_function(runner_item.context, runner_item.function_name) # Push a function call in-progress downstream. This frame will let our # assistant context aggregator know that we are in the middle of a From ab1d2dbe6a2186f8dcd834512922e301bfafa805 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 16:24:44 -0400 Subject: [PATCH 03/18] Add STTService docstrings --- src/pipecat/services/stt_service.py | 99 ++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/src/pipecat/services/stt_service.py b/src/pipecat/services/stt_service.py index 5e57b3104..e659b403b 100644 --- a/src/pipecat/services/stt_service.py +++ b/src/pipecat/services/stt_service.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Base classes for Speech-to-Text services with continuous and segmented processing.""" + import io import wave from abc import abstractmethod @@ -26,7 +28,19 @@ from pipecat.transcriptions.language import Language class STTService(AIService): - """STTService is a base class for speech-to-text services.""" + """Base class for speech-to-text services. + + Provides common functionality for STT services including audio passthrough, + muting, settings management, and audio processing. Subclasses must implement + the run_stt method to provide actual speech recognition. + + Args: + audio_passthrough: Whether to pass audio frames downstream after processing. + Defaults to True. + sample_rate: The sample rate for audio input. If None, will be determined + from the start frame. + **kwargs: Additional arguments passed to the parent AIService. + """ def __init__( self, @@ -44,25 +58,59 @@ class STTService(AIService): @property def is_muted(self) -> bool: - """Returns whether the STT service is currently muted.""" + """Check if the STT service is currently muted. + + Returns: + True if the service is muted and will not process audio. + """ return self._muted @property def sample_rate(self) -> int: + """Get the current sample rate for audio processing. + + Returns: + The sample rate in Hz. + """ return self._sample_rate async def set_model(self, model: str): + """Set the speech recognition model. + + Args: + model: The name of the model to use for speech recognition. + """ self.set_model_name(model) async def set_language(self, language: Language): + """Set the language for speech recognition. + + Args: + language: The language to use for speech recognition. + """ pass @abstractmethod async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: - """Returns transcript as a string""" + """Run speech-to-text on the provided audio data. + + This method must be implemented by subclasses to provide actual speech + recognition functionality. + + Args: + audio: Raw audio bytes to transcribe. + + Yields: + Frame: Frames containing transcription results (typically TextFrame). + """ pass async def start(self, frame: StartFrame): + """Start the STT service. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._sample_rate = self._init_sample_rate or frame.audio_in_sample_rate @@ -80,13 +128,24 @@ class STTService(AIService): logger.warning(f"Unknown setting for STT service: {key}") async def process_audio_frame(self, frame: AudioRawFrame, direction: FrameDirection): + """Process an audio frame for speech recognition. + + Args: + frame: The audio frame to process. + direction: The direction of frame processing. + """ if self._muted: return await self.process_generator(self.run_stt(frame.audio)) async def process_frame(self, frame: Frame, direction: FrameDirection): - """Processes a frame of audio data, either buffering or transcribing it.""" + """Process frames, handling VAD events and audio segmentation. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, AudioRawFrame): @@ -106,14 +165,19 @@ class STTService(AIService): class SegmentedSTTService(STTService): - """SegmentedSTTService is an STTService that uses VAD events to detect - speech and will run speech-to-text on speech segments only, instead of a - continous stream. Since it uses VAD it means that VAD needs to be enabled in - the pipeline. + """STT service that processes speech in segments using VAD events. - This service always keeps a small audio buffer to take into account that VAD - events are delayed from when the user speech really starts. + Uses Voice Activity Detection (VAD) events to detect speech segments and runs + speech-to-text only on those segments, rather than continuously. + Requires VAD to be enabled in the pipeline to function properly. Maintains a + small audio buffer to account for the delay between actual speech start and + VAD detection. + + Args: + sample_rate: The sample rate for audio input. If None, will be determined + from the start frame. + **kwargs: Additional arguments passed to the parent STTService. """ def __init__(self, *, sample_rate: Optional[int] = None, **kwargs): @@ -125,10 +189,16 @@ class SegmentedSTTService(STTService): self._user_speaking = False async def start(self, frame: StartFrame): + """Start the segmented STT service and initialize audio buffer. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._audio_buffer_size_1s = self.sample_rate * 2 async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames, handling VAD events and audio segmentation.""" await super().process_frame(frame, direction) if isinstance(frame, UserStartedSpeakingFrame): @@ -162,6 +232,15 @@ class SegmentedSTTService(STTService): self._audio_buffer.clear() async def process_audio_frame(self, frame: AudioRawFrame, direction: FrameDirection): + """Process audio frames by buffering them for segmented transcription. + + Continuously buffers audio, growing the buffer while user is speaking and + maintaining a small buffer when not speaking to account for VAD delay. + + Args: + frame: The audio frame to process. + direction: The direction of frame processing. + """ # If the user is speaking the audio buffer will keep growing. self._audio_buffer += frame.audio From 33f3a4cea1c37bbd8e78305e180d65d76ddc30d4 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 17:20:21 -0400 Subject: [PATCH 04/18] Add TTSService docstrings --- src/pipecat/services/tts_service.py | 261 +++++++++++++++++++++++++--- 1 file changed, 234 insertions(+), 27 deletions(-) diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index 904f603a9..68b480de2 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Base classes for Text-to-speech services.""" + import asyncio from abc import abstractmethod from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional, Sequence, Tuple @@ -42,6 +44,28 @@ from pipecat.utils.time import seconds_to_nanoseconds class TTSService(AIService): + """Base class for text-to-speech services. + + Provides common functionality for TTS services including text aggregation, + filtering, audio generation, and frame management. Supports configurable + sentence aggregation, silence insertion, and frame processing control. + + Args: + aggregate_sentences: Whether to aggregate text into sentences before synthesis. + push_text_frames: Whether to push TextFrames and LLMFullResponseEndFrames. + push_stop_frames: Whether to automatically push TTSStoppedFrames. + stop_frame_timeout_s: Idle time before pushing TTSStoppedFrame when push_stop_frames is True. + push_silence_after_stop: Whether to push silence audio after TTSStoppedFrame. + silence_time_s: Duration of silence to push when push_silence_after_stop is True. + pause_frame_processing: Whether to pause frame processing during audio generation. + sample_rate: Output sample rate for generated audio. + text_aggregator: Custom text aggregator for processing incoming text. + text_filters: Sequence of text filters to apply after aggregation. + text_filter: Single text filter (deprecated, use text_filters). + transport_destination: Destination for generated audio frames. + **kwargs: Additional arguments passed to the parent AIService. + """ + def __init__( self, *, @@ -104,54 +128,113 @@ class TTSService(AIService): @property def sample_rate(self) -> int: + """Get the current sample rate for audio output. + + Returns: + The sample rate in Hz. + """ return self._sample_rate @property def chunk_size(self) -> int: - """This property indicates how much audio we download (from TTS services + """Get the recommended chunk size for audio streaming. + + This property indicates how much audio we download (from TTS services that require chunking) before we start pushing the first audio frame. This will make sure we download the rest of the audio while audio is being played without causing audio glitches (specially at the beginning). Of course, this will also depend on how fast the TTS service generates bytes. + Returns: + The recommended chunk size in bytes. """ CHUNK_SECONDS = 0.5 return int(self.sample_rate * CHUNK_SECONDS * 2) # 2 bytes/sample async def set_model(self, model: str): + """Set the TTS model to use. + + Args: + model: The name of the TTS model. + """ self.set_model_name(model) def set_voice(self, voice: str): + """Set the voice for speech synthesis. + + Args: + voice: The voice identifier or name. + """ self._voice_id = voice # Converts the text to audio. @abstractmethod async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: + """Run text-to-speech synthesis on the provided text. + + This method must be implemented by subclasses to provide actual TTS functionality. + + Args: + text: The text to synthesize into speech. + + Yields: + Frame: Audio frames containing the synthesized speech. + """ pass def language_to_service_language(self, language: Language) -> Optional[str]: + """Convert a language to the service-specific language format. + + Args: + language: The language to convert. + + Returns: + The service-specific language identifier, or None if not supported. + """ return Language(language) async def update_setting(self, key: str, value: Any): + """Update a service-specific setting. + + Args: + key: The setting key to update. + value: The new value for the setting. + """ pass async def flush_audio(self): + """Flush any buffered audio data.""" pass async def start(self, frame: StartFrame): + """Start the TTS service. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._sample_rate = self._init_sample_rate or frame.audio_out_sample_rate if self._push_stop_frames and not self._stop_frame_task: self._stop_frame_task = self.create_task(self._stop_frame_handler()) async def stop(self, frame: EndFrame): + """Stop the TTS service. + + Args: + frame: The end frame. + """ await super().stop(frame) if self._stop_frame_task: await self.cancel_task(self._stop_frame_task) self._stop_frame_task = None async def cancel(self, frame: CancelFrame): + """Cancel the TTS service. + + Args: + frame: The cancel frame. + """ await super().cancel(frame) if self._stop_frame_task: await self.cancel_task(self._stop_frame_task) @@ -175,9 +258,23 @@ class TTSService(AIService): logger.warning(f"Unknown setting for TTS service: {key}") async def say(self, text: str): + """Immediately speak the provided text. + + Args: + text: The text to speak. + """ await self.queue_frame(TTSSpeakFrame(text)) async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames for text-to-speech conversion. + + Handles TextFrames for synthesis, interruption frames, settings updates, + and various control frames. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if ( @@ -222,6 +319,12 @@ class TTSService(AIService): await self.push_frame(frame, direction) async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM): + """Push a frame downstream with TTS-specific handling. + + Args: + frame: The frame to push. + direction: The direction to push the frame. + """ if self._push_silence_after_stop and isinstance(frame, TTSStoppedFrame): silence_num_bytes = int(self._silence_time_s * self.sample_rate * 2) # 16-bit silence_frame = TTSAudioRawFrame( @@ -318,10 +421,13 @@ class TTSService(AIService): class WordTTSService(TTSService): - """This is a base class for TTS services that support word timestamps. Word - timestamps are useful to synchronize audio with text of the spoken + """Base class for TTS services that support word timestamps. + + Word timestamps are useful to synchronize audio with text of the spoken words. This way only the spoken words are added to the conversation context. + Args: + **kwargs: Additional arguments passed to the parent TTSService. """ def __init__(self, **kwargs): @@ -332,29 +438,57 @@ class WordTTSService(TTSService): self._llm_response_started: bool = False def start_word_timestamps(self): + """Start tracking word timestamps from the current time.""" if self._initial_word_timestamp == -1: self._initial_word_timestamp = self.get_clock().get_time() def reset_word_timestamps(self): + """Reset word timestamp tracking.""" self._initial_word_timestamp = -1 async def add_word_timestamps(self, word_times: List[Tuple[str, float]]): + """Add word timestamps to the processing queue. + + Args: + word_times: List of (word, timestamp) tuples where timestamp is in seconds. + """ for word, timestamp in word_times: await self._words_queue.put((word, seconds_to_nanoseconds(timestamp))) async def start(self, frame: StartFrame): + """Start the word TTS service. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._create_words_task() async def stop(self, frame: EndFrame): + """Stop the word TTS service. + + Args: + frame: The end frame. + """ await super().stop(frame) await self._stop_words_task() async def cancel(self, frame: CancelFrame): + """Cancel the word TTS service. + + Args: + frame: The cancel frame. + """ await super().cancel(frame) await self._stop_words_task() async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames with word timestamp awareness. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, LLMFullResponseStartFrame): @@ -400,15 +534,24 @@ class WordTTSService(TTSService): class WebsocketTTSService(TTSService, WebsocketService): - """This is a base class for websocket-based TTS services. + """Base class for websocket-based TTS services. - If an error occurs with the websocket, an "on_connection_error" event will - be triggered: + Combines TTS functionality with websocket connectivity, providing automatic + error handling and reconnection capabilities. - @tts.event_handler("on_connection_error") - async def on_connection_error(tts: TTSService, error: str): - ... + Args: + reconnect_on_error: Whether to automatically reconnect on websocket errors. + **kwargs: Additional arguments passed to parent classes. + Event handlers: + on_connection_error: Called when a websocket connection error occurs. + + Example: + ```python + @tts.event_handler("on_connection_error") + async def on_connection_error(tts: TTSService, error: str): + logger.error(f"TTS connection error: {error}") + ``` """ def __init__(self, *, reconnect_on_error: bool = True, **kwargs): @@ -422,10 +565,13 @@ class WebsocketTTSService(TTSService, WebsocketService): class InterruptibleTTSService(WebsocketTTSService): - """This is a base class for websocket-based TTS services that don't support - word timestamps and that don't offer a way to correlate the generated audio - to the requested text. + """Websocket-based TTS service that handles interruptions without word timestamps. + Designed for TTS services that don't support word timestamps. Handles interruptions + by reconnecting the websocket when the bot is speaking and gets interrupted. + + Args: + **kwargs: Additional arguments passed to the parent WebsocketTTSService. """ def __init__(self, **kwargs): @@ -443,6 +589,12 @@ class InterruptibleTTSService(WebsocketTTSService): await self._connect() async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames with bot speaking state tracking. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, BotStartedSpeakingFrame): @@ -452,16 +604,23 @@ class InterruptibleTTSService(WebsocketTTSService): class WebsocketWordTTSService(WordTTSService, WebsocketService): - """This is a base class for websocket-based TTS services that support word - timestamps. + """Base class for websocket-based TTS services that support word timestamps. - If an error occurs with the websocket a "on_connection_error" event will be - triggered: + Combines word timestamp functionality with websocket connectivity. - @tts.event_handler("on_connection_error") - async def on_connection_error(tts: TTSService, error: str): - ... + Args: + reconnect_on_error: Whether to automatically reconnect on websocket errors. + **kwargs: Additional arguments passed to parent classes. + Event handlers: + on_connection_error: Called when a websocket connection error occurs. + + Example: + ```python + @tts.event_handler("on_connection_error") + async def on_connection_error(tts: TTSService, error: str): + logger.error(f"TTS connection error: {error}") + ``` """ def __init__(self, *, reconnect_on_error: bool = True, **kwargs): @@ -475,10 +634,13 @@ class WebsocketWordTTSService(WordTTSService, WebsocketService): class InterruptibleWordTTSService(WebsocketWordTTSService): - """This is a base class for websocket-based TTS services that support word - timestamps but don't offer a way to correlate the generated audio to the - requested text. + """Websocket-based TTS service with word timestamps that handles interruptions. + For TTS services that support word timestamps but can't correlate generated + audio with requested text. Handles interruptions by reconnecting when needed. + + Args: + **kwargs: Additional arguments passed to the parent WebsocketWordTTSService. """ def __init__(self, **kwargs): @@ -496,6 +658,12 @@ class InterruptibleWordTTSService(WebsocketWordTTSService): await self._connect() async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames with bot speaking state tracking. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, BotStartedSpeakingFrame): @@ -505,7 +673,9 @@ class InterruptibleWordTTSService(WebsocketWordTTSService): class AudioContextWordTTSService(WebsocketWordTTSService): - """This is a base class for websocket-based TTS services that support word + """Websocket-based TTS service with word timestamps and audio context management. + + This is a base class for websocket-based TTS services that support word timestamps and also allow correlating the generated audio with the requested text. @@ -517,6 +687,8 @@ class AudioContextWordTTSService(WebsocketWordTTSService): we requested audio for a context "A" and then audio for context "B", the audio from context ID "A" will be played first. + Args: + **kwargs: Additional arguments passed to the parent WebsocketWordTTSService. """ def __init__(self, **kwargs): @@ -526,13 +698,22 @@ class AudioContextWordTTSService(WebsocketWordTTSService): self._audio_context_task = None async def create_audio_context(self, context_id: str): - """Create a new audio context.""" + """Create a new audio context for grouping related audio. + + Args: + context_id: Unique identifier for the audio context. + """ await self._contexts_queue.put(context_id) self._contexts[context_id] = asyncio.Queue() logger.trace(f"{self} created audio context {context_id}") async def append_to_audio_context(self, context_id: str, frame: TTSAudioRawFrame): - """Append audio to an existing context.""" + """Append audio to an existing context. + + Args: + context_id: The context to append audio to. + frame: The audio frame to append. + """ if self.audio_context_available(context_id): logger.trace(f"{self} appending audio {frame} to audio context {context_id}") await self._contexts[context_id].put(frame) @@ -540,7 +721,11 @@ class AudioContextWordTTSService(WebsocketWordTTSService): logger.warning(f"{self} unable to append audio to context {context_id}") async def remove_audio_context(self, context_id: str): - """Remove an existing audio context.""" + """Remove an existing audio context. + + Args: + context_id: The context to remove. + """ if self.audio_context_available(context_id): # We just mark the audio context for deletion by appending # None. Once we reach None while handling audio we know we can @@ -551,14 +736,31 @@ class AudioContextWordTTSService(WebsocketWordTTSService): logger.warning(f"{self} unable to remove context {context_id}") def audio_context_available(self, context_id: str) -> bool: - """Checks whether the given audio context is registered.""" + """Check whether the given audio context is registered. + + Args: + context_id: The context ID to check. + + Returns: + True if the context exists and is available. + """ return context_id in self._contexts async def start(self, frame: StartFrame): + """Start the audio context TTS service. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._create_audio_context_task() async def stop(self, frame: EndFrame): + """Stop the audio context TTS service. + + Args: + frame: The end frame. + """ await super().stop(frame) if self._audio_context_task: # Indicate no more audio contexts are available. this will end the @@ -568,6 +770,11 @@ class AudioContextWordTTSService(WebsocketWordTTSService): self._audio_context_task = None async def cancel(self, frame: CancelFrame): + """Cancel the audio context TTS service. + + Args: + frame: The cancel frame. + """ await super().cancel(frame) await self._stop_audio_context_task() From 691999b402921c430b67812da9d148b616ba975a Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 17:22:09 -0400 Subject: [PATCH 05/18] Add AIServices docstring --- src/pipecat/services/ai_services.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pipecat/services/ai_services.py b/src/pipecat/services/ai_services.py index 1a1e1ab56..833a20eae 100644 --- a/src/pipecat/services/ai_services.py +++ b/src/pipecat/services/ai_services.py @@ -4,6 +4,17 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Deprecated AI services module. + +This module is deprecated. Import services directly from their respective modules: +- pipecat.services.ai_service +- pipecat.services.image_service +- pipecat.services.llm_service +- pipecat.services.stt_service +- pipecat.services.tts_service +- pipecat.services.vision_service +""" + import sys from pipecat.services import DeprecatedModuleProxy From a1e5a1eff40406d758efb3ab1c3022ecd8ea97bc Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 17:24:37 -0400 Subject: [PATCH 06/18] Add AIService docstrings --- src/pipecat/services/ai_service.py | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/pipecat/services/ai_service.py b/src/pipecat/services/ai_service.py index 985b61c8e..175673e43 100644 --- a/src/pipecat/services/ai_service.py +++ b/src/pipecat/services/ai_service.py @@ -4,6 +4,12 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Base AI service implementation. + +Provides the foundation for all AI services in the Pipecat framework, including +model management, settings handling, and frame processing lifecycle methods. +""" + from typing import Any, AsyncGenerator, Dict, Mapping from loguru import logger @@ -20,6 +26,17 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor class AIService(FrameProcessor): + """Base class for all AI services. + + Provides common functionality for AI services including model management, + settings handling, session properties, and frame processing lifecycle. + Subclasses should implement specific AI functionality while leveraging + this base infrastructure. + + Args: + **kwargs: Additional arguments passed to the parent FrameProcessor. + """ + def __init__(self, **kwargs): super().__init__(**kwargs) self._model_name: str = "" @@ -28,19 +45,53 @@ class AIService(FrameProcessor): @property def model_name(self) -> str: + """Get the current model name. + + Returns: + The name of the AI model being used. + """ return self._model_name def set_model_name(self, model: str): + """Set the AI model name and update metrics. + + Args: + model: The name of the AI model to use. + """ self._model_name = model self.set_core_metrics_data(MetricsData(processor=self.name, model=self._model_name)) async def start(self, frame: StartFrame): + """Start the AI service. + + Called when the service should begin processing. Subclasses should + override this method to perform service-specific initialization. + + Args: + frame: The start frame containing initialization parameters. + """ pass async def stop(self, frame: EndFrame): + """Stop the AI service. + + Called when the service should stop processing. Subclasses should + override this method to perform cleanup operations. + + Args: + frame: The end frame. + """ pass async def cancel(self, frame: CancelFrame): + """Cancel the AI service. + + Called when the service should cancel all operations. Subclasses should + override this method to handle cancellation logic. + + Args: + frame: The cancel frame. + """ pass async def _update_settings(self, settings: Mapping[str, Any]): @@ -87,6 +138,15 @@ class AIService(FrameProcessor): logger.warning(f"Unknown setting for {self.name} service: {key}") async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames and handle service lifecycle. + + Automatically handles StartFrame, EndFrame, and CancelFrame by calling + the appropriate lifecycle methods. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, StartFrame): @@ -97,6 +157,14 @@ class AIService(FrameProcessor): await self.stop(frame) async def process_generator(self, generator: AsyncGenerator[Frame | None, None]): + """Process frames from an async generator. + + Takes an async generator that yields frames and processes each one, + handling error frames specially by pushing them as errors. + + Args: + generator: An async generator that yields Frame objects or None. + """ async for f in generator: if f: if isinstance(f, ErrorFrame): From 2007ae43170a9c1aba1bee459921b24de6b16e51 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 17:40:04 -0400 Subject: [PATCH 07/18] Add ImageGenService docstrings --- src/pipecat/services/image_service.py | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/pipecat/services/image_service.py b/src/pipecat/services/image_service.py index 43dbd0bb5..27084f1d1 100644 --- a/src/pipecat/services/image_service.py +++ b/src/pipecat/services/image_service.py @@ -4,6 +4,12 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Image generation service implementation. + +Provides base functionality for AI-powered image generation services that convert +text prompts into images. +""" + from abc import abstractmethod from typing import AsyncGenerator @@ -13,15 +19,46 @@ from pipecat.services.ai_service import AIService class ImageGenService(AIService): + """Base class for image generation services. + + Processes TextFrames by using their content as prompts for image generation. + Subclasses must implement the run_image_gen method to provide actual image + generation functionality using their specific AI service. + + Args: + **kwargs: Additional arguments passed to the parent AIService. + """ + def __init__(self, **kwargs): super().__init__(**kwargs) # Renders the image. Returns an Image object. @abstractmethod async def run_image_gen(self, prompt: str) -> AsyncGenerator[Frame, None]: + """Generate an image from a text prompt. + + This method must be implemented by subclasses to provide actual image + generation functionality using their specific AI service. + + Args: + prompt: The text prompt to generate an image from. + + Yields: + Frame: Frames containing the generated image (typically ImageRawFrame + or URLImageRawFrame). + """ pass async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames for image generation. + + TextFrames are used as prompts for image generation, while other frames + are passed through unchanged. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, TextFrame): From f80f62c7d12b875918a3a36f5164eb6af58f1358 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 17:43:31 -0400 Subject: [PATCH 08/18] Add VisionService docstrings --- src/pipecat/services/vision_service.py | 39 +++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/vision_service.py b/src/pipecat/services/vision_service.py index 23eb79c4e..d4314f874 100644 --- a/src/pipecat/services/vision_service.py +++ b/src/pipecat/services/vision_service.py @@ -4,6 +4,13 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Vision service implementation. + +Provides base classes and implementations for computer vision services that can +analyze images and generate textual descriptions or answers to questions about +visual content. +""" + from abc import abstractmethod from typing import AsyncGenerator @@ -13,7 +20,15 @@ from pipecat.services.ai_service import AIService class VisionService(AIService): - """VisionService is a base class for vision services.""" + """Base class for vision services. + + Provides common functionality for vision services that process images and + generate textual responses. Handles image frame processing and integrates + with the AI service infrastructure for metrics and lifecycle management. + + Args: + **kwargs: Additional arguments passed to the parent AIService. + """ def __init__(self, **kwargs): super().__init__(**kwargs) @@ -21,9 +36,31 @@ class VisionService(AIService): @abstractmethod async def run_vision(self, frame: VisionImageRawFrame) -> AsyncGenerator[Frame, None]: + """Process a vision image frame and generate results. + + This method must be implemented by subclasses to provide actual computer + vision functionality such as image description, object detection, or + visual question answering. + + Args: + frame: The vision image frame to process, containing image data. + + Yields: + Frame: Frames containing the vision analysis results, typically TextFrame + objects with descriptions or answers. + """ pass async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames, handling vision image frames for analysis. + + Automatically processes VisionImageRawFrame objects by calling run_vision + and handles metrics tracking. Other frames are passed through unchanged. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, VisionImageRawFrame): From bb3bb8d9c6e2e5668da8e3ba238898acd4bb1eca Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 17:48:43 -0400 Subject: [PATCH 09/18] Improve WebsocketService docstrings --- src/pipecat/services/websocket_service.py | 66 ++++++++++++++++------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/src/pipecat/services/websocket_service.py b/src/pipecat/services/websocket_service.py index d757946f9..6fc8efb2e 100644 --- a/src/pipecat/services/websocket_service.py +++ b/src/pipecat/services/websocket_service.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Base websocket service with automatic reconnection and error handling.""" + import asyncio from abc import ABC, abstractmethod from typing import Awaitable, Callable, Optional @@ -17,18 +19,26 @@ from pipecat.utils.network import exponential_backoff_time class WebsocketService(ABC): - """Base class for websocket-based services with reconnection logic.""" + """Base class for websocket-based services with automatic reconnection. + + Provides websocket connection management, automatic reconnection with + exponential backoff, connection verification, and error handling. + Subclasses implement service-specific connection and message handling logic. + + Args: + reconnect_on_error: Whether to automatically reconnect on connection errors. + **kwargs: Additional arguments (unused, for compatibility). + """ def __init__(self, *, reconnect_on_error: bool = True, **kwargs): - """Initialize websocket attributes.""" self._websocket: Optional[websockets.WebSocketClientProtocol] = None self._reconnect_on_error = reconnect_on_error async def _verify_connection(self) -> bool: - """Verify websocket connection is working. + """Verify the websocket connection is active and responsive. Returns: - bool: True if connection is verified working, False otherwise + True if connection is verified working, False otherwise. """ try: if not self._websocket or self._websocket.closed: @@ -40,13 +50,13 @@ class WebsocketService(ABC): return False async def _reconnect_websocket(self, attempt_number: int) -> bool: - """Reconnect the websocket. + """Reconnect the websocket with the current attempt number. Args: - attempt_number: Current retry attempt number + attempt_number: Current retry attempt number for logging. Returns: - bool: True if reconnection and verification successful, False otherwise + True if reconnection and verification successful, False otherwise. """ logger.warning(f"{self} reconnecting (attempt: {attempt_number})") await self._disconnect_websocket() @@ -54,10 +64,14 @@ class WebsocketService(ABC): return await self._verify_connection() async def _receive_task_handler(self, report_error: Callable[[ErrorFrame], Awaitable[None]]): - """Handles WebSocket message receiving with automatic retry logic. + """Handle websocket message receiving with automatic retry logic. + + Continuously receives messages with automatic reconnection on errors. + Uses exponential backoff between retry attempts and reports fatal errors + after maximum retries are exhausted. Args: - report_error: Callback to report errors + report_error: Callback function to report connection errors. """ retry_count = 0 MAX_RETRIES = 3 @@ -98,33 +112,45 @@ class WebsocketService(ABC): @abstractmethod async def _connect(self): - """Implement service-specific connection logic. This function will - connect to the websocket via _connect_websocket() among other connection - logic.""" + """Connect to the service. + + Implement service-specific connection logic including websocket connection + via _connect_websocket() and any additional setup required. + """ pass @abstractmethod async def _disconnect(self): - """Implement service-specific disconnection logic. This function will - disconnect to the websocket via _connect_websocket() among other - connection logic. + """Disconnect from the service. + Implement service-specific disconnection logic including websocket + disconnection via _disconnect_websocket() and any cleanup required. """ pass @abstractmethod async def _connect_websocket(self): - """Implement service-specific websocket connection logic. This function - should only connect to the websocket.""" + """Establish the websocket connection. + + Implement the low-level websocket connection logic specific to the service. + Should only handle websocket connection, not additional service setup. + """ pass @abstractmethod async def _disconnect_websocket(self): - """Implement service-specific websocket disconnection logic. This - function should only disconnect from the websocket.""" + """Close the websocket connection. + + Implement the low-level websocket disconnection logic specific to the service. + Should only handle websocket disconnection, not additional service cleanup. + """ pass @abstractmethod async def _receive_messages(self): - """Implement service-specific message receiving logic.""" + """Receive and process websocket messages. + + Implement service-specific logic for receiving and handling messages + from the websocket connection. Called continuously by the receive task handler. + """ pass From 04b70ddf131a400ac8ac45b0c584385d2d250f6b Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 17:51:42 -0400 Subject: [PATCH 10/18] Add MCPClient docstrings --- src/pipecat/services/mcp_service.py | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/mcp_service.py b/src/pipecat/services/mcp_service.py index a644d8f1b..48b0f9f1d 100644 --- a/src/pipecat/services/mcp_service.py +++ b/src/pipecat/services/mcp_service.py @@ -1,3 +1,11 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""MCP (Model Context Protocol) client for integrating external tools with LLMs.""" + import json from typing import Any, Dict, List, Optional, Union @@ -19,6 +27,20 @@ except ModuleNotFoundError as e: class MCPClient(BaseObject): + """Client for Model Context Protocol (MCP) servers. + + Enables integration with MCP servers to provide external tools and resources + to LLMs. Supports both stdio and SSE server connections with automatic tool + registration and schema conversion. + + Args: + server_params: Server connection parameters (stdio or SSE). + **kwargs: Additional arguments passed to the parent BaseObject. + + Raises: + TypeError: If server_params is not a supported parameter type. + """ + def __init__( self, server_params: Union[StdioServerParameters, SseServerParameters], @@ -39,6 +61,17 @@ class MCPClient(BaseObject): ) async def register_tools(self, llm) -> ToolsSchema: + """Register all available MCP tools with an LLM service. + + Connects to the MCP server, discovers available tools, converts their + schemas to Pipecat format, and registers them with the LLM service. + + Args: + llm: The Pipecat LLM service to register tools with. + + Returns: + A ToolsSchema containing all successfully registered tools. + """ tools_schema = await self._register_tools(llm) return tools_schema @@ -46,13 +79,13 @@ class MCPClient(BaseObject): self, tool_name: str, tool_schema: Dict[str, Any] ) -> FunctionSchema: """Convert an mcp tool schema to Pipecat's FunctionSchema format. + Args: tool_name: The name of the tool tool_schema: The mcp tool schema Returns: A FunctionSchema instance """ - logger.debug(f"Converting schema for tool '{tool_name}'") logger.trace(f"Original schema: {json.dumps(tool_schema, indent=2)}") @@ -72,6 +105,7 @@ class MCPClient(BaseObject): async def _sse_register_tools(self, llm) -> ToolsSchema: """Register all available mcp.run tools with the LLM service. + Args: llm: The Pipecat LLM service to register tools with Returns: @@ -120,6 +154,7 @@ class MCPClient(BaseObject): async def _stdio_register_tools(self, llm) -> ToolsSchema: """Register all available mcp.run tools with the LLM service. + Args: llm: The Pipecat LLM service to register tools with Returns: From cc66fddca97c7f3e134a0b13f5327a3dc7323d5d Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 19:55:51 -0400 Subject: [PATCH 11/18] Modify docs auto-gen rules to remove duplicate parameters listing --- CONTRIBUTING.md | 76 ++++++++++++++++++++++++++++++++++++------------ docs/api/conf.py | 3 +- pyproject.toml | 3 +- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fee75bd0..677dc6b7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,36 +41,76 @@ We use Ruff for code linting and formatting. Please ensure your code passes all We follow Google-style docstrings with these specific conventions: -- Class docstrings should fully document all parameters used in `__init__` -- We don't require separate docstrings for `__init__` methods when parameters are documented in the class docstring -- Property methods should have docstrings explaining their purpose and return value +**Regular Classes:** -Example of correctly documented class: +- Class docstring describes the class purpose and documents all `__init__` parameters in an `Args:` section +- No separate `__init__` docstring needed +- All public methods must have docstrings with `Args:` and `Returns:` sections as appropriate + +**Dataclasses:** + +- Class docstring describes the purpose and documents all fields in a `Parameters:` section +- No `__init__` docstring (auto-generated) + +**Properties:** + +- Must have docstrings with `Returns:` section + +**Abstract Methods:** + +- Must have docstrings explaining what subclasses should implement + +#### Examples: ```python -class MyClass: - """Class description. - - Additional details about the class. +# Regular class +class MyService(BaseService): + """Description of what the service does. Args: - param1: Description of first parameter. - param2: Description of second parameter. + param1: Description of param1. + param2: Description of param2. Defaults to True. + **kwargs: Additional arguments passed to parent. """ - def __init__(self, param1, param2): - # No docstring required here as parameters are documented above - self.param1 = param1 - self.param2 = param2 + def __init__(self, param1: str, param2: bool = True, **kwargs): + # No docstring - parameters documented above + super().__init__(**kwargs) @property - def some_property(self) -> str: - """Get the formatted property value. + def sample_rate(self) -> int: + """Get the current sample rate. Returns: - A string representation of the property. + The sample rate in Hz. """ - return f"Property: {self.param1}" + return self._sample_rate + + async def process_data(self, data: str) -> bool: + """Process the provided data. + + Args: + data: The data to process. + + Returns: + True if processing succeeded. + """ + pass + +# Dataclass +@dataclass +class ConfigParams: + """Configuration parameters for the service. + + Parameters: + host: The host address. + port: The port number. Defaults to 8080. + timeout: Connection timeout in seconds. + """ + + host: str + port: int = 8080 + timeout: float = 30.0 ``` # Contributor Covenant Code of Conduct diff --git a/docs/api/conf.py b/docs/api/conf.py index a33caa10c..fee337eff 100644 --- a/docs/api/conf.py +++ b/docs/api/conf.py @@ -27,13 +27,12 @@ extensions = [ # Napoleon settings napoleon_google_docstring = True napoleon_numpy_docstring = False -napoleon_include_init_with_doc = True +napoleon_include_init_with_doc = False # AutoDoc settings autodoc_default_options = { "members": True, "member-order": "bysource", - "special-members": "__init__", "undoc-members": True, "exclude-members": "__weakref__", "no-index": True, diff --git a/pyproject.toml b/pyproject.toml index f7c73d49a..8fdab4742 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,8 +123,7 @@ select = [ "D", # Docstring rules "I", # Import rules ] -# We ignore D107 because class docstrings already document __init__ parameters -# and our Sphinx configuration uses napoleon_include_init_with_doc=True +# Ignore requirement for __init__ docstrings ignore = ["D107"] [tool.ruff.lint.pydocstyle] From fe6bbdaefe24b6c8a1269f5502dccdf13f37528d Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 22:23:51 -0400 Subject: [PATCH 12/18] Skip dataclass attributes to remove duplicate entries --- docs/api/conf.py | 2 +- src/pipecat/services/llm_service.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api/conf.py b/docs/api/conf.py index fee337eff..97ab73bc7 100644 --- a/docs/api/conf.py +++ b/docs/api/conf.py @@ -34,7 +34,7 @@ autodoc_default_options = { "members": True, "member-order": "bysource", "undoc-members": True, - "exclude-members": "__weakref__", + "exclude-members": "__weakref__,__init__", "no-index": True, "show-inheritance": True, } diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 1a1ac3783..f7779df98 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -64,7 +64,7 @@ class FunctionCallResultCallback(Protocol): class FunctionCallParams: """Parameters for a function call. - Attributes: + Parameters: function_name: The name of the function being called. tool_call_id: A unique identifier for the function call. arguments: The arguments for the function. @@ -87,7 +87,7 @@ class FunctionCallRegistryItem: This is what the user registers when calling register_function. - Attributes: + Parameters: function_name: The name of the function (None for catch-all handler). handler: The handler for processing function call parameters. cancel_on_interruption: Whether to cancel the call on interruption. @@ -104,7 +104,7 @@ class FunctionCallRunnerItem: The runner executes function calls in order. - Attributes: + Parameters: registry_item: The registry item containing handler information. function_name: The name of the function. tool_call_id: A unique identifier for the function call. From 6ef2ae12b7f1f1c7c3edb421dd655ff6bbdd0fd9 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 23:15:45 -0400 Subject: [PATCH 13/18] Mock mcp imports --- docs/api/conf.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/api/conf.py b/docs/api/conf.py index 97ab73bc7..bc518e3ad 100644 --- a/docs/api/conf.py +++ b/docs/api/conf.py @@ -144,6 +144,28 @@ autodoc_mock_imports = [ "transformers.AutoFeatureExtractor", # Also add specific classes that are imported "AutoFeatureExtractor", + # Sentry dependencies + "sentry_sdk", + # AWS Nova Sonic dependencies + "aws_sdk_bedrock_runtime", + "aws_sdk_bedrock_runtime.client", + "aws_sdk_bedrock_runtime.config", + "aws_sdk_bedrock_runtime.models", + "smithy_aws_core", + "smithy_aws_core.credentials_resolvers", + "smithy_aws_core.credentials_resolvers.static", + "smithy_aws_core.identity", + "smithy_core", + "smithy_core.aio", + "smithy_core.aio.eventstream", + # MCP dependencies (you may already have these) + "mcp", + "mcp.client", + "mcp.client.session_group", + "mcp.client.sse", + "mcp.client.stdio", + "mcp.ClientSession", + "mcp.StdioServerParameters", ] # HTML output settings From f04e058c96095834914796a820da96dae1f41f8a Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 23:18:33 -0400 Subject: [PATCH 14/18] Programmatically set the copyright date in docs --- docs/api/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api/conf.py b/docs/api/conf.py index bc518e3ad..86a40a871 100644 --- a/docs/api/conf.py +++ b/docs/api/conf.py @@ -1,5 +1,6 @@ import logging import sys +from datetime import datetime from pathlib import Path # Configure logging @@ -13,7 +14,8 @@ sys.path.insert(0, str(project_root / "src")) # Project information project = "pipecat-ai" -copyright = "2024, Daily" +current_year = datetime.now().year +copyright = f"2024-{current_year}, Daily" if current_year > 2024 else "2024, Daily" author = "Daily" # General configuration From 0aa197e4a412c2aea84abe7e8211e77fae521ec7 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 23:36:04 -0400 Subject: [PATCH 15/18] Add docstrings to DeepgramSTTService --- src/pipecat/services/deepgram/stt.py | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/pipecat/services/deepgram/stt.py b/src/pipecat/services/deepgram/stt.py index 308ad1d1d..da7ec535f 100644 --- a/src/pipecat/services/deepgram/stt.py +++ b/src/pipecat/services/deepgram/stt.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Deepgram speech-to-text service implementation.""" + from typing import AsyncGenerator, Dict, Optional from loguru import logger @@ -41,6 +43,22 @@ except ModuleNotFoundError as e: class DeepgramSTTService(STTService): + """Deepgram speech-to-text service. + + Provides real-time speech recognition using Deepgram's WebSocket API. + Supports configurable models, languages, VAD events, and various audio + processing options. + + Args: + api_key: Deepgram API key for authentication. + url: 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: Deepgram LiveOptions for detailed configuration. + addons: Additional Deepgram features to enable. + **kwargs: Additional arguments passed to the parent STTService. + """ + def __init__( self, *, @@ -108,12 +126,27 @@ class DeepgramSTTService(STTService): @property def vad_enabled(self): + """Check if Deepgram VAD events are enabled. + + Returns: + True if VAD events are enabled in the current settings. + """ return self._settings["vad_events"] def can_generate_metrics(self) -> bool: + """Check if this service can generate processing metrics. + + Returns: + True, as Deepgram service supports metrics generation. + """ return True async def set_model(self, model: str): + """Set the Deepgram model and reconnect. + + Args: + model: The Deepgram model name to use. + """ await super().set_model(model) logger.info(f"Switching STT model to: [{model}]") self._settings["model"] = model @@ -121,25 +154,53 @@ class DeepgramSTTService(STTService): await self._connect() async def set_language(self, language: Language): + """Set the recognition language and reconnect. + + Args: + language: The language to use for speech recognition. + """ logger.info(f"Switching STT language to: [{language}]") self._settings["language"] = language await self._disconnect() await self._connect() async def start(self, frame: StartFrame): + """Start the Deepgram STT service. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._settings["sample_rate"] = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): + """Stop the Deepgram STT service. + + Args: + frame: The end frame. + """ await super().stop(frame) await self._disconnect() async def cancel(self, frame: CancelFrame): + """Cancel the Deepgram STT service. + + Args: + frame: The cancel frame. + """ await super().cancel(frame) await self._disconnect() async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: + """Send audio data to Deepgram for transcription. + + Args: + audio: Raw audio bytes to transcribe. + + Yields: + Frame: None (transcription results come via WebSocket callbacks). + """ await self._connection.send(audio) yield None @@ -172,6 +233,7 @@ class DeepgramSTTService(STTService): await self._connection.finish() async def start_metrics(self): + """Start TTFB and processing metrics collection.""" await self.start_ttfb_metrics() await self.start_processing_metrics() @@ -235,6 +297,12 @@ class DeepgramSTTService(STTService): ) async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames with Deepgram-specific handling. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) if isinstance(frame, UserStartedSpeakingFrame) and not self.vad_enabled: From 5b8f1fe3e392056d67e77164c8ec8a14c801721d Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Wed, 25 Jun 2025 23:50:55 -0400 Subject: [PATCH 16/18] Add Cartesia TTS docstrings --- src/pipecat/services/cartesia/tts.py | 143 +++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/pipecat/services/cartesia/tts.py b/src/pipecat/services/cartesia/tts.py index 22493f229..402ea27a0 100644 --- a/src/pipecat/services/cartesia/tts.py +++ b/src/pipecat/services/cartesia/tts.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Cartesia text-to-speech service implementations.""" + import base64 import json import uuid @@ -42,6 +44,14 @@ except ModuleNotFoundError as e: def language_to_cartesia_language(language: Language) -> Optional[str]: + """Convert a Language enum to Cartesia language code. + + Args: + language: The Language enum value to convert. + + Returns: + The corresponding Cartesia language code, or None if not supported. + """ BASE_LANGUAGES = { Language.DE: "de", Language.EN: "en", @@ -74,7 +84,35 @@ def language_to_cartesia_language(language: Language) -> Optional[str]: class CartesiaTTSService(AudioContextWordTTSService): + """Cartesia TTS service with WebSocket streaming and word timestamps. + + Provides text-to-speech using Cartesia's streaming WebSocket API. + Supports word-level timestamps, audio context management, and various voice + customization options including speed and emotion controls. + + Args: + api_key: Cartesia API key for authentication. + voice_id: ID of the voice to use for synthesis. + cartesia_version: API version string for Cartesia service. + url: WebSocket URL for Cartesia TTS API. + model: TTS model to use (e.g., "sonic-2"). + sample_rate: Audio sample rate. If None, uses default. + encoding: Audio encoding format. + container: Audio container format. + params: Additional input parameters for voice customization. + text_aggregator: Custom text aggregator for processing input text. + **kwargs: Additional arguments passed to the parent service. + """ + class InputParams(BaseModel): + """Input parameters for Cartesia TTS configuration. + + Parameters: + language: Language to use for synthesis. + speed: Voice speed control (string or float). + emotion: List of emotion controls (deprecated). + """ + language: Optional[Language] = Language.EN speed: Optional[Union[str, float]] = "" emotion: Optional[List[str]] = [] @@ -137,14 +175,32 @@ class CartesiaTTSService(AudioContextWordTTSService): self._receive_task = None def can_generate_metrics(self) -> bool: + """Check if this service can generate processing metrics. + + Returns: + True, as Cartesia service supports metrics generation. + """ return True async def set_model(self, model: str): + """Set the TTS model. + + Args: + model: The model name to use for synthesis. + """ self._model_id = model await super().set_model(model) logger.info(f"Switching TTS model to: [{model}]") def language_to_service_language(self, language: Language) -> Optional[str]: + """Convert a Language enum to Cartesia language format. + + Args: + language: The language to convert. + + Returns: + The Cartesia-specific language code, or None if not supported. + """ return language_to_cartesia_language(language) def _build_msg( @@ -182,15 +238,30 @@ class CartesiaTTSService(AudioContextWordTTSService): return json.dumps(msg) async def start(self, frame: StartFrame): + """Start the Cartesia TTS service. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._settings["output_format"]["sample_rate"] = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): + """Stop the Cartesia TTS service. + + Args: + frame: The end frame. + """ await super().stop(frame) await self._disconnect() async def cancel(self, frame: CancelFrame): + """Stop the Cartesia TTS service. + + Args: + frame: The end frame. + """ await super().cancel(frame) await self._disconnect() @@ -247,6 +318,7 @@ class CartesiaTTSService(AudioContextWordTTSService): self._context_id = None async def flush_audio(self): + """Flush any pending audio and finalize the current context.""" if not self._context_id or not self._websocket: return logger.trace(f"{self}: flushing audio") @@ -287,6 +359,14 @@ class CartesiaTTSService(AudioContextWordTTSService): @traced_tts async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: + """Generate speech from text using Cartesia's streaming API. + + Args: + text: The text to synthesize into speech. + + Yields: + Frame: Audio frames containing the synthesized speech. + """ logger.debug(f"{self}: Generating TTS [{text}]") try: @@ -316,7 +396,34 @@ class CartesiaTTSService(AudioContextWordTTSService): class CartesiaHttpTTSService(TTSService): + """Cartesia HTTP-based TTS service. + + Provides text-to-speech using Cartesia's HTTP API for simpler, non-streaming + synthesis. Suitable for use cases where streaming is not required and simpler + integration is preferred. + + Args: + api_key: Cartesia API key for authentication. + voice_id: ID of the voice to use for synthesis. + model: TTS model to use (e.g., "sonic-2"). + base_url: Base URL for Cartesia HTTP API. + cartesia_version: API version string for Cartesia service. + sample_rate: Audio sample rate. If None, uses default. + encoding: Audio encoding format. + container: Audio container format. + params: Additional input parameters for voice customization. + **kwargs: Additional arguments passed to the parent TTSService. + """ + class InputParams(BaseModel): + """Input parameters for Cartesia HTTP TTS configuration. + + Parameters: + language: Language to use for synthesis. + speed: Voice speed control (string or float). + emotion: List of emotion controls (deprecated). + """ + language: Optional[Language] = Language.EN speed: Optional[Union[str, float]] = "" emotion: Optional[List[str]] = Field(default_factory=list) @@ -363,25 +470,61 @@ class CartesiaHttpTTSService(TTSService): ) def can_generate_metrics(self) -> bool: + """Check if this service can generate processing metrics. + + Returns: + True, as Cartesia HTTP service supports metrics generation. + """ return True def language_to_service_language(self, language: Language) -> Optional[str]: + """Convert a Language enum to Cartesia language format. + + Args: + language: The language to convert. + + Returns: + The Cartesia-specific language code, or None if not supported. + """ return language_to_cartesia_language(language) async def start(self, frame: StartFrame): + """Start the Cartesia HTTP TTS service. + + Args: + frame: The start frame containing initialization parameters. + """ await super().start(frame) self._settings["output_format"]["sample_rate"] = self.sample_rate async def stop(self, frame: EndFrame): + """Stop the Cartesia HTTP TTS service. + + Args: + frame: The end frame. + """ await super().stop(frame) await self._client.close() async def cancel(self, frame: CancelFrame): + """Cancel the Cartesia HTTP TTS service. + + Args: + frame: The cancel frame. + """ await super().cancel(frame) await self._client.close() @traced_tts async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: + """Generate speech from text using Cartesia's HTTP API. + + Args: + text: The text to synthesize into speech. + + Yields: + Frame: Audio frames containing the synthesized speech. + """ logger.debug(f"{self}: Generating TTS [{text}]") try: From ac61139243b7c55d8fe0752422767e3b1410c886 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Thu, 26 Jun 2025 00:06:57 -0400 Subject: [PATCH 17/18] Add OpenAI LLM docstrings --- src/pipecat/services/openai/base_llm.py | 71 ++++++++++++++++++-- src/pipecat/services/openai/llm.py | 88 +++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 11 deletions(-) diff --git a/src/pipecat/services/openai/base_llm.py b/src/pipecat/services/openai/base_llm.py index 2badfed96..abeb1ea11 100644 --- a/src/pipecat/services/openai/base_llm.py +++ b/src/pipecat/services/openai/base_llm.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""Base OpenAI LLM service implementation.""" + import base64 import json from typing import Any, Dict, List, Mapping, Optional @@ -39,16 +41,39 @@ from pipecat.utils.tracing.service_decorators import traced_llm class BaseOpenAILLMService(LLMService): - """This is the base for all services that use the AsyncOpenAI client. + """Base class for all services that use the AsyncOpenAI client. This service consumes OpenAILLMContextFrame frames, which contain a reference - to an OpenAILLMContext frame. The OpenAILLMContext object defines the context - sent to the LLM for a completion. This includes user, assistant and system messages - as well as tool choices and the tool, which is used if requesting function - calls from the LLM. + to an OpenAILLMContext object. The context defines what is sent to the LLM for + completion, including user, assistant, and system messages, as well as tool + choices and function call configurations. + + Args: + model: The OpenAI model name to use (e.g., "gpt-4.1", "gpt-4o"). + api_key: OpenAI API key. If None, uses environment variable. + base_url: Custom base URL for OpenAI API. If None, uses default. + organization: OpenAI organization ID. + project: OpenAI project ID. + default_headers: Additional HTTP headers to include in requests. + params: Input parameters for model configuration and behavior. + **kwargs: Additional arguments passed to the parent LLMService. """ class InputParams(BaseModel): + """Input parameters for OpenAI model configuration. + + Parameters: + frequency_penalty: Penalty for frequent tokens (-2.0 to 2.0). + presence_penalty: Penalty for new tokens (-2.0 to 2.0). + seed: Random seed for deterministic outputs. + temperature: Sampling temperature (0.0 to 2.0). + top_k: Top-k sampling parameter (currently ignored by OpenAI). + top_p: Top-p (nucleus) sampling parameter (0.0 to 1.0). + max_tokens: Maximum tokens in response (deprecated, use max_completion_tokens). + max_completion_tokens: Maximum completion tokens to generate. + extra: Additional model-specific parameters. + """ + frequency_penalty: Optional[float] = Field( default_factory=lambda: NOT_GIVEN, ge=-2.0, le=2.0 ) @@ -110,6 +135,19 @@ class BaseOpenAILLMService(LLMService): default_headers=None, **kwargs, ): + """Create an AsyncOpenAI client instance. + + Args: + api_key: OpenAI API key. + base_url: Custom base URL for the API. + organization: OpenAI organization ID. + project: OpenAI project ID. + default_headers: Additional HTTP headers. + **kwargs: Additional client configuration arguments. + + Returns: + Configured AsyncOpenAI client instance. + """ return AsyncOpenAI( api_key=api_key, base_url=base_url, @@ -124,11 +162,25 @@ class BaseOpenAILLMService(LLMService): ) def can_generate_metrics(self) -> bool: + """Check if this service can generate processing metrics. + + Returns: + True, as OpenAI service supports metrics generation. + """ return True async def get_chat_completions( self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam] ) -> AsyncStream[ChatCompletionChunk]: + """Get streaming chat completions from OpenAI API. + + Args: + context: The LLM context containing tools and configuration. + messages: List of chat completion messages to send. + + Returns: + Async stream of chat completion chunks. + """ params = { "model": self.model_name, "stream": True, @@ -274,6 +326,15 @@ class BaseOpenAILLMService(LLMService): await self.run_function_calls(function_calls) async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames for LLM completion requests. + + Handles OpenAILLMContextFrame, LLMMessagesFrame, VisionImageRawFrame, + and LLMUpdateSettingsFrame to trigger LLM completions and manage settings. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ await super().process_frame(frame, direction) context = None diff --git a/src/pipecat/services/openai/llm.py b/src/pipecat/services/openai/llm.py index 44e0a40ce..c27eab867 100644 --- a/src/pipecat/services/openai/llm.py +++ b/src/pipecat/services/openai/llm.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # +"""OpenAI LLM service implementation with context aggregators.""" + import json from dataclasses import dataclass from typing import Any, Optional @@ -26,17 +28,46 @@ from pipecat.services.openai.base_llm import BaseOpenAILLMService @dataclass class OpenAIContextAggregatorPair: + """Pair of OpenAI context aggregators for user and assistant messages. + + Parameters: + _user: User context aggregator for processing user messages. + _assistant: Assistant context aggregator for processing assistant messages. + """ + _user: "OpenAIUserContextAggregator" _assistant: "OpenAIAssistantContextAggregator" def user(self) -> "OpenAIUserContextAggregator": + """Get the user context aggregator. + + Returns: + The user context aggregator instance. + """ return self._user def assistant(self) -> "OpenAIAssistantContextAggregator": + """Get the assistant context aggregator. + + Returns: + The assistant context aggregator instance. + """ return self._assistant class OpenAILLMService(BaseOpenAILLMService): + """OpenAI LLM service implementation. + + Provides a complete OpenAI LLM service with context aggregation support. + Uses the BaseOpenAILLMService for core functionality and adds OpenAI-specific + context aggregator creation. + + Args: + model: The OpenAI model name to use. Defaults to "gpt-4.1". + params: Input parameters for model configuration. + **kwargs: Additional arguments passed to the parent BaseOpenAILLMService. + """ + def __init__( self, *, @@ -53,14 +84,15 @@ class OpenAILLMService(BaseOpenAILLMService): user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(), assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(), ) -> OpenAIContextAggregatorPair: - """Create an instance of OpenAIContextAggregatorPair from an - OpenAILLMContext. Constructor keyword arguments for both the user and - assistant aggregators can be provided. + """Create OpenAI-specific context aggregators. + + Creates a pair of context aggregators optimized for OpenAI's message format, + including support for function calls, tool usage, and image handling. Args: - context (OpenAILLMContext): The LLM context. - user_params (LLMUserAggregatorParams, optional): User aggregator parameters. - assistant_params (LLMAssistantAggregatorParams, optional): User aggregator parameters. + context: The LLM context to create aggregators for. + user_params: Parameters for user message aggregation. + assistant_params: Parameters for assistant message aggregation. Returns: OpenAIContextAggregatorPair: A pair of context aggregators, one for @@ -75,11 +107,32 @@ class OpenAILLMService(BaseOpenAILLMService): class OpenAIUserContextAggregator(LLMUserContextAggregator): + """OpenAI-specific user context aggregator. + + Handles aggregation of user messages for OpenAI LLM services. + Inherits all functionality from the base LLMUserContextAggregator. + """ + pass class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator): + """OpenAI-specific assistant context aggregator. + + Handles aggregation of assistant messages for OpenAI LLM services, + with specialized support for OpenAI's function calling format, + tool usage tracking, and image message handling. + """ + async def handle_function_call_in_progress(self, frame: FunctionCallInProgressFrame): + """Handle a function call in progress. + + Adds the function call to the context with an IN_PROGRESS status + to track ongoing function execution. + + Args: + frame: Frame containing function call progress information. + """ self._context.add_message( { "role": "assistant", @@ -104,6 +157,14 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator): ) async def handle_function_call_result(self, frame: FunctionCallResultFrame): + """Handle the result of a function call. + + Updates the context with the function call result, replacing any + previous IN_PROGRESS status. + + Args: + frame: Frame containing the function call result. + """ if frame.result: result = json.dumps(frame.result) await self._update_function_call_result(frame.function_name, frame.tool_call_id, result) @@ -113,6 +174,13 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator): ) async def handle_function_call_cancel(self, frame: FunctionCallCancelFrame): + """Handle a cancelled function call. + + Updates the context to mark the function call as cancelled. + + Args: + frame: Frame containing the function call cancellation information. + """ await self._update_function_call_result( frame.function_name, frame.tool_call_id, "CANCELLED" ) @@ -129,6 +197,14 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator): message["content"] = result async def handle_user_image_frame(self, frame: UserImageRawFrame): + """Handle a user image frame from a function call request. + + Marks the associated function call as completed and adds the image + to the context for processing. + + Args: + frame: Frame containing the user image and request context. + """ await self._update_function_call_result( frame.request.function_name, frame.request.tool_call_id, "COMPLETED" ) From 951c8d34da9638d78e836668193cbd3a6c1cbde3 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Thu, 26 Jun 2025 00:15:09 -0400 Subject: [PATCH 18/18] Add special case handling for STT, TTS, LLM --- docs/api/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/conf.py b/docs/api/conf.py index 86a40a871..a2a568134 100644 --- a/docs/api/conf.py +++ b/docs/api/conf.py @@ -272,6 +272,9 @@ def clean_title(title: str) -> str: "playht": "PlayHT", "xtts": "XTTS", "lmnt": "LMNT", + "stt": "STT", + "tts": "TTS", + "llm": "LLM", } # Check if the entire title is a special case