services(simli): remove ready flag, events vs sleep, handle CancelledError

This commit is contained in:
Aleix Conchillo Flaqué
2024-12-10 09:54:45 -08:00
parent b85072637f
commit 87c1f2bcce

View File

@@ -1,3 +1,9 @@
#
# Copyright (c) 2024, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio import asyncio
from pipecat.frames.frames import ( from pipecat.frames.frames import (
@@ -11,19 +17,31 @@ from pipecat.frames.frames import (
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, StartFrame from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, StartFrame
import numpy as np import numpy as np
from av import AudioFrame
from av.audio.resampler import AudioResampler
from simli import SimliClient, SimliConfig
from loguru import logger from loguru import logger
try:
from av.audio.frame import AudioFrame
from av.audio.resampler import AudioResampler
from simli import SimliClient, SimliConfig
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error("In order to use Simli, you need to `pip install pipecat-ai[simli]`.")
raise Exception(f"Missing module: {e}")
class SimliVideoService(FrameProcessor): class SimliVideoService(FrameProcessor):
def __init__(self, simli_config: SimliConfig, use_turn_server=False, latency_interval=0): def __init__(
self,
simli_config: SimliConfig,
use_turn_server: bool = False,
latency_interval: int = 0,
):
super().__init__() super().__init__()
self._simli_client = SimliClient(simli_config, use_turn_server, latency_interval) self._simli_client = SimliClient(simli_config, use_turn_server, latency_interval)
self._ready = False self._pipecat_resampler_event = asyncio.Event()
self._pipecat_resampler: AudioResampler = None self._pipecat_resampler: AudioResampler = None
self._simli_resampler = AudioResampler("s16", 1, 16000) self._simli_resampler = AudioResampler("s16", 1, 16000)
@@ -32,17 +50,14 @@ class SimliVideoService(FrameProcessor):
async def _start_connection(self): async def _start_connection(self):
await self._simli_client.Initialize() await self._simli_client.Initialize()
self._ready = True
# Create task to consume and process audio and video # Create task to consume and process audio and video
self._audio_task = asyncio.create_task(self._consume_and_process_audio()) self._audio_task = asyncio.create_task(self._consume_and_process_audio())
self._video_task = asyncio.create_task(self._consume_and_process_video()) self._video_task = asyncio.create_task(self._consume_and_process_video())
async def _consume_and_process_audio(self): async def _consume_and_process_audio(self):
while self._pipecat_resampler is None: try:
await asyncio.sleep(0.001) await self._pipecat_resampler_event.wait()
async for audio_frame in self._simli_client.getAudioStreamIterator(): async for audio_frame in self._simli_client.getAudioStreamIterator():
# Process the audio frame
try:
resampled_frames = self._pipecat_resampler.resample(audio_frame) resampled_frames = self._pipecat_resampler.resample(audio_frame)
for resampled_frame in resampled_frames: for resampled_frame in resampled_frames:
await self.push_frame( await self.push_frame(
@@ -52,60 +67,71 @@ class SimliVideoService(FrameProcessor):
num_channels=1, num_channels=1,
), ),
) )
except Exception as e: except Exception as e:
logger.exception(f"{self} exception: {e}") logger.exception(f"{self} exception: {e}")
except asyncio.CancelledError:
pass
async def _consume_and_process_video(self): async def _consume_and_process_video(self):
while self._pipecat_resampler is None: try:
await asyncio.sleep(0.001) await self._pipecat_resampler_event.wait()
async for video_frame in self._simli_client.getVideoStreamIterator(targetFormat="rgb24"): async for video_frame in self._simli_client.getVideoStreamIterator(
# Process the video frame targetFormat="rgb24"
convertedFrame: OutputImageRawFrame = OutputImageRawFrame( ):
image=video_frame.to_rgb().to_image().tobytes(), # Process the video frame
size=(video_frame.width, video_frame.height), convertedFrame: OutputImageRawFrame = OutputImageRawFrame(
format="RGB", image=video_frame.to_rgb().to_image().tobytes(),
) size=(video_frame.width, video_frame.height),
convertedFrame.pts = video_frame.pts format="RGB",
await self.push_frame( )
convertedFrame, convertedFrame.pts = video_frame.pts
) await self.push_frame(convertedFrame)
except Exception as e:
logger.exception(f"{self} exception: {e}")
except asyncio.CancelledError:
pass
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, StartFrame): if isinstance(frame, StartFrame):
await self.push_frame(frame, direction)
await self._start_connection() await self._start_connection()
elif isinstance(frame, TTSAudioRawFrame): elif isinstance(frame, TTSAudioRawFrame):
# Send audio frame to Simli # Send audio frame to Simli
try: try:
if self._ready: old_frame = AudioFrame.from_ndarray(
oldFrame = AudioFrame.from_ndarray( np.frombuffer(frame.audio, dtype=np.int16)[None, :],
np.frombuffer(frame.audio, dtype=np.int16)[None, :], layout="mono" if frame.num_channels == 1 else "stereo",
layout=frame.num_channels, )
) old_frame.sample_rate = frame.sample_rate
oldFrame.sample_rate = frame.sample_rate
if self._pipecat_resampler is None:
self._pipecat_resampler = AudioResampler(
"s16", oldFrame.layout, oldFrame.sample_rate
)
resampledFrame = self._simli_resampler.resample(oldFrame) if self._pipecat_resampler is None:
for frame in resampledFrame: self._pipecat_resampler = AudioResampler(
await self._simli_client.send(frame.to_ndarray().astype(np.int16).tobytes()) "s16", old_frame.layout, old_frame.sample_rate
return )
else: self._pipecat_resampler_event.set()
logger.warning(
"Simli Connection is not Initialized properly, passing audio to next processor" resampled_frames = self._simli_resampler.resample(old_frame)
for resampled_frame in resampled_frames:
await self._simli_client.send(
resampled_frame.to_ndarray().astype(np.int16).tobytes()
) )
except Exception as e: except Exception as e:
logger.exception(f"{self} exception: {e}") logger.exception(f"{self} exception: {e}")
elif isinstance(frame, (EndFrame, CancelFrame)): elif isinstance(frame, (EndFrame, CancelFrame)):
await self._simli_client.stop() await self._stop()
self._audio_task.cancel() await self.push_frame(frame, direction)
await self._audio_task
self._video_task.cancel()
await self._video_task
elif isinstance(frame, StartInterruptionFrame): elif isinstance(frame, StartInterruptionFrame):
await self._simli_client.clearBuffer() await self._simli_client.clearBuffer()
await self.push_frame(frame, direction)
else:
await self.push_frame(frame, direction)
await self.push_frame(frame, direction) async def _stop(self):
await self._simli_client.stop()
if self._audio_task:
self._audio_task.cancel()
await self._audio_task
if self._video_task:
self._video_task.cancel()
await self._video_task