Merge pull request #926 from imsakg/main
feat(gemini): add text handling to GeminiMultimodalLive
This commit is contained in:
@@ -18,11 +18,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- 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
|
||||||
|
|
||||||
|
|||||||
85
examples/foundational/26d-gemini-multimodal-live-text.py
Normal file
85
examples/foundational/26d-gemini-multimodal-live-text.py
Normal 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())
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user