diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d7c5d97..59e272a16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,13 @@ reason")`. ### Changed +- `STTMuteFilter` no longer sends `STTMuteFrame` to the STT service. The filter + now blocks frames locally without instructing the STT service to stop + processing audio. This prevents inactivity-related errors (such as 409 errors + from Google STT) while maintaining the same muting behavior at the application + level. Important: The STTMuteFilter should be placed _after_ the STT service + itself. + - Bumped the `fastapi` dependency's upperbound to `<0.122.0`. - Updated the default model for `GoogleVertexLLMService` to `gemini-2.5-flash`. diff --git a/src/pipecat/processors/filters/stt_mute_filter.py b/src/pipecat/processors/filters/stt_mute_filter.py index 613d1ef51..a134fee5d 100644 --- a/src/pipecat/processors/filters/stt_mute_filter.py +++ b/src/pipecat/processors/filters/stt_mute_filter.py @@ -118,24 +118,16 @@ class STTMuteFilter(FrameProcessor): self._first_speech_handled = False self._bot_is_speaking = False self._function_call_in_progress = False - self._is_muted = False # Initialize as unmuted, will set state on StartFrame if needed - - @property - def is_muted(self) -> bool: - """Check if STT is currently muted. - - Returns: - True if STT is currently muted and audio frames are being suppressed. - """ - return self._is_muted + self._is_muted = False async def _handle_mute_state(self, should_mute: bool): """Handle STT muting and interruption control state changes.""" - if should_mute != self.is_muted: + if should_mute != self._is_muted: logger.debug(f"STTMuteFilter {'muting' if should_mute else 'unmuting'}") self._is_muted = should_mute - await self.push_frame(STTMuteFrame(mute=should_mute), FrameDirection.UPSTREAM) - await self.push_frame(STTMuteFrame(mute=should_mute), FrameDirection.DOWNSTREAM) + # 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.""" @@ -215,7 +207,7 @@ class STTMuteFilter(FrameProcessor): ), ): # Only pass VAD-related frames when not muted - if not self.is_muted: + if not self._is_muted: await self.push_frame(frame, direction) else: logger.trace(f"{frame.__class__.__name__} suppressed - STT currently muted") @@ -224,5 +216,5 @@ class STTMuteFilter(FrameProcessor): 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: + if should_mute is not None and should_mute != self._is_muted: await self._handle_mute_state(should_mute) diff --git a/tests/test_stt_mute_filter.py b/tests/test_stt_mute_filter.py index 3d707dca7..2bd041b65 100644 --- a/tests/test_stt_mute_filter.py +++ b/tests/test_stt_mute_filter.py @@ -13,7 +13,6 @@ from pipecat.frames.frames import ( FunctionCallResultFrame, InputAudioRawFrame, InterimTranscriptionFrame, - STTMuteFrame, TranscriptionFrame, UserStartedSpeakingFrame, UserStoppedSpeakingFrame, @@ -49,9 +48,7 @@ class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): expected_returned_frames = [ BotStartedSpeakingFrame, - STTMuteFrame, # mute=True BotStoppedSpeakingFrame, - STTMuteFrame, # mute=False BotStartedSpeakingFrame, VADUserStartedSpeakingFrame, # Now passes through UserStartedSpeakingFrame, # Now passes through @@ -98,18 +95,14 @@ class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): expected_returned_frames = [ BotStartedSpeakingFrame, - STTMuteFrame, # mute=True BotStoppedSpeakingFrame, - STTMuteFrame, # mute=False VADUserStartedSpeakingFrame, UserStartedSpeakingFrame, InputAudioRawFrame, VADUserStoppedSpeakingFrame, UserStoppedSpeakingFrame, BotStartedSpeakingFrame, - STTMuteFrame, # mute=True BotStoppedSpeakingFrame, - STTMuteFrame, # mute=False ] await run_test( @@ -144,9 +137,7 @@ class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): expected_returned_frames = [ BotStartedSpeakingFrame, - STTMuteFrame, # mute=True BotStoppedSpeakingFrame, - STTMuteFrame, # mute=False InterimTranscriptionFrame, # Only passes through after bot stops speaking TranscriptionFrame, # Only passes through after bot stops speaking ] @@ -193,9 +184,7 @@ class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): # VADUserStoppedSpeakingFrame, # UserStoppedSpeakingFrame, # FunctionCallInProgressFrame, - # STTMuteFrame, # mute=True # FunctionCallResultFrame, - # STTMuteFrame, # mute=False # VADUserStartedSpeakingFrame, # UserStartedSpeakingFrame, # VADUserStoppedSpeakingFrame, @@ -245,10 +234,8 @@ class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): ] expected_returned_frames = [ - STTMuteFrame, # mute=True after first speech BotStartedSpeakingFrame, BotStoppedSpeakingFrame, - STTMuteFrame, # mute=False after first speech VADUserStartedSpeakingFrame, UserStartedSpeakingFrame, InputAudioRawFrame, @@ -320,9 +307,7 @@ class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): VADUserStoppedSpeakingFrame, UserStoppedSpeakingFrame, BotStartedSpeakingFrame, - STTMuteFrame, # mute=True BotStoppedSpeakingFrame, - STTMuteFrame, # mute=False VADUserStartedSpeakingFrame, UserStartedSpeakingFrame, InputAudioRawFrame,