[Fix] Azure AI Speech - Ensure voice is mapped from request body -> SSML body , allow sending role and style (#15810)

* update map_openai_params

* fix update voice transform

* fix text_to_speech_provider_config

* test_azure_ava_tts_with_custom_voice

* test Azure AVA style, role sent

* _build_express_as_element

* docs custom params

* build LANG

* fix transform

* fix transform

* fix speech

* docs update

* docs azure ai speech
This commit is contained in:
Ishaan Jaff
2025-10-22 14:41:11 -07:00
committed by GitHub
parent d91efa7a7b
commit ec6a5ffa2d
6 changed files with 680 additions and 44 deletions
@@ -136,6 +136,168 @@ response = speech(
| `wav` | riff-24khz-16bit-mono-pcm | 24kHz |
| `pcm` | raw-24khz-16bit-mono-pcm | 24kHz |
## Sending Azure-Specific Params
Azure AI Speech supports advanced SSML features through optional parameters:
- `style`: Speaking style (e.g., "cheerful", "sad", "angry", "whispering")
- `styledegree`: Style intensity (0.01 to 2)
- `role`: Voice role (e.g., "Girl", "Boy", "SeniorFemale", "SeniorMale")
- `lang`: Language code for multilingual voices (e.g., "es-ES", "fr-FR", "hi-IN")
### **LiteLLM SDK**
#### Custom Azure Voice
```python showLineNumbers title="Custom Azure Voice"
from litellm import speech
response = speech(
model="azure/speech/azure-tts",
voice="en-US-AndrewNeural", # Use Azure voice directly
input="Hello, this is a test",
api_base="https://eastus.tts.speech.microsoft.com",
api_key=os.environ["AZURE_TTS_API_KEY"],
response_format="mp3"
)
response.stream_to_file("speech.mp3")
```
#### Speaking Style
```python showLineNumbers title="Speaking Style"
from litellm import speech
response = speech(
model="azure/speech/azure-tts",
voice="en-US-JennyNeural", # Must be a voice that supports styles
input="Who are you? What is chicken dinner?",
api_base="https://eastus.tts.speech.microsoft.com",
api_key=os.environ["AZURE_TTS_API_KEY"],
style="whispering", # Azure-specific: cheerful, sad, angry, whispering, etc.
)
response.stream_to_file("speech.mp3")
```
#### Style with Degree and Role
```python showLineNumbers title="Style with Degree and Role"
from litellm import speech
response = speech(
model="azure/speech/azure-tts",
voice="en-US-AriaNeural",
input="Good morning! How are you today?",
api_base="https://eastus.tts.speech.microsoft.com",
api_key=os.environ["AZURE_TTS_API_KEY"],
style="cheerful", # Azure-specific: Speaking style
styledegree="2", # Azure-specific: 0.01 to 2 (intensity)
role="SeniorFemale", # Azure-specific: Girl, Boy, SeniorFemale, etc.
)
response.stream_to_file("speech.mp3")
```
#### Language Override for Multilingual Voices
```python showLineNumbers title="Language Override"
from litellm import speech
response = speech(
model="azure/speech/azure-tts",
voice="en-US-AvaMultilingualNeural", # Multilingual voice
input="आप कौन हैं? चिकन डिनर क्या है?", # Hindi text
api_base="https://eastus.tts.speech.microsoft.com",
api_key=os.environ["AZURE_TTS_API_KEY"],
lang="hi-IN", # Azure-specific: Override language
)
response.stream_to_file("speech.mp3")
```
### **LiteLLM AI Gateway (CURL)**
First, ensure you have set up your proxy config as shown in the [LiteLLM Proxy setup](#quick-start) above.
**Using the model name from your config:**
```yaml
model_list:
- model_name: azure-speech # This is what you'll use in your API calls
litellm_params:
model: azure/speech/azure-tts
api_base: https://eastus.tts.speech.microsoft.com
api_key: os.environ/AZURE_TTS_API_KEY
```
#### Custom Azure Voice
```bash
curl http://0.0.0.0:4000/v1/audio/speech \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"model": "azure-speech",
"voice": "en-US-AndrewNeural",
"input": "Hello, this is a test"
}' \
--output speech.mp3
```
#### Speaking Style
```bash
curl http://0.0.0.0:4000/v1/audio/speech \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"model": "azure-speech",
"input": "Who are you? What is chicken dinner?",
"voice": "en-US-JennyNeural",
"style": "whispering"
}' \
--output speech.mp3
```
#### Style with Degree and Role
```bash
curl http://0.0.0.0:4000/v1/audio/speech \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"model": "azure-speech",
"voice": "en-US-AriaNeural",
"input": "Good morning! How are you today?",
"style": "cheerful",
"styledegree": "2",
"role": "SeniorFemale"
}' \
--output speech.mp3
```
#### Language Override
```bash
curl http://0.0.0.0:4000/v1/audio/speech \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"model": "azure-speech",
"input": "आप कौन हैं? चिकन डिनर क्या है?",
"voice": "en-US-AvaMultilingualNeural",
"lang": "hi-IN"
}' \
--output speech.mp3
```
### Azure-Specific Parameters Reference
| Parameter | Description | Example Values | Notes |
|-----------|-------------|----------------|-------|
| `style` | Speaking style | `cheerful`, `sad`, `angry`, `excited`, `friendly`, `hopeful`, `shouting`, `terrified`, `unfriendly`, `whispering` | Only supported by certain voices. See [Azure voice styles documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#use-speaking-styles-and-roles) |
| `styledegree` | Style intensity | `0.01` to `2` | Higher values = more intense. Default is `1` |
| `role` | Voice role | `Girl`, `Boy`, `YoungAdultFemale`, `YoungAdultMale`, `OlderAdultFemale`, `OlderAdultMale`, `SeniorFemale`, `SeniorMale` | Only supported by certain voices |
| `lang` | Language code | `es-ES`, `fr-FR`, `de-DE`, `hi-IN`, etc. | For multilingual voices. Overrides the default language |
## Async Support
```python showLineNumbers title="Async Usage"
@@ -4,7 +4,7 @@ Azure AVA (Cognitive Services) Text-to-Speech transformation
Maps OpenAI TTS spec to Azure Cognitive Services TTS API
"""
from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Union
from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Tuple, Union
from urllib.parse import urlparse
import httpx
@@ -32,6 +32,7 @@ class AzureAVATextToSpeechConfig(BaseTextToSpeechConfig):
"""
# Azure endpoint domains
DEFAULT_VOICE = "en-US-AriaNeural"
COGNITIVE_SERVICES_DOMAIN = "api.cognitive.microsoft.com"
TTS_SPEECH_DOMAIN = "tts.speech.microsoft.com"
TTS_ENDPOINT_PATH = "/cognitiveservices/v1"
@@ -134,6 +135,9 @@ class AzureAVATextToSpeechConfig(BaseTextToSpeechConfig):
def get_supported_openai_params(self, model: str) -> list:
"""
Azure AVA TTS supports these OpenAI parameters
Note: Azure also supports additional SSML-specific parameters (style, styledegree, role)
which can be passed but are not part of the OpenAI spec
"""
return ["voice", "response_format", "speed"]
@@ -154,28 +158,93 @@ class AzureAVATextToSpeechConfig(BaseTextToSpeechConfig):
"""
rate_percentage = int((speed - 1.0) * 100)
return f"{rate_percentage:+d}%"
def _build_express_as_element(
self,
content: str,
style: Optional[str] = None,
styledegree: Optional[str] = None,
role: Optional[str] = None,
) -> str:
"""
Build mstts:express-as element with optional style, styledegree, and role attributes
Args:
content: The inner content to wrap
style: Speaking style (e.g., "cheerful", "sad", "angry")
styledegree: Style intensity (0.01 to 2)
role: Voice role (e.g., "Girl", "Boy", "SeniorFemale", "SeniorMale")
Returns:
Content wrapped in mstts:express-as if any attributes provided, otherwise raw content
"""
if not (style or styledegree or role):
return content
express_as_attrs = []
if style:
express_as_attrs.append(f"style='{style}'")
if styledegree:
express_as_attrs.append(f"styledegree='{styledegree}'")
if role:
express_as_attrs.append(f"role='{role}'")
express_as_attrs_str = " ".join(express_as_attrs)
return f"<mstts:express-as {express_as_attrs_str}>{content}</mstts:express-as>"
def _get_voice_language(
self,
voice_name: Optional[str],
explicit_lang: Optional[str] = None,
) -> Optional[str]:
"""
Get the language for the voice element's xml:lang attribute
Args:
voice_name: The Azure voice name (e.g., "en-US-AriaNeural")
explicit_lang: Explicitly provided language code (takes precedence)
Returns:
Language code if available (e.g., "es-ES"), or None
Examples:
- explicit_lang="es-ES""es-ES" (explicit takes precedence)
- voice_name="en-US-AriaNeural", explicit_lang=None → None (use default from voice)
- voice_name="en-US-AvaMultilingualNeural", explicit_lang="fr-FR""fr-FR"
"""
# If explicit language is provided, use it (for multilingual voices)
if explicit_lang:
return explicit_lang
# For non-multilingual voices, we don't need to set xml:lang on the voice element
# The voice name already encodes the language (e.g., en-US-AriaNeural)
# Only return a language if explicitly set
return None
def map_openai_params(
self,
model: str,
optional_params: Dict,
drop_params: bool,
) -> Dict:
voice: Optional[Union[str, Dict]] = None,
drop_params: bool = False,
kwargs: Dict = {},
) -> Tuple[Optional[str], Dict]:
"""
Map OpenAI parameters to Azure AVA TTS parameters
"""
mapped_params = {}
##########################################################
# Map voice
if "voice" in optional_params:
voice = optional_params["voice"]
# If it's already an Azure voice, use it directly
if isinstance(voice, str):
if voice in self.VOICE_MAPPINGS:
mapped_params["voice"] = self.VOICE_MAPPINGS[voice]
else:
# Assume it's already an Azure voice name
mapped_params["voice"] = voice
# OpenAI uses voice as a required param, hence not in optional_params
##########################################################
# If it's already an Azure voice, use it directly
mapped_voice: Optional[str] = None
if isinstance(voice, str):
if voice in self.VOICE_MAPPINGS:
mapped_voice = self.VOICE_MAPPINGS[voice]
else:
# Assume it's already an Azure voice name
mapped_voice = voice
# Map response format
if "response_format" in optional_params:
@@ -195,7 +264,19 @@ class AzureAVATextToSpeechConfig(BaseTextToSpeechConfig):
if speed is not None:
mapped_params["rate"] = self._convert_speed_to_azure_rate(speed=speed)
return mapped_params
# Pass through Azure-specific SSML parameters
if "style" in kwargs:
mapped_params["style"] = kwargs["style"]
if "styledegree" in kwargs:
mapped_params["styledegree"] = kwargs["styledegree"]
if "role" in kwargs:
mapped_params["role"] = kwargs["role"]
if "lang" in kwargs:
mapped_params["lang"] = kwargs["lang"]
return mapped_voice, mapped_params
def validate_environment(
self,
@@ -315,11 +396,17 @@ class AzureAVATextToSpeechConfig(BaseTextToSpeechConfig):
Note: optional_params should already be mapped via map_openai_params in main.py
Supports Azure-specific SSML features:
- style: Speaking style (e.g., "cheerful", "sad", "angry")
- styledegree: Style intensity (0.01 to 2)
- role: Voice role (e.g., "Girl", "Boy", "SeniorFemale", "SeniorMale")
- lang: Language code for multilingual voices (e.g., "es-ES", "fr-FR")
Returns:
TextToSpeechRequestData: Contains SSML body and Azure-specific headers
"""
# Get voice (already mapped in main.py, or use default)
azure_voice = optional_params.get("voice", "en-US-AriaNeural")
azure_voice = voice or self.DEFAULT_VOICE
# Get output format (already mapped in main.py)
output_format = optional_params.get(
@@ -329,6 +416,10 @@ class AzureAVATextToSpeechConfig(BaseTextToSpeechConfig):
# Build SSML
rate = optional_params.get("rate", "0%")
style = optional_params.get("style")
styledegree = optional_params.get("styledegree")
role = optional_params.get("role")
lang = optional_params.get("lang")
# Escape XML special characters in input text
escaped_input = (
@@ -339,15 +430,38 @@ class AzureAVATextToSpeechConfig(BaseTextToSpeechConfig):
.replace("'", "&apos;")
)
ssml_body = f"""
<speak version='1.0' xml:lang='en-US'>
<voice name='{azure_voice}'>
<prosody rate='{rate}'>
{escaped_input}
</prosody>
</voice>
</speak>
"""
# Determine if we need mstts namespace (for express-as element)
use_mstts = style or role or styledegree
# Build the xmlns attributes
if use_mstts:
xmlns = "xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts'"
else:
xmlns = "xmlns='http://www.w3.org/2001/10/synthesis'"
# Build the inner content with prosody
prosody_content = f"<prosody rate='{rate}'>{escaped_input}</prosody>"
# Wrap in mstts:express-as if style or role is specified
voice_content = self._build_express_as_element(
content=prosody_content,
style=style,
styledegree=styledegree,
role=role,
)
# Build voice element with optional xml:lang attribute
voice_lang = self._get_voice_language(
voice_name=azure_voice,
explicit_lang=lang,
)
voice_lang_attr = f" xml:lang='{voice_lang}'" if voice_lang else ""
ssml_body = f"""<speak version='1.0' {xmlns} xml:lang='en-US'>
<voice name='{azure_voice}'{voice_lang_attr}>
{voice_content}
</voice>
</speak>"""
return {
"ssml_body": ssml_body,
@@ -1,6 +1,6 @@
import types
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, Optional, TypedDict
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, TypedDict, Union
import httpx
@@ -68,8 +68,10 @@ class BaseTextToSpeechConfig(ABC):
self,
model: str,
optional_params: Dict,
drop_params: bool,
) -> Dict:
voice: Optional[Union[str, Dict]] = None,
drop_params: bool = False,
kwargs: Dict = {},
) -> Tuple[Optional[str], Dict]:
"""
Map OpenAI TTS parameters to provider-specific parameters
"""
+4 -2
View File
@@ -5706,17 +5706,19 @@ def speech( # noqa: PLR0915
# Map OpenAI params to provider-specific params if config exists
if text_to_speech_provider_config is not None:
optional_params = text_to_speech_provider_config.map_openai_params(
voice, optional_params = text_to_speech_provider_config.map_openai_params(
model=model,
optional_params=optional_params,
voice=voice,
drop_params=False,
kwargs=kwargs,
)
logging_obj: Logging = cast(Logging, kwargs.get("litellm_logging_obj"))
logging_obj.update_environment_variables(
model=model,
user=user,
optional_params={},
optional_params=optional_params,
litellm_params={
"litellm_call_id": litellm_call_id,
"proxy_server_request": proxy_server_request,
+88 -1
View File
@@ -376,6 +376,93 @@ async def test_azure_ava_tts_async():
# assert response cost is greater than 0
print("Response cost: ", response._hidden_params["response_cost"])
assert response._hidden_params["response_cost"] > 0
except Exception as e:
pytest.fail(f"Test failed with exception: {str(e)}")
@pytest.mark.asyncio
async def test_azure_ava_tts_with_custom_voice():
"""
Test that when using a custom Azure voice (en-US-AndrewNeural),
the SSML request body contains the selected voice.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
# Mock response
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="azure/speech/azure-tts",
voice="en-US-AndrewNeural",
input="Hello, this is a test",
api_base="https://eastus.tts.speech.microsoft.com",
api_key="fake-key",
response_format="mp3",
)
# Verify the mock was called
assert mock_post.called
# Get the call arguments
call_args = mock_post.call_args
ssml_body = call_args.kwargs.get("data")
# Verify the SSML contains the custom voice
assert ssml_body is not None
assert "en-US-AndrewNeural" in ssml_body
assert "Hello, this is a test" in ssml_body
assert "<speak" in ssml_body
assert "<voice" in ssml_body
@pytest.mark.asyncio
async def test_azure_ava_tts_fable_voice_mapping():
"""
Test that when using OpenAI voice 'fable',
it gets mapped to Azure voice 'en-GB-RyanNeural' in the SSML.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
# Mock response
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="azure/speech/azure-tts",
voice="fable",
input="Testing voice mapping",
api_base="https://eastus.tts.speech.microsoft.com",
api_key="fake-key",
response_format="mp3",
)
# Verify the mock was called
assert mock_post.called
# Get the call arguments
call_args = mock_post.call_args
ssml_body = call_args.kwargs.get("data")
# Verify the SSML contains the mapped voice (en-GB-RyanNeural, not 'fable')
assert ssml_body is not None
assert "en-GB-RyanNeural" in ssml_body
assert "fable" not in ssml_body.lower()
assert "Testing voice mapping" in ssml_body
assert "<speak" in ssml_body
assert "<voice" in ssml_body
@@ -19,30 +19,32 @@ def test_map_openai_params_voice_mapping(azure_tts_config: AzureAVATextToSpeechC
"""
Test mapping OpenAI voice to Azure AVA voice
"""
optional_params = {"voice": "alloy"}
optional_params = {}
mapped = azure_tts_config.map_openai_params(
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
voice="alloy",
drop_params=False
)
assert mapped["voice"] == "en-US-JennyNeural"
assert mapped_voice == "en-US-JennyNeural"
def test_map_openai_params_custom_azure_voice(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test using custom Azure voice directly
"""
optional_params = {"voice": "en-GB-RyanNeural"}
optional_params = {}
mapped = azure_tts_config.map_openai_params(
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
voice="en-GB-RyanNeural",
drop_params=False
)
assert mapped["voice"] == "en-GB-RyanNeural"
assert mapped_voice == "en-GB-RyanNeural"
def test_map_openai_params_response_format(azure_tts_config: AzureAVATextToSpeechConfig):
@@ -51,13 +53,13 @@ def test_map_openai_params_response_format(azure_tts_config: AzureAVATextToSpeec
"""
optional_params = {"response_format": "mp3"}
mapped = azure_tts_config.map_openai_params(
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
drop_params=False
)
assert mapped["output_format"] == "audio-24khz-48kbitrate-mono-mp3"
assert mapped_params["output_format"] == "audio-24khz-48kbitrate-mono-mp3"
def test_map_openai_params_default_format(azure_tts_config: AzureAVATextToSpeechConfig):
@@ -66,13 +68,13 @@ def test_map_openai_params_default_format(azure_tts_config: AzureAVATextToSpeech
"""
optional_params = {}
mapped = azure_tts_config.map_openai_params(
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
drop_params=False
)
assert mapped["output_format"] == "audio-24khz-48kbitrate-mono-mp3"
assert mapped_params["output_format"] == "audio-24khz-48kbitrate-mono-mp3"
def test_map_openai_params_speed(azure_tts_config: AzureAVATextToSpeechConfig):
@@ -81,14 +83,14 @@ def test_map_openai_params_speed(azure_tts_config: AzureAVATextToSpeechConfig):
"""
optional_params = {"speed": 1.5}
mapped = azure_tts_config.map_openai_params(
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
drop_params=False
)
# Speed 1.5 should map to +50%
assert mapped["rate"] == "+50%"
assert mapped_params["rate"] == "+50%"
def test_map_openai_params_slow_speed(azure_tts_config: AzureAVATextToSpeechConfig):
@@ -97,14 +99,14 @@ def test_map_openai_params_slow_speed(azure_tts_config: AzureAVATextToSpeechConf
"""
optional_params = {"speed": 0.5}
mapped = azure_tts_config.map_openai_params(
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
drop_params=False
)
# Speed 0.5 should map to -50%
assert mapped["rate"] == "-50%"
assert mapped_params["rate"] == "-50%"
# Tests for get_complete_url
@@ -282,3 +284,270 @@ def test_transform_text_to_speech_response(azure_tts_config: AzureAVATextToSpeec
from litellm.types.llms.openai import HttpxBinaryResponseContent
assert isinstance(result, HttpxBinaryResponseContent)
# Tests for helper methods
def test_build_express_as_element_with_style(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test _build_express_as_element helper with style only
"""
result = azure_tts_config._build_express_as_element(
content="<prosody rate='+0%'>Test</prosody>",
style="cheerful"
)
assert result == "<mstts:express-as style='cheerful'><prosody rate='+0%'>Test</prosody></mstts:express-as>"
def test_build_express_as_element_with_all_attrs(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test _build_express_as_element helper with all attributes
"""
result = azure_tts_config._build_express_as_element(
content="<prosody rate='+0%'>Test</prosody>",
style="cheerful",
styledegree="2",
role="SeniorFemale"
)
assert "<mstts:express-as" in result
assert "style='cheerful'" in result
assert "styledegree='2'" in result
assert "role='SeniorFemale'" in result
assert "<prosody rate='+0%'>Test</prosody>" in result
assert "</mstts:express-as>" in result
def test_build_express_as_element_no_attrs(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test _build_express_as_element helper returns content unchanged when no attrs
"""
content = "<prosody rate='+0%'>Test</prosody>"
result = azure_tts_config._build_express_as_element(content=content)
assert result == content
assert "<mstts:express-as" not in result
def test_get_voice_language_with_explicit_lang(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test _get_voice_language returns explicit language when provided
"""
result = azure_tts_config._get_voice_language(
voice_name="en-US-AvaMultilingualNeural",
explicit_lang="es-ES"
)
assert result == "es-ES"
def test_get_voice_language_without_explicit_lang(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test _get_voice_language returns None when no explicit language provided
"""
result = azure_tts_config._get_voice_language(
voice_name="en-US-AriaNeural",
explicit_lang=None
)
assert result is None
def test_get_voice_language_explicit_takes_precedence(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test that explicit language takes precedence over voice name
"""
result = azure_tts_config._get_voice_language(
voice_name="en-US-AvaMultilingualNeural",
explicit_lang="fr-FR"
)
assert result == "fr-FR"
# Tests for Azure-specific SSML features
def test_map_openai_params_style(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test passing through Azure style parameter
"""
optional_params = {"style": "cheerful"}
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
drop_params=False
)
assert mapped_params["style"] == "cheerful"
def test_map_openai_params_style_and_role(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test passing through Azure style, styledegree, and role parameters
"""
optional_params = {
"style": "cheerful",
"styledegree": "2",
"role": "SeniorFemale"
}
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
drop_params=False
)
assert mapped_params["style"] == "cheerful"
assert mapped_params["styledegree"] == "2"
assert mapped_params["role"] == "SeniorFemale"
def test_map_openai_params_lang(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test passing through Azure lang parameter for multilingual voices
"""
optional_params = {"lang": "es-ES"}
mapped_voice, mapped_params = azure_tts_config.map_openai_params(
model="azure-tts",
optional_params=optional_params,
drop_params=False
)
assert mapped_params["lang"] == "es-ES"
def test_transform_text_to_speech_request_with_style(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test SSML generation with style parameter includes mstts:express-as element
"""
result = azure_tts_config.transform_text_to_speech_request(
model="azure-tts",
input="Hello world",
voice="en-US-AriaNeural",
optional_params={
"voice": "en-US-AriaNeural",
"style": "cheerful"
},
litellm_params={},
headers={}
)
ssml = result["ssml_body"]
# Should include mstts namespace
assert "xmlns:mstts='https://www.w3.org/2001/mstts'" in ssml
# Should include mstts:express-as with style
assert "<mstts:express-as style='cheerful'>" in ssml
assert "</mstts:express-as>" in ssml
# Should still include the content
assert "Hello world" in ssml
assert "en-US-AriaNeural" in ssml
def test_transform_text_to_speech_request_with_style_degree_role(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test SSML generation with style, styledegree, and role parameters
"""
result = azure_tts_config.transform_text_to_speech_request(
model="azure-tts",
input="Test message",
voice="en-US-AriaNeural",
optional_params={
"voice": "en-US-AriaNeural",
"style": "cheerful",
"styledegree": "2",
"role": "SeniorFemale"
},
litellm_params={},
headers={}
)
ssml = result["ssml_body"]
# Should include mstts namespace
assert "xmlns:mstts='https://www.w3.org/2001/mstts'" in ssml
# Should include mstts:express-as with all attributes
assert "<mstts:express-as" in ssml
assert "style='cheerful'" in ssml
assert "styledegree='2'" in ssml
assert "role='SeniorFemale'" in ssml
assert "</mstts:express-as>" in ssml
def test_transform_text_to_speech_request_without_style(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test that SSML without style does not include mstts namespace or express-as
"""
result = azure_tts_config.transform_text_to_speech_request(
model="azure-tts",
input="Hello world",
voice="en-US-AriaNeural",
optional_params={"voice": "en-US-AriaNeural"},
litellm_params={},
headers={}
)
ssml = result["ssml_body"]
# Should NOT include mstts namespace
assert "xmlns:mstts" not in ssml
# Should NOT include mstts:express-as
assert "<mstts:express-as" not in ssml
# Should still include basic SSML structure
assert "<speak" in ssml
assert "<voice" in ssml
assert "en-US-AriaNeural" in ssml
assert "Hello world" in ssml
def test_transform_text_to_speech_request_with_lang(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test SSML generation with lang parameter for multilingual voices
"""
result = azure_tts_config.transform_text_to_speech_request(
model="azure-tts",
input="Hola mundo",
voice="en-US-AvaMultilingualNeural",
optional_params={
"voice": "en-US-AvaMultilingualNeural",
"lang": "es-ES"
},
litellm_params={},
headers={}
)
ssml = result["ssml_body"]
# Should include xml:lang on voice element
assert "xml:lang='es-ES'" in ssml
# Should still include the content and voice
assert "Hola mundo" in ssml
assert "en-US-AvaMultilingualNeural" in ssml
def test_transform_text_to_speech_request_without_lang(azure_tts_config: AzureAVATextToSpeechConfig):
"""
Test that SSML without lang parameter does not include xml:lang on voice element
"""
result = azure_tts_config.transform_text_to_speech_request(
model="azure-tts",
input="Hello world",
voice="en-US-AriaNeural",
optional_params={"voice": "en-US-AriaNeural"},
litellm_params={},
headers={}
)
ssml = result["ssml_body"]
# Voice element should not have xml:lang attribute (only the speak element should)
# Check that voice element doesn't have xml:lang by ensuring the pattern doesn't exist
assert "<voice name='en-US-AriaNeural' xml:lang=" not in ssml
assert "<voice name='en-US-AriaNeural'>" in ssml