Stop spurious watchdog re-prompts after the call ends

The SilenceWatchdog armed a timer on the goodbye turn; it then fired ~silence_secs
later, after EndCallProcessor had already hung up — logging phantom "re-prompt"s
(3 of 5 in the last batch were after "Goodbye"). Now it stops for good on a
closing keyword (LLMFullResponseEnd) or an EndFrame/CancelFrame, so it never
re-arms once the call is closing. Real mid-call silences still re-prompt.
Runtime-tested: no reprompt after goodbye / endframe; reprompt on normal silence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tocmo0nlord
2026-06-28 00:14:53 +00:00
parent a521dc168e
commit 7f34b06415

21
bot.py
View File

@@ -25,6 +25,7 @@ from pipecat.audio.vad.vad_analyzer import VADParams
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame,
EndFrame, EndFrame,
EndTaskFrame, EndTaskFrame,
Frame, Frame,
@@ -373,6 +374,8 @@ class SilenceWatchdog(FrameProcessor):
self._prompts = 0 self._prompts = 0
self._bot_speaking = False self._bot_speaking = False
self._ending = False self._ending = False
self._stopped = False # call is closing/ended — never arm again
self._buf = ""
def _cancel(self): def _cancel(self):
if self._timer and not self._timer.done(): if self._timer and not self._timer.done():
@@ -409,7 +412,19 @@ class SilenceWatchdog(FrameProcessor):
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction) await super().process_frame(frame, direction)
if isinstance(frame, UserStartedSpeakingFrame): # Stop for good once the call is closing — otherwise a timer armed on the goodbye
# turn fires ~silence_secs later, after the call already ended (spurious re-prompt).
if isinstance(frame, (EndFrame, CancelFrame)):
self._stopped = True
self._cancel()
elif isinstance(frame, LLMTextFrame):
self._buf += frame.text
elif isinstance(frame, LLMFullResponseEndFrame):
if EndCallProcessor._is_closing(self._buf):
self._stopped = True
self._cancel()
self._buf = ""
elif isinstance(frame, UserStartedSpeakingFrame):
self._prompts = 0 # caller engaged again — reset self._prompts = 0 # caller engaged again — reset
self._cancel() self._cancel()
elif isinstance(frame, BotStartedSpeakingFrame): elif isinstance(frame, BotStartedSpeakingFrame):
@@ -417,7 +432,9 @@ class SilenceWatchdog(FrameProcessor):
self._cancel() self._cancel()
elif isinstance(frame, BotStoppedSpeakingFrame): elif isinstance(frame, BotStoppedSpeakingFrame):
self._bot_speaking = False self._bot_speaking = False
if self._ending: if self._stopped:
pass # call ending — don't re-arm
elif self._ending:
asyncio.create_task(self._end_soon()) asyncio.create_task(self._end_soon())
else: else:
self._arm() # start counting silence once the agent finishes self._arm() # start counting silence once the agent finishes