Merge pull request #814 from pipecat-ai/aleix/simli-updates

minor simli updates
This commit is contained in:
Aleix Conchillo Flaqué
2024-12-10 18:48:29 -08:00
committed by GitHub
4 changed files with 95 additions and 93 deletions

View File

@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added `SimliVideoService`. This is an integration for Simli AI avatars.
(see https://www.simli.com)
- Added NVIDIA Riva's `FastPitchTTSService` and `ParakeetSTTService`. - Added NVIDIA Riva's `FastPitchTTSService` and `ParakeetSTTService`.
(see https://www.nvidia.com/en-us/ai-data-science/products/riva/) (see https://www.nvidia.com/en-us/ai-data-science/products/riva/)

View File

@@ -58,5 +58,5 @@ TAVUS_PERSONA_ID=...
SIMLI_API_KEY=... SIMLI_API_KEY=...
SIMLI_FACE_ID=... SIMLI_FACE_ID=...
#Krisp # Krisp
KRISP_MODEL_PATH=... KRISP_MODEL_PATH=...

View File

@@ -1,3 +1,9 @@
#
# Copyright (c) 2024, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio import asyncio
import aiohttp import aiohttp
import os import os
@@ -10,13 +16,10 @@ from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.frames.frames import LLMMessagesFrame from pipecat.frames.frames import LLMMessagesFrame
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.ai_services import AIService
from pipecat.services.cartesia import CartesiaTTSService from pipecat.services.cartesia import CartesiaTTSService
from pipecat.services.elevenlabs import ElevenLabsTTSService
from pipecat.services.openai import OpenAILLMService from pipecat.services.openai import OpenAILLMService
from pipecat.transports.services.daily import DailyParams, DailyTransport from pipecat.transports.services.daily import DailyParams, DailyTransport
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomParams
from runner import configure from runner import configure
from loguru import logger from loguru import logger
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -32,23 +35,11 @@ logger.add(sys.stderr, level="DEBUG")
async def main(): async def main():
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
_, token = await configure(session) room, token = await configure(session)
print("Creating room")
aiohttp_session = aiohttp.ClientSession()
daily_helper = DailyRESTHelper(
daily_api_key=os.getenv("DAILY_API_KEY", ""),
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
aiohttp_session=aiohttp_session,
)
room = await daily_helper.create_room(DailyRoomParams())
expiry_time: float = 60 * 60
token = await daily_helper.get_token(room.url, expiry_time)
print("Room created ", room.url)
transport = DailyTransport( transport = DailyTransport(
room.url, room,
token, token,
"Chatbot", "Simli",
DailyParams( DailyParams(
audio_out_enabled=True, audio_out_enabled=True,
camera_out_enabled=True, camera_out_enabled=True,
@@ -57,45 +48,27 @@ async def main():
vad_enabled=True, vad_enabled=True,
vad_analyzer=SileroVADAnalyzer(), vad_analyzer=SileroVADAnalyzer(),
transcription_enabled=True, transcription_enabled=True,
#
# Spanish
#
# transcription_settings=DailyTranscriptionSettings(
# language="es",
# tier="nova",
# model="2-general"
# )
), ),
) )
# tts = ElevenLabsTTSService(
# api_key=os.getenv("ELEVENLABS_API_KEY"),
# voice_id="pNInz6obpgDQGcFmaJgB",
# )
tts = CartesiaTTSService( tts = CartesiaTTSService(
api_key=os.getenv("CARTESIA_API_KEY"), api_key=os.getenv("CARTESIA_API_KEY"),
voice_id="a167e0f3-df7e-4d52-a9c3-f949145efdab", voice_id="a167e0f3-df7e-4d52-a9c3-f949145efdab",
) )
simli_ai = SimliVideoService(
SimliConfig(os.getenv("SIMLI_API_KEY"), os.getenv("SIMLI_FACE_ID"))
)
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini") llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini")
messages = [ messages = [
{ {
"role": "system", "role": "system",
#
# English
#
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.", "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.",
#
# Spanish
#
# "content": "Eres Chatbot, un amigable y útil robot. Tu objetivo es demostrar tus capacidades de una manera breve. Tus respuestas se convertiran a audio así que nunca no debes incluir caracteres especiales. Contesta a lo que el usuario pregunte de una manera creativa, útil y breve. Empieza por presentarte a ti mismo.",
}, },
] ]
simli_ai = SimliVideoService(
SimliConfig(os.getenv("SIMLI_API_KEY"), os.getenv("SIMLI_FACE_ID"))
)
context = OpenAILLMContext(messages) context = OpenAILLMContext(messages)
context_aggregator = llm.create_context_aggregator(context) context_aggregator = llm.create_context_aggregator(context)

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