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 00000000000..68d22cd383e Binary files /dev/null and b/tests/audio_tests/aws_polly_speech.mp3 differ 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 00000000000..68d22cd383e Binary files /dev/null and b/tests/audio_tests/aws_polly_speech_generative.mp3 differ 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}")