From 2677d9d30d72fa5780480ca0a4675aef22897e81 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 22 Dec 2025 18:19:34 +0530 Subject: [PATCH] [Feat] New provider TTS - Add AWS polly API for TTS (#18326) * add aws_polly as new provider * init AWSPollyTextToSpeechConfig * test_aws_polly_tts_with_native_voice * init aws_polly + AWS polly dispatch * init AWSPollyTextToSpeechConfig * fix transform * add aws_polly as a new provider for TTS API * add to sidebar * docs aws polly * code qa fix * add AWS Polly Text-to-Speech * add cost tracking for AWS polly * docs fix --- docs/my-website/docs/providers/aws_polly.md | 364 ++++++++++++++++ docs/my-website/docs/text_to_speech.md | 9 +- docs/my-website/sidebars.js | 1 + litellm/llms/aws_polly/__init__.py | 0 .../llms/aws_polly/text_to_speech/__init__.py | 0 .../text_to_speech/transformation.py | 391 ++++++++++++++++++ litellm/main.py | 58 ++- ...odel_prices_and_context_window_backup.json | 36 ++ litellm/types/utils.py | 1 + litellm/utils.py | 6 + model_prices_and_context_window.json | 36 ++ provider_endpoints_support.json | 16 + tests/audio_tests/aws_polly_speech.mp3 | Bin 0 -> 33452 bytes .../aws_polly_speech_generative.mp3 | Bin 0 -> 33452 bytes tests/audio_tests/test_audio_speech.py | 162 ++++++++ 15 files changed, 1065 insertions(+), 15 deletions(-) create mode 100644 docs/my-website/docs/providers/aws_polly.md create mode 100644 litellm/llms/aws_polly/__init__.py create mode 100644 litellm/llms/aws_polly/text_to_speech/__init__.py create mode 100644 litellm/llms/aws_polly/text_to_speech/transformation.py create mode 100644 tests/audio_tests/aws_polly_speech.mp3 create mode 100644 tests/audio_tests/aws_polly_speech_generative.mp3 diff --git a/docs/my-website/docs/providers/aws_polly.md b/docs/my-website/docs/providers/aws_polly.md new file mode 100644 index 00000000000..21b0fa679bf --- /dev/null +++ b/docs/my-website/docs/providers/aws_polly.md @@ -0,0 +1,364 @@ +# AWS Polly Text to Speech (tts) + +## Overview + +| Property | Details | +|-------|-------| +| Description | Convert text to natural-sounding speech using AWS Polly's neural and standard TTS engines | +| Provider Route on LiteLLM | `aws_polly/` | +| Supported Operations | `/audio/speech` | +| Link to Provider Doc | [AWS Polly SynthesizeSpeech ↗](https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html) | + +## Quick Start + +### **LiteLLM SDK** + +```python showLineNumbers title="SDK Usage" +import litellm +from pathlib import Path +import os + +# Set environment variables +os.environ["AWS_ACCESS_KEY_ID"] = "" +os.environ["AWS_SECRET_ACCESS_KEY"] = "" +os.environ["AWS_REGION_NAME"] = "us-east-1" + +# AWS Polly call +speech_file_path = Path(__file__).parent / "speech.mp3" +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="the quick brown fox jumped over the lazy dogs", +) +response.stream_to_file(speech_file_path) +``` + +### **LiteLLM PROXY** + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: polly-neural + litellm_params: + model: aws_polly/neural + aws_access_key_id: "os.environ/AWS_ACCESS_KEY_ID" + aws_secret_access_key: "os.environ/AWS_SECRET_ACCESS_KEY" + aws_region_name: "us-east-1" +``` + +## Polly Engines + +AWS Polly supports different speech synthesis engines. Specify the engine in the model name: + +| Model | Engine | Cost (per 1M chars) | Description | +|-------|--------|---------------------|-------------| +| `aws_polly/standard` | Standard | $4.00 | Original Polly voices, faster and lowest cost | +| `aws_polly/neural` | Neural | $16.00 | More natural, human-like speech (recommended) | +| `aws_polly/generative` | Generative | $30.00 | Most expressive, highest quality (limited voices) | +| `aws_polly/long-form` | Long-form | $100.00 | Optimized for long content like articles | + +### **LiteLLM SDK** + +```python showLineNumbers title="Using Different Engines" +import litellm + +# Neural engine (recommended) +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="Hello world", +) + +# Standard engine (lower cost) +response = litellm.speech( + model="aws_polly/standard", + voice="Joanna", + input="Hello world", +) + +# Generative engine (highest quality) +response = litellm.speech( + model="aws_polly/generative", + voice="Matthew", + input="Hello world", +) +``` + +### **LiteLLM PROXY** + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: polly-neural + litellm_params: + model: aws_polly/neural + aws_region_name: "us-east-1" + - model_name: polly-standard + litellm_params: + model: aws_polly/standard + aws_region_name: "us-east-1" + - model_name: polly-generative + litellm_params: + model: aws_polly/generative + aws_region_name: "us-east-1" +``` + +## Available Voices + +### Native Polly Voices + +AWS Polly has many voices across different languages. Here are popular US English voices: + +| Voice | Gender | Engine Support | +|-------|--------|----------------| +| `Joanna` | Female | Neural, Standard | +| `Matthew` | Male | Neural, Standard, Generative | +| `Ivy` | Female (child) | Neural, Standard | +| `Kendra` | Female | Neural, Standard | +| `Amy` | Female (British) | Neural, Standard | +| `Brian` | Male (British) | Neural, Standard | + +### **LiteLLM SDK** + +```python showLineNumbers title="Using Native Polly Voices" +import litellm + +# US English female +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="Hello from Joanna", +) + +# US English male +response = litellm.speech( + model="aws_polly/neural", + voice="Matthew", + input="Hello from Matthew", +) + +# British English female +response = litellm.speech( + model="aws_polly/neural", + voice="Amy", + input="Hello from Amy", +) +``` + +### **LiteLLM PROXY** + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: polly-joanna + litellm_params: + model: aws_polly/neural + voice: "Joanna" + aws_region_name: "us-east-1" + - model_name: polly-matthew + litellm_params: + model: aws_polly/neural + voice: "Matthew" + aws_region_name: "us-east-1" +``` + +### OpenAI Voice Mappings + +LiteLLM also supports OpenAI voice names, which are automatically mapped to Polly voices: + +| OpenAI Voice | Maps to Polly Voice | +|--------------|---------------------| +| `alloy` | Joanna | +| `echo` | Matthew | +| `fable` | Amy | +| `onyx` | Brian | +| `nova` | Ivy | +| `shimmer` | Kendra | + +### **LiteLLM SDK** + +```python showLineNumbers title="Using OpenAI Voice Names" +import litellm + +# These are equivalent +response = litellm.speech( + model="aws_polly/neural", + voice="alloy", # Maps to Joanna + input="Hello world", +) + +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", # Native Polly voice + input="Hello world", +) +``` + +## SSML Support + +AWS Polly supports SSML (Speech Synthesis Markup Language) for advanced control over speech output. LiteLLM automatically detects SSML input. + +### **LiteLLM SDK** + +```python showLineNumbers title="SSML Example" +import litellm + +ssml_input = """ + + Hello, + this is a test with emphasis + and slower speech. + +""" + +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input=ssml_input, +) +``` + +### **LiteLLM PROXY** + +```bash showLineNumbers title="cURL Request with SSML" +curl -X POST http://localhost:4000/v1/audio/speech \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "polly-neural", + "voice": "Joanna", + "input": "Hello world" + }' \ + --output speech.mp3 +``` + +## Supported Parameters + +```python showLineNumbers title="All Parameters" +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", # Required: Voice selection + input="text to convert", # Required: Input text (or SSML) + response_format="mp3", # Optional: mp3, ogg_vorbis, pcm + + # AWS-specific parameters + language_code="en-US", # Optional: Language code + sample_rate="22050", # Optional: Sample rate in Hz +) +``` + +## Response Formats + +| Format | Description | +|--------|-------------| +| `mp3` | MP3 audio (default) | +| `ogg_vorbis` | Ogg Vorbis audio | +| `pcm` | Raw PCM audio | + +### **LiteLLM SDK** + +```python showLineNumbers title="Different Response Formats" +import litellm + +# MP3 (default) +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="Hello", + response_format="mp3", +) + +# Ogg Vorbis +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="Hello", + response_format="ogg_vorbis", +) +``` + +## AWS Authentication + +LiteLLM supports multiple AWS authentication methods. + +### **LiteLLM SDK** + +```python showLineNumbers title="Authentication Options" +import litellm +import os + +# Option 1: Environment variables (recommended) +os.environ["AWS_ACCESS_KEY_ID"] = "your-access-key" +os.environ["AWS_SECRET_ACCESS_KEY"] = "your-secret-key" +os.environ["AWS_REGION_NAME"] = "us-east-1" + +response = litellm.speech(model="aws_polly/neural", voice="Joanna", input="Hello") + +# Option 2: Pass credentials directly +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="Hello", + aws_access_key_id="your-access-key", + aws_secret_access_key="your-secret-key", + aws_region_name="us-east-1", +) + +# Option 3: IAM Role (when running on AWS) +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="Hello", + aws_region_name="us-east-1", +) + +# Option 4: AWS Profile +response = litellm.speech( + model="aws_polly/neural", + voice="Joanna", + input="Hello", + aws_profile_name="my-profile", +) +``` + +### **LiteLLM PROXY** + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + # Using environment variables + - model_name: polly-neural + litellm_params: + model: aws_polly/neural + aws_access_key_id: "os.environ/AWS_ACCESS_KEY_ID" + aws_secret_access_key: "os.environ/AWS_SECRET_ACCESS_KEY" + aws_region_name: "us-east-1" + + # Using IAM Role (when proxy runs on AWS) + - model_name: polly-neural-iam + litellm_params: + model: aws_polly/neural + aws_region_name: "us-east-1" + + # Using AWS Profile + - model_name: polly-neural-profile + litellm_params: + model: aws_polly/neural + aws_profile_name: "my-profile" +``` + +## Async Support + +```python showLineNumbers title="Async Usage" +import litellm +import asyncio + +async def main(): + response = await litellm.aspeech( + model="aws_polly/neural", + voice="Joanna", + input="Hello from async AWS Polly", + aws_region_name="us-east-1", + ) + + with open("output.mp3", "wb") as f: + f.write(response.content) + +asyncio.run(main()) +``` diff --git a/docs/my-website/docs/text_to_speech.md b/docs/my-website/docs/text_to_speech.md index ea2a9c2eff3..ce298b538df 100644 --- a/docs/my-website/docs/text_to_speech.md +++ b/docs/my-website/docs/text_to_speech.md @@ -14,7 +14,7 @@ import TabItem from '@theme/TabItem'; | Fallbacks | ✅ | Works between supported models | | Loadbalancing | ✅ | Works between supported models | | Guardrails | ✅ | Applies to input text (non-streaming only) | -| Supported Providers | OpenAI, Azure OpenAI, Vertex AI | | +| Supported Providers | OpenAI, Azure OpenAI, Vertex AI, AWS Polly, ElevenLabs | | ## **LiteLLM Python SDK Usage** ### Quick Start @@ -101,6 +101,7 @@ litellm --config /path/to/config.yaml | OpenAI | [Usage](#quick-start) | | Azure OpenAI| [Usage](../docs/providers/azure#azure-text-to-speech-tts) | | Azure AI Speech Service (AVA)| [Usage](../docs/providers/azure_ai_speech) | +| AWS Polly | [Usage](#aws-polly-text-to-speech) | | Vertex AI | [Usage](../docs/providers/vertex#text-to-speech-apis) | | Gemini | [Usage](#gemini-text-to-speech) | | ElevenLabs | [Usage](../docs/providers/elevenlabs#text-to-speech-tts) | @@ -246,6 +247,12 @@ curl http://0.0.0.0:4000/v1/audio/speech \ --output vertex_speech.mp3 ``` +### AWS Polly Text-to-Speech + +AWS Polly provides neural and standard text-to-speech engines with support for multiple voices and languages. + +See the [AWS Polly provider documentation](../docs/providers/aws_polly) for detailed usage examples. + ## ✨ Enterprise LiteLLM Proxy - Set Max Request File Size Use this when you want to limit the file size for requests sent to `audio/transcriptions` diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index fbaebf19366..b6b8fe1223d 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -664,6 +664,7 @@ const sidebars = { "providers/bedrock_agents", "providers/bedrock_writer", "providers/bedrock_batches", + "providers/aws_polly", "providers/bedrock_vector_store", ] }, diff --git a/litellm/llms/aws_polly/__init__.py b/litellm/llms/aws_polly/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/aws_polly/text_to_speech/__init__.py b/litellm/llms/aws_polly/text_to_speech/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/aws_polly/text_to_speech/transformation.py b/litellm/llms/aws_polly/text_to_speech/transformation.py new file mode 100644 index 00000000000..dc6c40000f1 --- /dev/null +++ b/litellm/llms/aws_polly/text_to_speech/transformation.py @@ -0,0 +1,391 @@ +""" +AWS Polly Text-to-Speech transformation + +Maps OpenAI TTS spec to AWS Polly SynthesizeSpeech API +Reference: https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html +""" + +import json +from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Tuple, Union + +import httpx + +from litellm.llms.base_llm.text_to_speech.transformation import ( + BaseTextToSpeechConfig, + TextToSpeechRequestData, +) +from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj + from litellm.types.llms.openai import HttpxBinaryResponseContent +else: + LiteLLMLoggingObj = Any + HttpxBinaryResponseContent = Any + + +class AWSPollyTextToSpeechConfig(BaseTextToSpeechConfig, BaseAWSLLM): + """ + Configuration for AWS Polly Text-to-Speech + + Reference: https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html + """ + + def __init__(self): + BaseTextToSpeechConfig.__init__(self) + BaseAWSLLM.__init__(self) + + # Default settings + DEFAULT_VOICE = "Joanna" + DEFAULT_ENGINE = "neural" + DEFAULT_OUTPUT_FORMAT = "mp3" + DEFAULT_REGION = "us-east-1" + + # Voice name mappings from OpenAI voices to Polly voices + VOICE_MAPPINGS = { + "alloy": "Joanna", # US English female + "echo": "Matthew", # US English male + "fable": "Amy", # British English female + "onyx": "Brian", # British English male + "nova": "Ivy", # US English female (child) + "shimmer": "Kendra", # US English female + } + + # Response format mappings from OpenAI to Polly + FORMAT_MAPPINGS = { + "mp3": "mp3", + "opus": "ogg_vorbis", + "aac": "mp3", # Polly doesn't support AAC, use MP3 + "flac": "mp3", # Polly doesn't support FLAC, use MP3 + "wav": "pcm", + "pcm": "pcm", + } + + # Valid Polly engines + VALID_ENGINES = {"standard", "neural", "long-form", "generative"} + + def dispatch_text_to_speech( + self, + model: str, + input: str, + voice: Optional[Union[str, Dict]], + optional_params: Dict, + litellm_params_dict: Dict, + logging_obj: "LiteLLMLoggingObj", + timeout: Union[float, httpx.Timeout], + extra_headers: Optional[Dict[str, Any]], + base_llm_http_handler: Any, + aspeech: bool, + api_base: Optional[str], + api_key: Optional[str], + **kwargs: Any, + ) -> Union[ + "HttpxBinaryResponseContent", + Coroutine[Any, Any, "HttpxBinaryResponseContent"], + ]: + """ + Dispatch method to handle AWS Polly TTS requests + + This method encapsulates AWS-specific credential resolution and parameter handling + + Args: + base_llm_http_handler: The BaseLLMHTTPHandler instance from main.py + """ + # Get AWS region from kwargs or environment + aws_region_name = kwargs.get("aws_region_name") or self._get_aws_region_name_for_polly( + optional_params=optional_params + ) + + # Convert voice to string if it's a dict + voice_str: Optional[str] = None + if isinstance(voice, str): + voice_str = voice + elif isinstance(voice, dict): + voice_str = voice.get("name") if voice else None + + # Update litellm_params with resolved values + # Note: AWS credentials (aws_access_key_id, aws_secret_access_key, etc.) + # are already in litellm_params_dict via get_litellm_params() in main.py + litellm_params_dict["aws_region_name"] = aws_region_name + litellm_params_dict["api_base"] = api_base + litellm_params_dict["api_key"] = api_key + + # Call the text_to_speech_handler + response = base_llm_http_handler.text_to_speech_handler( + model=model, + input=input, + voice=voice_str, + text_to_speech_provider_config=self, + text_to_speech_optional_params=optional_params, + custom_llm_provider="aws_polly", + litellm_params=litellm_params_dict, + logging_obj=logging_obj, + timeout=timeout, + extra_headers=extra_headers, + client=None, + _is_async=aspeech, + ) + + return response + + def _get_aws_region_name_for_polly(self, optional_params: Dict) -> str: + """Get AWS region name for Polly API calls.""" + aws_region_name = optional_params.get("aws_region_name") + if aws_region_name is None: + aws_region_name = self.get_aws_region_name_for_non_llm_api_calls() + return aws_region_name + + def get_supported_openai_params(self, model: str) -> list: + """ + AWS Polly TTS supports these OpenAI parameters + """ + return ["voice", "response_format", "speed"] + + def map_openai_params( + self, + model: str, + optional_params: Dict, + voice: Optional[Union[str, Dict]] = None, + drop_params: bool = False, + kwargs: Dict = {}, + ) -> Tuple[Optional[str], Dict]: + """ + Map OpenAI parameters to AWS Polly parameters + """ + mapped_params = {} + + # Map voice - support both native Polly voices and OpenAI voice mappings + mapped_voice: Optional[str] = None + if isinstance(voice, str): + if voice in self.VOICE_MAPPINGS: + # OpenAI voice -> Polly voice + mapped_voice = self.VOICE_MAPPINGS[voice] + else: + # Assume it's already a Polly voice name + mapped_voice = voice + + # Map response format + if "response_format" in optional_params: + format_name = optional_params["response_format"] + if format_name in self.FORMAT_MAPPINGS: + mapped_params["output_format"] = self.FORMAT_MAPPINGS[format_name] + else: + mapped_params["output_format"] = format_name + else: + mapped_params["output_format"] = self.DEFAULT_OUTPUT_FORMAT + + # Extract engine from model name (e.g., "aws_polly/neural" -> "neural") + engine = self._extract_engine_from_model(model) + mapped_params["engine"] = engine + + # Pass through Polly-specific parameters (use AWS API casing) + if "language_code" in kwargs: + mapped_params["LanguageCode"] = kwargs["language_code"] + if "lexicon_names" in kwargs: + mapped_params["LexiconNames"] = kwargs["lexicon_names"] + if "sample_rate" in kwargs: + mapped_params["SampleRate"] = kwargs["sample_rate"] + + return mapped_voice, mapped_params + + def _extract_engine_from_model(self, model: str) -> str: + """ + Extract engine from model name. + + Examples: + - aws_polly/neural -> neural + - aws_polly/standard -> standard + - aws_polly/long-form -> long-form + - aws_polly -> neural (default) + """ + if "/" in model: + parts = model.split("/") + if len(parts) >= 2: + engine = parts[1].lower() + if engine in self.VALID_ENGINES: + return engine + return self.DEFAULT_ENGINE + + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + """ + Validate AWS environment and set up headers. + AWS SigV4 signing will be done in transform_text_to_speech_request. + """ + validated_headers = headers.copy() + validated_headers["Content-Type"] = "application/json" + return validated_headers + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for AWS Polly SynthesizeSpeech request + + Polly endpoint format: + https://polly.{region}.amazonaws.com/v1/speech + """ + if api_base is not None: + return api_base.rstrip("/") + "/v1/speech" + + aws_region_name = litellm_params.get("aws_region_name", self.DEFAULT_REGION) + return f"https://polly.{aws_region_name}.amazonaws.com/v1/speech" + + def is_ssml_input(self, input: str) -> bool: + """ + Returns True if input is SSML, False otherwise. + + Based on AWS Polly SSML requirements - must contain tag. + """ + return "" in input or " Tuple[Dict[str, str], str]: + """ + Sign the AWS Polly request using SigV4. + + Returns: + Tuple of (signed_headers, json_body_string) + """ + try: + from botocore.auth import SigV4Auth + from botocore.awsrequest import AWSRequest + except ImportError: + raise ImportError("Missing boto3 to call AWS Polly. Run 'pip install boto3'.") + + # Get AWS region + aws_region_name = litellm_params.get("aws_region_name", self.DEFAULT_REGION) + + # Get AWS credentials + credentials = self.get_credentials( + aws_access_key_id=litellm_params.get("aws_access_key_id"), + aws_secret_access_key=litellm_params.get("aws_secret_access_key"), + aws_session_token=litellm_params.get("aws_session_token"), + aws_region_name=aws_region_name, + aws_session_name=litellm_params.get("aws_session_name"), + aws_profile_name=litellm_params.get("aws_profile_name"), + aws_role_name=litellm_params.get("aws_role_name"), + aws_web_identity_token=litellm_params.get("aws_web_identity_token"), + aws_sts_endpoint=litellm_params.get("aws_sts_endpoint"), + aws_external_id=litellm_params.get("aws_external_id"), + ) + + # Serialize request body to JSON + json_body = json.dumps(request_body) + + # Create headers for signing + headers = { + "Content-Type": "application/json", + } + + # Create AWS request for signing + aws_request = AWSRequest( + method="POST", + url=endpoint_url, + data=json_body, + headers=headers, + ) + + # Sign the request + SigV4Auth(credentials, "polly", aws_region_name).add_auth(aws_request) + + # Return signed headers and body + return dict(aws_request.headers), json_body + + def transform_text_to_speech_request( + self, + model: str, + input: str, + voice: Optional[str], + optional_params: Dict, + litellm_params: Dict, + headers: dict, + ) -> TextToSpeechRequestData: + """ + Transform OpenAI TTS request to AWS Polly SynthesizeSpeech format. + + Supports: + - Native Polly voices (Joanna, Matthew, etc.) + - OpenAI voice mapping (alloy, echo, etc.) + - SSML input (auto-detected via tag) + - Multiple engines (neural, standard, long-form, generative) + + Returns: + TextToSpeechRequestData: Contains signed request for Polly API + """ + # Get voice (already mapped in main.py, or use default) + polly_voice = voice or self.DEFAULT_VOICE + + # Get output format + output_format = optional_params.get("output_format", self.DEFAULT_OUTPUT_FORMAT) + + # Get engine + engine = optional_params.get("engine", self.DEFAULT_ENGINE) + + # Build request body + request_body: Dict[str, Any] = { + "Engine": engine, + "OutputFormat": output_format, + "Text": input, + "VoiceId": polly_voice, + } + + # Auto-detect SSML + if self.is_ssml_input(input): + request_body["TextType"] = "ssml" + else: + request_body["TextType"] = "text" + + # Add optional Polly parameters (already in AWS casing from map_openai_params) + for key in ["LanguageCode", "LexiconNames", "SampleRate"]: + if key in optional_params: + request_body[key] = optional_params[key] + + # Get endpoint URL + endpoint_url = self.get_complete_url( + model=model, + api_base=litellm_params.get("api_base"), + litellm_params=litellm_params, + ) + + # Sign the request with AWS SigV4 + signed_headers, json_body = self._sign_polly_request( + request_body=request_body, + endpoint_url=endpoint_url, + litellm_params=litellm_params, + ) + + # Return as ssml_body so the handler uses data= instead of json= + # This preserves the exact JSON string that was signed + return TextToSpeechRequestData( + ssml_body=json_body, + headers=signed_headers, + ) + + def transform_text_to_speech_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: "LiteLLMLoggingObj", + ) -> "HttpxBinaryResponseContent": + """ + Transform AWS Polly response to standard format. + + Polly returns the audio data directly in the response body. + """ + from litellm.types.llms.openai import HttpxBinaryResponseContent + + return HttpxBinaryResponseContent(raw_response) + diff --git a/litellm/main.py b/litellm/main.py index 5550d098985..60fe3eb2dec 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -105,10 +105,22 @@ from litellm.llms.vertex_ai.common_utils import ( from litellm.realtime_api.main import _realtime_health_check from litellm.secret_managers.main import get_secret_bool, get_secret_str from litellm.types.router import GenericLiteLLMParams -from litellm.types.utils import RawRequestTypedDict, StreamingChoices +from litellm.types.utils import ( + ModelResponseStream, + RawRequestTypedDict, + StreamingChoices, +) from litellm.utils import ( + Choices, CustomStreamWrapper, + EmbeddingResponse, + Message, + ModelResponse, ProviderConfigManager, + TextChoices, + TextCompletionResponse, + TextCompletionStreamWrapper, + TranscriptionResponse, Usage, _get_model_info_helper, add_provider_specific_params_to_optional_params, @@ -166,8 +178,8 @@ from .llms.azure_ai.anthropic.handler import AzureAnthropicChatCompletion from .llms.azure_ai.embed import AzureAIEmbedding from .llms.bedrock.chat import BedrockConverseLLM, BedrockLLM from .llms.bedrock.embed.embedding import BedrockEmbedding -from .llms.bedrock.image_generation.image_handler import BedrockImageGeneration from .llms.bedrock.image_edit.handler import BedrockImageEdit +from .llms.bedrock.image_generation.image_handler import BedrockImageGeneration from .llms.bytez.chat.transformation import BytezChatConfig from .llms.clarifai.chat.transformation import ClarifaiConfig from .llms.codestral.completion.handler import CodestralTextCompletion @@ -240,18 +252,6 @@ from .types.utils import ( all_litellm_params, ) -from litellm.types.utils import ModelResponseStream -from litellm.utils import ( - Choices, - EmbeddingResponse, - Message, - ModelResponse, - TextChoices, - TextCompletionResponse, - TextCompletionStreamWrapper, - TranscriptionResponse, -) - ####### ENVIRONMENT VARIABLES ################### openai_chat_completions = OpenAIChatCompletion() openai_text_completions = OpenAITextCompletion() @@ -6471,6 +6471,35 @@ def speech( # noqa: PLR0915 api_key=api_key, **kwargs, ) + elif custom_llm_provider == "aws_polly": + from litellm.llms.aws_polly.text_to_speech.transformation import ( + AWSPollyTextToSpeechConfig, + ) + + # AWS Polly Text-to-Speech + if text_to_speech_provider_config is None: + text_to_speech_provider_config = AWSPollyTextToSpeechConfig() + + # Cast to specific AWS Polly config type to access dispatch method + aws_polly_config = cast( + AWSPollyTextToSpeechConfig, text_to_speech_provider_config + ) + + response = aws_polly_config.dispatch_text_to_speech( + model=model, + input=input, + voice=voice, + optional_params=optional_params, + litellm_params_dict=litellm_params_dict, + logging_obj=logging_obj, + timeout=timeout, + extra_headers=extra_headers, + base_llm_http_handler=base_llm_http_handler, + aspeech=aspeech or False, + api_base=api_base, + api_key=api_key, + **kwargs, + ) if response is None: raise Exception( @@ -6905,6 +6934,7 @@ def _get_encoding(): global _encoding_cache if _encoding_cache is None: import sys + # Access via module to trigger __getattr__ if not cached _encoding_cache = sys.modules[__name__].encoding return _encoding_cache diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index e973c43f60c..f4b42d1fd6e 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -25423,6 +25423,42 @@ "/v1/audio/speech" ] }, + "aws_polly/standard": { + "input_cost_per_character": 4e-06, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, + "aws_polly/neural": { + "input_cost_per_character": 1.6e-05, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, + "aws_polly/long-form": { + "input_cost_per_character": 1e-04, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, + "aws_polly/generative": { + "input_cost_per_character": 3e-05, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, "us.amazon.nova-lite-v1:0": { "input_cost_per_token": 6e-08, "litellm_provider": "bedrock_converse", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index f39afbfc2e0..3416459bc28 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2915,6 +2915,7 @@ class LlmProviders(str, Enum): BYTEZ = "bytez" REPLICATE = "replicate" RUNWAYML = "runwayml" + AWS_POLLY = "aws_polly" HUGGINGFACE = "huggingface" TOGETHER_AI = "together_ai" OPENROUTER = "openrouter" diff --git a/litellm/utils.py b/litellm/utils.py index 8aa5eb6561b..805fbafcfce 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8096,6 +8096,12 @@ class ProviderConfigManager: ) return VertexAITextToSpeechConfig() + elif litellm.LlmProviders.AWS_POLLY == provider: + from litellm.llms.aws_polly.text_to_speech.transformation import ( + AWSPollyTextToSpeechConfig, + ) + + return AWSPollyTextToSpeechConfig() return None @staticmethod diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index e973c43f60c..f4b42d1fd6e 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -25423,6 +25423,42 @@ "/v1/audio/speech" ] }, + "aws_polly/standard": { + "input_cost_per_character": 4e-06, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, + "aws_polly/neural": { + "input_cost_per_character": 1.6e-05, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, + "aws_polly/long-form": { + "input_cost_per_character": 1e-04, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, + "aws_polly/generative": { + "input_cost_per_character": 3e-05, + "litellm_provider": "aws_polly", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ], + "source": "https://aws.amazon.com/polly/pricing/" + }, "us.amazon.nova-lite-v1:0": { "input_cost_per_token": 6e-08, "litellm_provider": "bedrock_converse", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 72e3bbbe1fd..152b3df52e6 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -205,6 +205,22 @@ "a2a": true } }, + "aws_polly": { + "display_name": "AWS - Polly (`aws_polly`)", + "url": "https://docs.litellm.ai/docs/providers/aws_polly", + "endpoints": { + "chat_completions": false, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": true, + "moderations": false, + "batches": false, + "rerank": false + } + }, "azure": { "display_name": "Azure (`azure`)", "url": "https://docs.litellm.ai/docs/providers/azure", diff --git a/tests/audio_tests/aws_polly_speech.mp3 b/tests/audio_tests/aws_polly_speech.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..68d22cd383ef973829c52b84fc759a7238477573 GIT binary patch literal 33452 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 literal 0 HcmV?d00001 diff --git a/tests/audio_tests/aws_polly_speech_generative.mp3 b/tests/audio_tests/aws_polly_speech_generative.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..68d22cd383ef973829c52b84fc759a7238477573 GIT binary patch literal 33452 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 literal 0 HcmV?d00001 diff --git a/tests/audio_tests/test_audio_speech.py b/tests/audio_tests/test_audio_speech.py index fa71326e11b..67e0dbffa61 100644 --- a/tests/audio_tests/test_audio_speech.py +++ b/tests/audio_tests/test_audio_speech.py @@ -522,3 +522,165 @@ async def test_azure_ava_tts_fable_voice_mapping(): assert "Testing voice mapping" in ssml_body assert " Joanna). + Verifies that OpenAI voices are correctly mapped to Polly voices. + """ + import json + from unittest.mock import MagicMock, patch + import httpx + + mock_response_content = b"fake_audio_data" + mock_httpx_response = MagicMock(spec=httpx.Response) + mock_httpx_response.content = mock_response_content + mock_httpx_response.status_code = 200 + mock_httpx_response.headers = {"content-type": "audio/mpeg"} + + with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post") as mock_post: + mock_post.return_value = mock_httpx_response + + response = await litellm.aspeech( + model="aws_polly/neural", + voice="alloy", + input="Testing OpenAI voice mapping", + aws_region_name="us-east-1", + ) + + assert mock_post.called + + call_args = mock_post.call_args + request_data = call_args.kwargs.get("data") + + # Parse the JSON body + assert request_data is not None + request_body = json.loads(request_data) + + # Verify alloy was mapped to Joanna + assert request_body["VoiceId"] == "Joanna" + assert request_body["Text"] == "Testing OpenAI voice mapping" + + +@pytest.mark.asyncio +async def test_aws_polly_tts_with_ssml(): + """ + Test AWS Polly TTS with SSML input. + Verifies that SSML is detected and TextType is set correctly. + """ + import json + from unittest.mock import MagicMock, patch + import httpx + + mock_response_content = b"fake_audio_data" + mock_httpx_response = MagicMock(spec=httpx.Response) + mock_httpx_response.content = mock_response_content + mock_httpx_response.status_code = 200 + mock_httpx_response.headers = {"content-type": "audio/mpeg"} + + ssml_input = 'Hello, this is SSML.' + + with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post") as mock_post: + mock_post.return_value = mock_httpx_response + + response = await litellm.aspeech( + model="aws_polly/neural", + voice="Joanna", + input=ssml_input, + aws_region_name="us-east-1", + ) + + assert mock_post.called + + call_args = mock_post.call_args + request_data = call_args.kwargs.get("data") + + # Parse the JSON body + assert request_data is not None + request_body = json.loads(request_data) + + # Verify SSML is detected and TextType is set to ssml + assert request_body["Text"] == ssml_input + assert request_body["TextType"] == "ssml" + assert request_body["VoiceId"] == "Joanna" + + +@pytest.mark.asyncio +async def test_aws_polly_tts_real_api(): + """ + Test AWS Polly TTS with real API request. + Requires AWS credentials to be configured. + """ + speech_file_path = Path(__file__).parent / "aws_polly_speech_generative.mp3" + + response = await litellm.aspeech( + model="aws_polly/generative", + voice="Joanna", + input="Hello, this is a test of AWS Polly text to speech integration with LiteLLM.", + aws_region_name="us-east-1", + ) + + from litellm.types.llms.openai import HttpxBinaryResponseContent + + assert isinstance(response, HttpxBinaryResponseContent) + + binary_content = response.content + assert len(binary_content) > 0 + + # MP3 files start with ID3 tag or MPEG sync word + assert binary_content[:3] == b"ID3" or binary_content[:2] == b"\xff\xfb" or binary_content[:2] == b"\xff\xf3" + + response.stream_to_file(speech_file_path) + + assert speech_file_path.exists() + assert speech_file_path.stat().st_size > 0 + + print(f"AWS Polly TTS audio saved to: {speech_file_path}")