diff --git a/changelog/3455.fixed.md b/changelog/3455.fixed.md new file mode 100644 index 000000000..1dba5838a --- /dev/null +++ b/changelog/3455.fixed.md @@ -0,0 +1 @@ +- Fixed an issue where user turn start strategies were not being reset after a user turn started, causing incorrect strategy behavior. diff --git a/src/pipecat/turns/user_turn_controller.py b/src/pipecat/turns/user_turn_controller.py index 56b6ad711..05cb46988 100644 --- a/src/pipecat/turns/user_turn_controller.py +++ b/src/pipecat/turns/user_turn_controller.py @@ -251,6 +251,10 @@ class UserTurnController(BaseObject): self._user_turn = True self._user_turn_stop_timeout_event.set() + # Reset all user turn start strategies to start fresh. + for s in self._user_turn_strategies.start or []: + await s.reset() + await self._call_event_handler("on_user_turn_started", strategy, params) async def _trigger_user_turn_stop( diff --git a/tests/test_user_turn_controller.py b/tests/test_user_turn_controller.py index 67c1a7860..8aa509a8a 100644 --- a/tests/test_user_turn_controller.py +++ b/tests/test_user_turn_controller.py @@ -8,12 +8,16 @@ import asyncio import unittest from pipecat.frames.frames import ( + BotStartedSpeakingFrame, TranscriptionFrame, UserStartedSpeakingFrame, UserStoppedSpeakingFrame, VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, ) +from pipecat.turns.user_start.min_words_user_turn_start_strategy import ( + MinWordsUserTurnStartStrategy, +) from pipecat.turns.user_turn_controller import UserTurnController from pipecat.turns.user_turn_strategies import ExternalUserTurnStrategies, UserTurnStrategies from pipecat.utils.asyncio.task_manager import TaskManager, TaskManagerParams @@ -58,6 +62,46 @@ class TestUserTurnController(unittest.IsolatedAsyncioTestCase): self.assertTrue(should_start) self.assertTrue(should_stop) + async def test_user_turn_start_reset(self): + controller = UserTurnController( + user_turn_strategies=UserTurnStrategies( + start=[MinWordsUserTurnStartStrategy(min_words=3)] + ), + user_turn_stop_timeout=USER_TURN_STOP_TIMEOUT, + ) + + await controller.setup(self.task_manager) + + should_start = 0 + + @controller.event_handler("on_user_turn_started") + async def on_user_turn_started(controller, strategy, params): + nonlocal should_start + should_start += 1 + + await controller.process_frame(BotStartedSpeakingFrame()) + await controller.process_frame(TranscriptionFrame(text="One", user_id="cat", timestamp="")) + self.assertEqual(should_start, 0) + + await controller.process_frame( + TranscriptionFrame(text=" two three!", user_id="cat", timestamp="") + ) + self.assertEqual(should_start, 1) + + # Trigger user stop turn so we can trigger user start turn again. + await asyncio.sleep(USER_TURN_STOP_TIMEOUT + 0.1) + + await controller.process_frame(BotStartedSpeakingFrame()) + await controller.process_frame( + TranscriptionFrame(text="Hello", user_id="cat", timestamp="") + ) + self.assertEqual(should_start, 1) + + await controller.process_frame( + TranscriptionFrame(text=" there friend!", user_id="cat", timestamp="") + ) + self.assertEqual(should_start, 2) + async def test_user_turn_stop_timeout_no_transcription(self): controller = UserTurnController( user_turn_strategies=UserTurnStrategies(),