From 87e8ed109ad7ef8c55fe9ba21f12f57aef2928c9 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Thu, 2 Apr 2026 10:52:41 -0400 Subject: [PATCH] Remove deprecated STTMuteFilter, STTMuteConfig, and STTMuteStrategy --- .../processors/filters/stt_mute_filter.py | 243 ------------ src/pipecat/services/google/stt.py | 2 +- tests/test_stt_mute_filter.py | 354 ------------------ 3 files changed, 1 insertion(+), 598 deletions(-) delete mode 100644 src/pipecat/processors/filters/stt_mute_filter.py delete mode 100644 tests/test_stt_mute_filter.py diff --git a/src/pipecat/processors/filters/stt_mute_filter.py b/src/pipecat/processors/filters/stt_mute_filter.py deleted file mode 100644 index 9f522a20d..000000000 --- a/src/pipecat/processors/filters/stt_mute_filter.py +++ /dev/null @@ -1,243 +0,0 @@ -# -# Copyright (c) 2024-2026, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -"""Speech-to-text (STT) muting control module. - -This module provides functionality to control STT muting based on different strategies, -such as during function calls, bot speech, or custom conditions. It helps manage when -the STT service should be active or inactive during a conversation. -""" - -from dataclasses import dataclass -from enum import Enum -from typing import Awaitable, Callable, Optional - -from loguru import logger - -from pipecat.frames.frames import ( - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - Frame, - FunctionCallCancelFrame, - FunctionCallResultFrame, - FunctionCallsStartedFrame, - InputAudioRawFrame, - InterimTranscriptionFrame, - InterruptionFrame, - StartFrame, - TranscriptionFrame, - UserStartedSpeakingFrame, - UserStoppedSpeakingFrame, - VADUserStartedSpeakingFrame, - VADUserStoppedSpeakingFrame, -) -from pipecat.processors.frame_processor import FrameDirection, FrameProcessor - - -class STTMuteStrategy(Enum): - """Strategies determining when STT should be muted. - - Each strategy defines different conditions under which speech-to-text - processing should be temporarily disabled to prevent unwanted audio - processing during specific conversation states. - - Parameters: - FIRST_SPEECH: Mute STT until the first bot speech is detected. - MUTE_UNTIL_FIRST_BOT_COMPLETE: Mute STT until the first bot completes speaking, - regardless of whether it is the first speech. - FUNCTION_CALL: Mute STT during function calls to prevent interruptions. - ALWAYS: Always mute STT when the bot is speaking. - CUSTOM: Use a custom callback to determine muting logic dynamically. - - .. deprecated:: 0.0.99 - `STTMuteStrategy` is deprecated and will be removed in a future version. - Use `LLMUserAggregator`'s new `user_mute_strategies` instead. - """ - - FIRST_SPEECH = "first_speech" - MUTE_UNTIL_FIRST_BOT_COMPLETE = "mute_until_first_bot_complete" - FUNCTION_CALL = "function_call" - ALWAYS = "always" - CUSTOM = "custom" - - -@dataclass -class STTMuteConfig: - """Configuration for STT muting behavior. - - Defines which muting strategies to apply and provides optional custom - callback for advanced muting logic. Multiple strategies can be combined - to create sophisticated muting behavior. - - Parameters: - strategies: Set of muting strategies to apply simultaneously. - should_mute_callback: Optional callback for custom muting logic. - Only required when using STTMuteStrategy.CUSTOM. Called with - the STTMuteFilter instance to determine muting state. - - Note: - MUTE_UNTIL_FIRST_BOT_COMPLETE and FIRST_SPEECH strategies should not be used together - as they handle the first bot speech differently. - - .. deprecated:: 0.0.99 - `STTMuteConfig` is deprecated and will be removed in a future version. - Use `LLMUserAggregator`'s new `user_mute_strategies` instead. - """ - - strategies: set[STTMuteStrategy] - should_mute_callback: Optional[Callable[["STTMuteFilter"], Awaitable[bool]]] = None - - def __post_init__(self): - """Validate configuration after initialization. - - Raises: - ValueError: If incompatible strategies are used together. - """ - if ( - STTMuteStrategy.MUTE_UNTIL_FIRST_BOT_COMPLETE in self.strategies - and STTMuteStrategy.FIRST_SPEECH in self.strategies - ): - raise ValueError( - "MUTE_UNTIL_FIRST_BOT_COMPLETE and FIRST_SPEECH strategies should not be used together" - ) - - -class STTMuteFilter(FrameProcessor): - """A processor that handles STT muting and interruption control. - - This processor combines STT muting and interruption control as a coordinated - feature. When STT is muted, interruptions are automatically disabled by - suppressing VAD-related frames. This prevents unwanted speech detection - during bot speech, function calls, or other specified conditions. - - .. deprecated:: 0.0.99 - `STTMuteFilter` is deprecated and will be removed in a future version. - Use `LLMUserAggregator`'s new `user_mute_strategies` instead. - """ - - def __init__(self, *, config: STTMuteConfig, **kwargs): - """Initialize the STT mute filter. - - Args: - config: Configuration specifying muting strategies and behavior. - **kwargs: Additional arguments passed to parent class. - """ - super().__init__(**kwargs) - self._config = config - self._first_speech_handled = False - self._bot_is_speaking = False - self._function_call_in_progress = set() - self._is_muted = False - - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn( - "`STTMuteFilter` is deprecated and will be removed in a future version. " - "Use `LLMUserAggregator`'s new `user_mute_strategies` instead.", - DeprecationWarning, - ) - - async def _handle_mute_state(self, should_mute: bool): - """Handle STT muting and interruption control state changes.""" - if should_mute != self._is_muted: - logger.debug(f"STTMuteFilter {'muting' if should_mute else 'unmuting'}") - self._is_muted = should_mute - # Note: We don't send STTMuteFrame to the STT service itself. - # The filter blocks frames locally, but the STT service continues - # processing audio to keep streaming connections alive (e.g., Google STT). - - async def _should_mute(self) -> bool: - """Determine if STT should be muted based on current state and strategies.""" - for strategy in self._config.strategies: - match strategy: - case STTMuteStrategy.FUNCTION_CALL: - if self._function_call_in_progress: - return True - - case STTMuteStrategy.ALWAYS: - if self._bot_is_speaking: - return True - - case STTMuteStrategy.FIRST_SPEECH: - if self._bot_is_speaking and not self._first_speech_handled: - self._first_speech_handled = True - return True - - case STTMuteStrategy.MUTE_UNTIL_FIRST_BOT_COMPLETE: - if not self._first_speech_handled: - return True - - case STTMuteStrategy.CUSTOM: - if self._bot_is_speaking and self._config.should_mute_callback: - should_mute = await self._config.should_mute_callback(self) - if should_mute: - return True - - return False - - async def process_frame(self, frame: Frame, direction: FrameDirection): - """Process incoming frames and manage muting state. - - Monitors conversation state through frame types and applies muting - strategies accordingly. Suppresses VAD-related frames when muted - while allowing other frames to pass through. - - Args: - frame: The incoming frame to process. - direction: The direction of frame flow in the pipeline. - """ - await super().process_frame(frame, direction) - - # Determine if we need to change mute state based on frame type - should_mute = None - - # Process frames to determine mute state - if isinstance(frame, StartFrame): - should_mute = await self._should_mute() - elif isinstance(frame, FunctionCallsStartedFrame): - for f in frame.function_calls: - self._function_call_in_progress.add(f.tool_call_id) - should_mute = await self._should_mute() - elif isinstance(frame, (FunctionCallCancelFrame, FunctionCallResultFrame)): - self._function_call_in_progress.remove(frame.tool_call_id) - should_mute = await self._should_mute() - elif isinstance(frame, BotStartedSpeakingFrame): - self._bot_is_speaking = True - should_mute = await self._should_mute() - elif isinstance(frame, BotStoppedSpeakingFrame): - self._bot_is_speaking = False - if not self._first_speech_handled: - self._first_speech_handled = True - should_mute = await self._should_mute() - - # Then push the original frame - if isinstance( - frame, - ( - InterruptionFrame, - VADUserStartedSpeakingFrame, - VADUserStoppedSpeakingFrame, - UserStartedSpeakingFrame, - UserStoppedSpeakingFrame, - InputAudioRawFrame, - InterimTranscriptionFrame, - TranscriptionFrame, - ), - ): - # Only pass VAD-related frames when not muted - if not self._is_muted: - await self.push_frame(frame, direction) - else: - logger.trace(f"{frame.__class__.__name__} suppressed - STT currently muted") - else: - # Pass all other frames through - await self.push_frame(frame, direction) - - # Finally handle mute state change if needed - if should_mute is not None and should_mute != self._is_muted: - await self._handle_mute_state(should_mute) diff --git a/src/pipecat/services/google/stt.py b/src/pipecat/services/google/stt.py index 9bf2685b7..282389a3a 100644 --- a/src/pipecat/services/google/stt.py +++ b/src/pipecat/services/google/stt.py @@ -1004,7 +1004,7 @@ class GoogleSTTService(STTService): except Aborted as e: # Handle stream abort due to inactivity (409 error). # This occurs when no audio is sent to the stream for 10+ seconds, - # which can happen when InputAudioRawFrames are blocked (e.g., by STTMuteFilter). + # which can happen when InputAudioRawFrames are blocked. # Google's STT service automatically closes the stream in this case. # We log at DEBUG level (not ERROR) since this is recoverable, then re-raise # to trigger automatic reconnection in _stream_audio. diff --git a/tests/test_stt_mute_filter.py b/tests/test_stt_mute_filter.py deleted file mode 100644 index 8f55bdecb..000000000 --- a/tests/test_stt_mute_filter.py +++ /dev/null @@ -1,354 +0,0 @@ -# -# Copyright (c) 2024-2026, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -import unittest - -from pipecat.frames.frames import ( - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - FunctionCallFromLLM, - FunctionCallResultFrame, - FunctionCallsStartedFrame, - InputAudioRawFrame, - InterimTranscriptionFrame, - InterruptionFrame, - TranscriptionFrame, - UserStartedSpeakingFrame, - UserStoppedSpeakingFrame, - VADUserStartedSpeakingFrame, - VADUserStoppedSpeakingFrame, -) -from pipecat.processors.filters.stt_mute_filter import STTMuteConfig, STTMuteFilter, STTMuteStrategy -from pipecat.tests.utils import SleepFrame, run_test - - -class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): - async def test_first_speech_strategy(self): - filter = STTMuteFilter(config=STTMuteConfig(strategies={STTMuteStrategy.FIRST_SPEECH})) - - frames_to_send = [ - BotStartedSpeakingFrame(), # First bot speech starts - VADUserStartedSpeakingFrame(), # Should be suppressed - UserStartedSpeakingFrame(), # Should be suppressed - InputAudioRawFrame( - audio=b"", sample_rate=16000, num_channels=1 - ), # Should be suppressed - VADUserStoppedSpeakingFrame(), # Should be suppressed - UserStoppedSpeakingFrame(), # Should be suppressed - BotStoppedSpeakingFrame(), # First bot speech ends - BotStartedSpeakingFrame(), # Second bot speech - VADUserStartedSpeakingFrame(), # Should pass through - UserStartedSpeakingFrame(), # Should pass through - InputAudioRawFrame(audio=b"", sample_rate=16000, num_channels=1), # Should pass through - VADUserStoppedSpeakingFrame(), # Should pass through - UserStoppedSpeakingFrame(), # Should pass through - BotStoppedSpeakingFrame(), - ] - - expected_returned_frames = [ - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - BotStartedSpeakingFrame, - VADUserStartedSpeakingFrame, # Now passes through - UserStartedSpeakingFrame, # Now passes through - InputAudioRawFrame, # Now passes through - VADUserStoppedSpeakingFrame, # Now passes through - UserStoppedSpeakingFrame, # Now passes through - BotStoppedSpeakingFrame, - ] - - await run_test( - filter, - frames_to_send=frames_to_send, - expected_down_frames=expected_returned_frames, - ) - - async def test_always_strategy(self): - filter = STTMuteFilter(config=STTMuteConfig(strategies={STTMuteStrategy.ALWAYS})) - - frames_to_send = [ - BotStartedSpeakingFrame(), # First speech starts - VADUserStartedSpeakingFrame(), # Should be suppressed - UserStartedSpeakingFrame(), # Should be suppressed - InputAudioRawFrame( - audio=b"", sample_rate=16000, num_channels=1 - ), # Should be suppressed - VADUserStoppedSpeakingFrame(), # Should be suppressed - UserStoppedSpeakingFrame(), # Should be suppressed - BotStoppedSpeakingFrame(), # First speech ends - VADUserStartedSpeakingFrame(), # Should pass through - UserStartedSpeakingFrame(), # Should pass through - InputAudioRawFrame(audio=b"", sample_rate=16000, num_channels=1), # Should pass through - VADUserStoppedSpeakingFrame(), # Should pass through - UserStoppedSpeakingFrame(), # Should pass through - BotStartedSpeakingFrame(), # Second speech starts - VADUserStartedSpeakingFrame(), # Should be suppressed again - UserStartedSpeakingFrame(), # Should be suppressed again - InputAudioRawFrame( - audio=b"", sample_rate=16000, num_channels=1 - ), # Should be suppressed again - VADUserStoppedSpeakingFrame(), # Should be suppressed again - UserStoppedSpeakingFrame(), # Should be suppressed again - BotStoppedSpeakingFrame(), # Second speech ends - ] - - expected_returned_frames = [ - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - VADUserStartedSpeakingFrame, - UserStartedSpeakingFrame, - InputAudioRawFrame, - VADUserStoppedSpeakingFrame, - UserStoppedSpeakingFrame, - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - ] - - await run_test( - filter, - frames_to_send=frames_to_send, - expected_down_frames=expected_returned_frames, - ) - - async def test_transcription_frames_with_always_strategy(self): - filter = STTMuteFilter(config=STTMuteConfig(strategies={STTMuteStrategy.ALWAYS})) - - frames_to_send = [ - # Bot speaking - should mute - BotStartedSpeakingFrame(), - SleepFrame(), # Wait for StartedSpeaking to process - InterimTranscriptionFrame( - user_id="user1", text="This should be suppressed", timestamp="1234567890" - ), - TranscriptionFrame( - user_id="user1", text="This should be suppressed", timestamp="1234567890" - ), - SleepFrame(), # Wait for transcription frames to queue - BotStoppedSpeakingFrame(), - # Bot not speaking - should pass through - InterimTranscriptionFrame( - user_id="user1", text="This should pass", timestamp="1234567891" - ), - TranscriptionFrame( - user_id="user1", text="This should pass through", timestamp="1234567891" - ), - ] - - expected_returned_frames = [ - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - InterimTranscriptionFrame, # Only passes through after bot stops speaking - TranscriptionFrame, # Only passes through after bot stops speaking - ] - - await run_test( - filter, - frames_to_send=frames_to_send, - expected_down_frames=expected_returned_frames, - ) - - async def test_function_call_strategy(self): - filter = STTMuteFilter(config=STTMuteConfig(strategies={STTMuteStrategy.FUNCTION_CALL})) - - frames_to_send = [ - VADUserStartedSpeakingFrame(), # Should pass through initially - UserStartedSpeakingFrame(), # Should pass through initially - VADUserStoppedSpeakingFrame(), # Should pass through initially - UserStoppedSpeakingFrame(), # Should pass through initially - FunctionCallsStartedFrame( - function_calls=[ - FunctionCallFromLLM( - function_name="get_weather", - tool_call_id="call_123", - arguments='{"location": "San Francisco"}', - context=None, - ) - ] - ), # Start function call - VADUserStartedSpeakingFrame(), # Should be suppressed - UserStartedSpeakingFrame(), # Should be suppressed - VADUserStoppedSpeakingFrame(), # Should be suppressed - UserStoppedSpeakingFrame(), # Should be suppressed - FunctionCallResultFrame( - function_name="get_weather", - tool_call_id="call_123", - arguments='{"location": "San Francisco"}', - result={"temperature": 22}, - ), # End function call - SleepFrame(), - VADUserStartedSpeakingFrame(), # Should pass through again - UserStartedSpeakingFrame(), # Should pass through again - VADUserStoppedSpeakingFrame(), - UserStoppedSpeakingFrame(), - ] - - expected_returned_frames = [ - VADUserStartedSpeakingFrame, - UserStartedSpeakingFrame, - VADUserStoppedSpeakingFrame, - UserStoppedSpeakingFrame, - FunctionCallsStartedFrame, - FunctionCallResultFrame, - VADUserStartedSpeakingFrame, - UserStartedSpeakingFrame, - VADUserStoppedSpeakingFrame, - UserStoppedSpeakingFrame, - ] - - await run_test( - filter, - frames_to_send=frames_to_send, - expected_down_frames=expected_returned_frames, - ) - - async def test_mute_until_first_bot_complete_strategy(self): - filter = STTMuteFilter( - config=STTMuteConfig(strategies={STTMuteStrategy.MUTE_UNTIL_FIRST_BOT_COMPLETE}) - ) - - frames_to_send = [ - VADUserStartedSpeakingFrame(), # Should be suppressed (starts muted) - UserStartedSpeakingFrame(), # Should be suppressed (starts muted) - InputAudioRawFrame( - audio=b"", sample_rate=16000, num_channels=1 - ), # Should be suppressed - VADUserStoppedSpeakingFrame(), # Should be suppressed - UserStoppedSpeakingFrame(), # Should be suppressed - BotStartedSpeakingFrame(), # First bot speech - VADUserStartedSpeakingFrame(), # Should be suppressed - UserStartedSpeakingFrame(), # Should be suppressed - InputAudioRawFrame( - audio=b"", sample_rate=16000, num_channels=1 - ), # Should be suppressed - VADUserStoppedSpeakingFrame(), # Should be suppressed - UserStoppedSpeakingFrame(), # Should be suppressed - BotStoppedSpeakingFrame(), # First speech ends, unmutes - VADUserStartedSpeakingFrame(), # Should pass through - UserStartedSpeakingFrame(), # Should pass through - InputAudioRawFrame(audio=b"", sample_rate=16000, num_channels=1), # Should pass through - VADUserStoppedSpeakingFrame(), # Should pass through - UserStoppedSpeakingFrame(), # Should pass through - BotStartedSpeakingFrame(), # Second speech - VADUserStartedSpeakingFrame(), # Should pass through - UserStartedSpeakingFrame(), # Should pass through - InputAudioRawFrame(audio=b"", sample_rate=16000, num_channels=1), # Should pass through - VADUserStoppedSpeakingFrame(), # Should pass through - UserStoppedSpeakingFrame(), # Should pass through - BotStoppedSpeakingFrame(), - ] - - expected_returned_frames = [ - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - VADUserStartedSpeakingFrame, - UserStartedSpeakingFrame, - InputAudioRawFrame, - VADUserStoppedSpeakingFrame, - UserStoppedSpeakingFrame, - BotStartedSpeakingFrame, - VADUserStartedSpeakingFrame, - UserStartedSpeakingFrame, - InputAudioRawFrame, - VADUserStoppedSpeakingFrame, - UserStoppedSpeakingFrame, - BotStoppedSpeakingFrame, - ] - - await run_test( - filter, - frames_to_send=frames_to_send, - expected_down_frames=expected_returned_frames, - ) - - async def test_incompatible_strategies(self): - with self.assertRaises(ValueError): - STTMuteFilter( - config=STTMuteConfig( - strategies={ - STTMuteStrategy.FIRST_SPEECH, - STTMuteStrategy.MUTE_UNTIL_FIRST_BOT_COMPLETE, - } - ) - ) - - async def test_custom_strategy(self): - async def custom_mute_logic(processor: STTMuteFilter) -> bool: - return processor._bot_is_speaking - - filter = STTMuteFilter( - config=STTMuteConfig( - strategies={STTMuteStrategy.CUSTOM}, - should_mute_callback=custom_mute_logic, - ) - ) - - frames_to_send = [ - VADUserStartedSpeakingFrame(), # Should pass through - UserStartedSpeakingFrame(), # Should pass through - InputAudioRawFrame(audio=b"", sample_rate=16000, num_channels=1), # Should pass through - VADUserStoppedSpeakingFrame(), # Should pass through - UserStoppedSpeakingFrame(), # Should pass through - BotStartedSpeakingFrame(), # Bot starts speaking - VADUserStartedSpeakingFrame(), # Should be suppressed - UserStartedSpeakingFrame(), # Should be suppressed - InputAudioRawFrame( - audio=b"", sample_rate=16000, num_channels=1 - ), # Should be suppressed - VADUserStoppedSpeakingFrame(), # Should be suppressed - UserStoppedSpeakingFrame(), # Should be suppressed - BotStoppedSpeakingFrame(), # Bot stops speaking - VADUserStartedSpeakingFrame(), # Should pass through - UserStartedSpeakingFrame(), # Should pass through - InputAudioRawFrame(audio=b"", sample_rate=16000, num_channels=1), # Should pass through - VADUserStoppedSpeakingFrame(), # Should pass through - UserStoppedSpeakingFrame(), # Should pass through - ] - - expected_returned_frames = [ - VADUserStartedSpeakingFrame, - UserStartedSpeakingFrame, - InputAudioRawFrame, - VADUserStoppedSpeakingFrame, - UserStoppedSpeakingFrame, - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - VADUserStartedSpeakingFrame, - UserStartedSpeakingFrame, - InputAudioRawFrame, - VADUserStoppedSpeakingFrame, - UserStoppedSpeakingFrame, - ] - - await run_test( - filter, - frames_to_send=frames_to_send, - expected_down_frames=expected_returned_frames, - ) - - async def test_interruption_frame_suppressed_when_muted(self): - """Test that InterruptionFrame is suppressed when the filter is muted.""" - filter = STTMuteFilter(config=STTMuteConfig(strategies={STTMuteStrategy.ALWAYS})) - - frames_to_send = [ - BotStartedSpeakingFrame(), - InterruptionFrame(), - BotStoppedSpeakingFrame(), - ] - - expected_returned_frames = [ - BotStartedSpeakingFrame, - BotStoppedSpeakingFrame, - ] - - await run_test( - filter, - frames_to_send=frames_to_send, - expected_down_frames=expected_returned_frames, - ) - - -if __name__ == "__main__": - unittest.main()