From 933b63cf1397ab9b0322d395b513e5095b80d2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Mon, 1 Jul 2024 14:57:42 -0700 Subject: [PATCH] processors: added new IdleFrameProcessor --- CHANGELOG.md | 4 + .../processors/idle_frame_processor.py | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/pipecat/processors/idle_frame_processor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 546550365..e0710a03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added `IdleFrameProcessor`. This processor can be used to wait for frames + within a given timeout. If no frame is received within the timeout a provided + callback is called. + - Added new frame `BotSpeakingFrame`. This frame will be continuously pushed upstream while the bot is talking. diff --git a/src/pipecat/processors/idle_frame_processor.py b/src/pipecat/processors/idle_frame_processor.py new file mode 100644 index 000000000..40304a5c6 --- /dev/null +++ b/src/pipecat/processors/idle_frame_processor.py @@ -0,0 +1,76 @@ +# +# Copyright (c) 2024, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio + +from typing import Awaitable, Callable, List + +from pipecat.frames.frames import Frame, SystemFrame +from pipecat.processors.async_frame_processor import AsyncFrameProcessor +from pipecat.processors.frame_processor import FrameDirection + + +class IdleFrameProcessor(AsyncFrameProcessor): + """This class waits to receive any frame or list of desired frames within a + given timeout. If the timeout is reached before receiving any of those + frames the provided callback will be called. + + The callback can then be used to push frames downstream by using + `queue_frame()` (or `push_frame()` for system frames). + + """ + + def __init__( + self, + *, + callback: Callable[["IdleFrameProcessor"], Awaitable[None]], + timeout: float, + types: List[type] = [], + **kwargs): + super().__init__(**kwargs) + + self._callback = callback + self._timeout = timeout + self._types = types + + self._create_idle_task() + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + + if isinstance(frame, SystemFrame): + await self.push_frame(frame, direction) + else: + await self.queue_frame(frame, direction) + + # If we are not waiting for any specific frame set the event, otherwise + # check if we have received one of the desired frames. + if not self._types: + self._idle_event.set() + else: + for t in self._types: + if isinstance(frame, t): + self._idle_event.set() + + # If we are not waiting for any specific frame set the event, otherwise + async def cleanup(self): + self._idle_task.cancel() + await self._idle_task + + def _create_idle_task(self): + self._idle_event = asyncio.Event() + self._idle_task = self.get_event_loop().create_task(self._idle_task_handler()) + + async def _idle_task_handler(self): + while True: + try: + await asyncio.wait_for(self._idle_event.wait(), timeout=self._timeout) + except asyncio.TimeoutError: + await self._callback(self) + except asyncio.CancelledError: + break + finally: + self._idle_event.clear()