Merge pull request #926 from imsakg/main

feat(gemini): add text handling to GeminiMultimodalLive
This commit is contained in:
Mark Backman
2025-01-08 13:42:17 -05:00
committed by GitHub
4 changed files with 120 additions and 2 deletions

View File

@@ -16,13 +16,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `enable_prejoin_ui`, `max_participants` and `start_video_off` params - Added `enable_prejoin_ui`, `max_participants` and `start_video_off` params
to `DailyRoomProperties`. to `DailyRoomProperties`.
- Added `session_timeout` to `FastAPIWebsocketTransport` and `WebsocketServerTransport` - Added `session_timeout` to `FastAPIWebsocketTransport` and `WebsocketServerTransport`
for configuring session timeouts (in seconds). Triggers `on_session_timeout` for custom timeout handling. for configuring session timeouts (in seconds). Triggers `on_session_timeout` for custom timeout handling.
See [examples/websocket-server/bot.py](https://github.com/pipecat-ai/pipecat/blob/main/examples/websocket-server/bot.py). See [examples/websocket-server/bot.py](https://github.com/pipecat-ai/pipecat/blob/main/examples/websocket-server/bot.py).
- Added the new modalities option and helper function to set Gemini output modalities.
- Added `examples/foundational/26d-gemini-multimodal-live-text.py` which is using Gemini as TEXT modality and using another TTS provider for TTS process.
### Changed ### Changed
- api_key, aws_access_key_id and region are no longer required parameters for the PollyTTSService (AWSTTSService) - api_key, aws_access_key_id and region are no longer required parameters for the PollyTTSService (AWSTTSService)
- Added `session_timeout` example in `examples/websocket-server/bot.py` to handle session timeout event. - Added `session_timeout` example in `examples/websocket-server/bot.py` to handle session timeout event.
- Changed `InputParams` in `src/pipecat/services/gemini_multimodal_live/gemini.py` to support different modalities.
### Fixed ### Fixed

View File

@@ -0,0 +1,85 @@
#
# Copyright (c) 2024, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
import os
import sys
import aiohttp
from agent.services.tts.cartesia_multilingual import CartesiaMultiLingualTTSService
from dotenv import load_dotenv
from loguru import logger
from runner import configure
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.audio.vad.vad_analyzer import VADParams
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.services.gemini_multimodal_live.gemini import GeminiMultimodalLiveLLMService
from pipecat.transports.services.daily import DailyParams, DailyTransport
load_dotenv(override=True)
logger.remove(0)
logger.add(sys.stderr, level="DEBUG")
async def main():
async with aiohttp.ClientSession() as session:
(room_url, token) = await configure(session)
transport = DailyTransport(
room_url,
token,
"Respond bot",
DailyParams(
audio_in_sample_rate=16000,
audio_out_sample_rate=24000,
audio_out_enabled=True,
vad_enabled=True,
vad_audio_passthrough=True,
# set stop_secs to something roughly similar to the internal setting
# of the Multimodal Live api, just to align events. This doesn't really
# matter because we can only use the Multimodal Live API's phrase
# endpointing, for now.
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
),
)
llm = GeminiMultimodalLiveLLMService(
api_key=os.getenv("GOOGLE_API_KEY"),
# system_instruction="Talk like a pirate."
)
llm.set_model_only_text() # This forces model to produce text only responses
tts = CartesiaMultiLingualTTSService(api_key=os.getenv("CARTESIA_API_KEY"))
pipeline = Pipeline(
[
transport.input(),
llm,
tts,
transport.output(),
]
)
task = PipelineTask(
pipeline,
PipelineParams(
allow_interruptions=True,
enable_metrics=True,
enable_usage_metrics=True,
),
)
runner = PipelineRunner()
await runner.run(task)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -105,6 +105,7 @@ class InlineData(BaseModel):
class Part(BaseModel): class Part(BaseModel):
inlineData: Optional[InlineData] = None inlineData: Optional[InlineData] = None
text: Optional[str] = None
class ModelTurn(BaseModel): class ModelTurn(BaseModel):

View File

@@ -8,6 +8,7 @@ import asyncio
import base64 import base64
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import websockets import websockets
@@ -132,6 +133,11 @@ class GeminiMultimodalLiveContextAggregatorPair:
return self._assistant return self._assistant
class GeminiMultimodalModalities(Enum):
TEXT = "TEXT"
AUDIO = "AUDIO"
class InputParams(BaseModel): class InputParams(BaseModel):
frequency_penalty: Optional[float] = Field(default=None, ge=0.0, le=2.0) frequency_penalty: Optional[float] = Field(default=None, ge=0.0, le=2.0)
max_tokens: Optional[int] = Field(default=4096, ge=1) max_tokens: Optional[int] = Field(default=4096, ge=1)
@@ -139,6 +145,9 @@ class InputParams(BaseModel):
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0) temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
top_k: Optional[int] = Field(default=None, ge=0) top_k: Optional[int] = Field(default=None, ge=0)
top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0) top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0)
modalities: Optional[GeminiMultimodalModalities] = Field(
default=GeminiMultimodalModalities.AUDIO
)
extra: Optional[Dict[str, Any]] = Field(default_factory=dict) extra: Optional[Dict[str, Any]] = Field(default_factory=dict)
@@ -188,6 +197,7 @@ class GeminiMultimodalLiveLLMService(LLMService):
self._bot_is_speaking = False self._bot_is_speaking = False
self._user_audio_buffer = bytearray() self._user_audio_buffer = bytearray()
self._bot_audio_buffer = bytearray() self._bot_audio_buffer = bytearray()
self._bot_text_buffer = ""
self._settings = { self._settings = {
"frequency_penalty": params.frequency_penalty, "frequency_penalty": params.frequency_penalty,
@@ -196,6 +206,7 @@ class GeminiMultimodalLiveLLMService(LLMService):
"temperature": params.temperature, "temperature": params.temperature,
"top_k": params.top_k, "top_k": params.top_k,
"top_p": params.top_p, "top_p": params.top_p,
"modalities": params.modalities,
"extra": params.extra if isinstance(params.extra, dict) else {}, "extra": params.extra if isinstance(params.extra, dict) else {},
} }
@@ -208,6 +219,9 @@ class GeminiMultimodalLiveLLMService(LLMService):
def set_video_input_paused(self, paused: bool): def set_video_input_paused(self, paused: bool):
self._video_input_paused = paused self._video_input_paused = paused
def set_model_modalities(self, modalities: GeminiMultimodalModalities):
self._settings["modalities"] = modalities
async def set_context(self, context: OpenAILLMContext): async def set_context(self, context: OpenAILLMContext):
"""Set the context explicitly from outside the pipeline. """Set the context explicitly from outside the pipeline.
@@ -383,7 +397,7 @@ class GeminiMultimodalLiveLLMService(LLMService):
"temperature": self._settings["temperature"], "temperature": self._settings["temperature"],
"top_k": self._settings["top_k"], "top_k": self._settings["top_k"],
"top_p": self._settings["top_p"], "top_p": self._settings["top_p"],
"response_modalities": ["AUDIO"], "response_modalities": self._settings["modalities"].value,
"speech_config": { "speech_config": {
"voice_config": { "voice_config": {
"prebuilt_voice_config": {"voice_name": self._voice_id} "prebuilt_voice_config": {"voice_name": self._voice_id}
@@ -604,6 +618,15 @@ class GeminiMultimodalLiveLLMService(LLMService):
part = evt.serverContent.modelTurn.parts[0] part = evt.serverContent.modelTurn.parts[0]
if not part: if not part:
return return
text = part.text
if text:
if not self._bot_text_buffer:
await self.push_frame(LLMFullResponseStartFrame())
self._bot_text_buffer += text
await self.push_frame(TextFrame(text=text))
inline_data = part.inlineData inline_data = part.inlineData
if not inline_data: if not inline_data:
return return
@@ -644,9 +667,15 @@ class GeminiMultimodalLiveLLMService(LLMService):
async def _handle_evt_turn_complete(self, evt): async def _handle_evt_turn_complete(self, evt):
self._bot_is_speaking = False self._bot_is_speaking = False
audio = self._bot_audio_buffer audio = self._bot_audio_buffer
text = self._bot_text_buffer
self._bot_audio_buffer = bytearray() self._bot_audio_buffer = bytearray()
self._bot_text_buffer = ""
if audio and self._transcribe_model_audio and self._context: if audio and self._transcribe_model_audio and self._context:
asyncio.create_task(self._handle_transcribe_model_audio(audio, self._context)) asyncio.create_task(self._handle_transcribe_model_audio(audio, self._context))
elif text:
await self.push_frame(LLMFullResponseEndFrame())
await self.push_frame(TTSStoppedFrame()) await self.push_frame(TTSStoppedFrame())
def create_context_aggregator( def create_context_aggregator(