From 885b2d1d2f3dcfa6e04b1c347848ca331c4722b8 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 6 May 2025 14:29:36 -0400 Subject: [PATCH] [WIP] AWS Nova Sonic service - make parameters configurable --- src/pipecat/services/aws_nova_sonic/aws.py | 73 ++++++++++++++-------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/pipecat/services/aws_nova_sonic/aws.py b/src/pipecat/services/aws_nova_sonic/aws.py index c2b56ef74..c0367fa6b 100644 --- a/src/pipecat/services/aws_nova_sonic/aws.py +++ b/src/pipecat/services/aws_nova_sonic/aws.py @@ -28,6 +28,7 @@ from aws_sdk_bedrock_runtime.models import ( InvokeModelWithBidirectionalStreamOutput, ) from loguru import logger +from pydantic import BaseModel, Field from smithy_aws_core.credentials_resolvers.static import StaticCredentialsResolver from smithy_aws_core.identity import AWSCredentialsIdentity from smithy_core.aio.eventstream import DuplexEventStream @@ -107,6 +108,23 @@ class CurrentContent: ) +class Params(BaseModel): + # Audio input + input_sample_rate: Optional[int] = Field(default=16000) + input_sample_size: Optional[int] = Field(default=16) + input_channel_count: Optional[int] = Field(default=1) + + # Audio output + output_sample_rate: Optional[int] = Field(default=24000) + output_sample_size: Optional[int] = Field(default=16) + output_channel_count: Optional[int] = Field(default=1) + + # Inference + max_tokens: Optional[int] = Field(default=1024) + top_p: Optional[float] = Field(default=0.9) + temperature: Optional[float] = Field(default=0.7) + + class AWSNovaSonicLLMService(LLMService): # Override the default adapter to use the AWSNovaSonicLLMAdapter one adapter_class = AWSNovaSonicLLMAdapter @@ -120,6 +138,7 @@ class AWSNovaSonicLLMService(LLMService): region: str, model: str = "amazon.nova-sonic-v1:0", voice_id: str = "matthew", # matthew, tiffany, amy + params: Params = Params(), system_instruction: Optional[str] = None, tools: Optional[ToolsSchema] = None, send_transcription_frames: bool = True, @@ -132,6 +151,7 @@ class AWSNovaSonicLLMService(LLMService): self._model = model self._client: BedrockRuntimeClient = None self._voice_id = voice_id + self._params = params self._system_instruction = system_instruction self._tools = tools self._send_transcription_frames = send_transcription_frames @@ -419,18 +439,18 @@ class AWSNovaSonicLLMService(LLMService): # TODO: make params configurable? async def _send_session_start_event(self): - session_start = """ - { - "event": { - "sessionStart": { - "inferenceConfiguration": { - "maxTokens": 1024, - "topP": 0.9, - "temperature": 0.7 - } - } - } - } + session_start = f""" + {{ + "event": {{ + "sessionStart": {{ + "inferenceConfiguration": {{ + "maxTokens": {self._params.max_tokens}, + "topP": {self._params.top_p}, + "temperature": {self._params.temperature} + }} + }} + }} + }} """ await self._send_client_event(session_start) @@ -458,9 +478,9 @@ class AWSNovaSonicLLMService(LLMService): }}, "audioOutputConfiguration": {{ "mediaType": "audio/lpcm", - "sampleRateHertz": 24000, - "sampleSizeBits": 16, - "channelCount": 1, + "sampleRateHertz": {self._params.output_sample_rate}, + "sampleSizeBits": {self._params.output_sample_size}, + "channelCount": {self._params.output_channel_count}, "voiceId": "{self._voice_id}", "encoding": "base64", "audioType": "SPEECH" @@ -483,9 +503,9 @@ class AWSNovaSonicLLMService(LLMService): "role": "USER", "audioInputConfiguration": {{ "mediaType": "audio/lpcm", - "sampleRateHertz": 16000, - "sampleSizeBits": 16, - "channelCount": 1, + "sampleRateHertz": {self._params.input_sample_rate}, + "sampleSizeBits": {self._params.input_sample_size}, + "channelCount": {self._params.input_channel_count}, "audioType": "SPEECH", "encoding": "base64" }} @@ -762,11 +782,10 @@ class AWSNovaSonicLLMService(LLMService): # Push audio frame audio = base64.b64decode(audio_content) - # TODO: make sample rate + channels (used in multiple places) consts frame = TTSAudioRawFrame( audio=audio, - sample_rate=24000, - num_channels=1, + sample_rate=self._params.output_sample_rate, + num_channels=self._params.output_channel_count, ) await self.push_frame(frame) @@ -941,11 +960,13 @@ class AWSNovaSonicLLMService(LLMService): self._triggering_assistant_response = False async def _send_assistant_response_trigger(self): - # TODO: if/when we make bitrate, etc configurable, avoid hard-coding this - chunk_size = 640 # equivalent to what we get from InputAudioRawFrame - chunk_duration = 640 / ( - 16000 * 2 - ) # 640 bytes of 16-bit (2-byte) PCM mono audio at 16kHz corresponds to 0.02 seconds + chunk_duration = 0.02 # what we might get from InputAudioRawFrame + chunk_size = int( + chunk_duration + * self._params.input_sample_rate + * self._params.input_channel_count + * (self._params.input_sample_size / 8) + ) # e.g. 0.02 seconds of 16-bit (2-byte) PCM mono audio at 16kHz is 640 bytes # Lead with a bit of blank audio, if needed. # It seems like the LLM can't quite "hear" the first little bit of audio sent on a