Remove deprecated STTMuteFilter, STTMuteConfig, and STTMuteStrategy

This commit is contained in:
Mark Backman
2026-04-02 10:52:41 -04:00
parent a5e1bbf4a3
commit 87e8ed109a
3 changed files with 1 additions and 598 deletions

View File

@@ -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)

View File

@@ -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.

View File

@@ -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()