372 lines
12 KiB
Python
372 lines
12 KiB
Python
"""
|
|
Tests for MiniMax Text-to-Speech integration
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(
|
|
0, os.path.abspath("../..")
|
|
) # Adds the parent directory to the system path
|
|
|
|
import litellm
|
|
from litellm import speech
|
|
from litellm.llms.minimax.text_to_speech.transformation import (
|
|
MinimaxTextToSpeechConfig,
|
|
)
|
|
|
|
|
|
class TestMinimaxTextToSpeechConfig:
|
|
"""Test MiniMax TTS configuration and parameter mapping"""
|
|
|
|
def test_get_supported_openai_params(self):
|
|
"""Test that supported OpenAI params are correctly defined"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
supported_params = config.get_supported_openai_params("speech-2.6-hd")
|
|
|
|
assert "voice" in supported_params
|
|
assert "response_format" in supported_params
|
|
assert "speed" in supported_params
|
|
|
|
def test_voice_mapping(self):
|
|
"""Test OpenAI voice to MiniMax voice_id mapping"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
# Test OpenAI voice mappings
|
|
assert config._extract_voice_id("alloy") == "male-qn-qingse"
|
|
assert config._extract_voice_id("echo") == "male-qn-jingying"
|
|
assert config._extract_voice_id("nova") == "female-yujie"
|
|
|
|
# Test custom voice passthrough
|
|
assert config._extract_voice_id("custom-voice-id") == "custom-voice-id"
|
|
|
|
def test_format_mapping(self):
|
|
"""Test response format mapping"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
assert config.FORMAT_MAPPINGS["mp3"] == "mp3"
|
|
assert config.FORMAT_MAPPINGS["pcm"] == "pcm"
|
|
assert config.FORMAT_MAPPINGS["wav"] == "wav"
|
|
assert config.FORMAT_MAPPINGS["flac"] == "flac"
|
|
|
|
def test_map_openai_params_basic(self):
|
|
"""Test basic parameter mapping from OpenAI to MiniMax format"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
optional_params = {
|
|
"response_format": "mp3",
|
|
"speed": 1.5,
|
|
}
|
|
|
|
voice, mapped_params = config.map_openai_params(
|
|
model="speech-2.6-hd",
|
|
optional_params=optional_params,
|
|
voice="alloy",
|
|
)
|
|
|
|
assert voice == "male-qn-qingse"
|
|
assert mapped_params["format"] == "mp3"
|
|
assert mapped_params["speed"] == 1.5
|
|
assert mapped_params["voice_id"] == "male-qn-qingse"
|
|
|
|
def test_map_openai_params_speed_clamping(self):
|
|
"""Test that speed is clamped to MiniMax's supported range"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
# Test speed too high
|
|
optional_params = {"speed": 5.0}
|
|
_, mapped_params = config.map_openai_params(
|
|
model="speech-2.6-hd",
|
|
optional_params=optional_params,
|
|
voice="alloy",
|
|
)
|
|
assert mapped_params["speed"] == 2.0 # Clamped to max
|
|
|
|
# Test speed too low
|
|
optional_params = {"speed": 0.1}
|
|
_, mapped_params = config.map_openai_params(
|
|
model="speech-2.6-hd",
|
|
optional_params=optional_params,
|
|
voice="alloy",
|
|
)
|
|
assert mapped_params["speed"] == 0.5 # Clamped to min
|
|
|
|
def test_map_openai_params_with_extra_body(self):
|
|
"""Test that extra_body parameters are passed through"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
optional_params = {
|
|
"extra_body": {
|
|
"vol": 1.5,
|
|
"pitch": 2,
|
|
"sample_rate": 24000,
|
|
}
|
|
}
|
|
|
|
_, mapped_params = config.map_openai_params(
|
|
model="speech-2.6-hd",
|
|
optional_params=optional_params,
|
|
voice="alloy",
|
|
)
|
|
|
|
assert mapped_params["vol"] == 1.5
|
|
assert mapped_params["pitch"] == 2
|
|
assert mapped_params["sample_rate"] == 24000
|
|
|
|
def test_validate_environment_with_api_key(self):
|
|
"""Test environment validation with API key"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
headers = {}
|
|
|
|
result_headers = config.validate_environment(
|
|
headers=headers,
|
|
model="speech-2.6-hd",
|
|
api_key="test-api-key",
|
|
)
|
|
|
|
assert "Authorization" in result_headers
|
|
assert result_headers["Authorization"] == "Bearer test-api-key"
|
|
assert result_headers["Content-Type"] == "application/json"
|
|
|
|
def test_validate_environment_missing_api_key(self):
|
|
"""Test that validation fails without API key"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
headers = {}
|
|
|
|
# Mock both litellm.api_key and get_secret_str to return None
|
|
import litellm
|
|
from unittest.mock import patch
|
|
|
|
original_api_key = litellm.api_key
|
|
try:
|
|
litellm.api_key = None
|
|
with patch("litellm.llms.minimax.text_to_speech.transformation.get_secret_str", return_value=None):
|
|
with pytest.raises(ValueError, match="MiniMax API key is required"):
|
|
config.validate_environment(
|
|
headers=headers,
|
|
model="speech-2.6-hd",
|
|
api_key=None,
|
|
)
|
|
finally:
|
|
litellm.api_key = original_api_key
|
|
|
|
def test_transform_text_to_speech_request(self):
|
|
"""Test request transformation to MiniMax format"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
optional_params = {
|
|
"voice_id": "male-qn-qingse",
|
|
"speed": 1.2,
|
|
"format": "mp3",
|
|
"vol": 1.0,
|
|
"pitch": 0,
|
|
"sample_rate": 32000,
|
|
"bitrate": 128000,
|
|
"channel": 1,
|
|
}
|
|
|
|
result = config.transform_text_to_speech_request(
|
|
model="speech-2.6-hd",
|
|
input="Hello, world!",
|
|
voice="male-qn-qingse",
|
|
optional_params=optional_params,
|
|
litellm_params={},
|
|
headers={},
|
|
)
|
|
|
|
assert "dict_body" in result
|
|
body = result["dict_body"]
|
|
|
|
assert body["model"] == "speech-2.6-hd"
|
|
assert body["text"] == "Hello, world!"
|
|
assert body["stream"] is False
|
|
assert body["voice_setting"]["voice_id"] == "male-qn-qingse"
|
|
assert body["voice_setting"]["speed"] == 1.2
|
|
assert body["audio_setting"]["format"] == "mp3"
|
|
assert body["audio_setting"]["sample_rate"] == 32000
|
|
|
|
def test_get_complete_url(self):
|
|
"""Test URL construction"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
url = config.get_complete_url(
|
|
model="speech-2.6-hd",
|
|
api_base=None,
|
|
litellm_params={},
|
|
)
|
|
|
|
assert url == "https://api.minimax.io/v1/t2a_v2"
|
|
|
|
def test_get_complete_url_custom_base(self):
|
|
"""Test URL construction with custom API base"""
|
|
config = MinimaxTextToSpeechConfig()
|
|
|
|
url = config.get_complete_url(
|
|
model="speech-2.6-hd",
|
|
api_base="https://custom.api.com",
|
|
litellm_params={},
|
|
)
|
|
|
|
assert url == "https://custom.api.com/v1/t2a_v2"
|
|
|
|
|
|
class TestMinimaxSpeechIntegration:
|
|
"""Integration tests for MiniMax TTS via litellm.speech()"""
|
|
|
|
@pytest.mark.skip(reason="Requires MiniMax API key")
|
|
def test_speech_basic(self):
|
|
"""Test basic speech synthesis call"""
|
|
# This test requires a real API key
|
|
os.environ["MINIMAX_API_KEY"] = "your-api-key-here"
|
|
|
|
speech_file_path = Path(__file__).parent / "test_minimax_speech.mp3"
|
|
|
|
response = speech(
|
|
model="minimax/speech-2.6-hd",
|
|
voice="alloy",
|
|
input="Hello, this is a test of MiniMax text to speech.",
|
|
)
|
|
|
|
response.stream_to_file(speech_file_path)
|
|
|
|
# Verify file was created
|
|
assert speech_file_path.exists()
|
|
assert speech_file_path.stat().st_size > 0
|
|
|
|
# Clean up
|
|
speech_file_path.unlink()
|
|
|
|
@pytest.mark.skip(reason="Requires MiniMax API key")
|
|
def test_speech_with_custom_params(self):
|
|
"""Test speech synthesis with custom parameters"""
|
|
os.environ["MINIMAX_API_KEY"] = "your-api-key-here"
|
|
|
|
speech_file_path = Path(__file__).parent / "test_minimax_speech_custom.mp3"
|
|
|
|
response = speech(
|
|
model="minimax/speech-2.6-turbo",
|
|
voice="nova",
|
|
input="Testing custom parameters.",
|
|
speed=1.5,
|
|
response_format="mp3",
|
|
extra_body={
|
|
"vol": 1.2,
|
|
"pitch": 1,
|
|
"sample_rate": 24000,
|
|
},
|
|
)
|
|
|
|
response.stream_to_file(speech_file_path)
|
|
|
|
# Verify file was created
|
|
assert speech_file_path.exists()
|
|
assert speech_file_path.stat().st_size > 0
|
|
|
|
# Clean up
|
|
speech_file_path.unlink()
|
|
|
|
def test_speech_mock_response(self):
|
|
"""Test speech synthesis with mocked response"""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Create mock audio data (hex-encoded as MiniMax returns)
|
|
mock_audio_bytes = b"fake audio data for testing"
|
|
mock_audio_hex = mock_audio_bytes.hex()
|
|
|
|
mock_response_json = {
|
|
"data": {
|
|
"audio": mock_audio_hex,
|
|
"status": 0,
|
|
"ced": ""
|
|
},
|
|
"extra_info": {},
|
|
}
|
|
|
|
with patch("litellm.llms.custom_httpx.llm_http_handler.BaseLLMHTTPHandler.text_to_speech_handler") as mock_tts:
|
|
# Create a mock httpx.Response
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.headers = {}
|
|
mock_response.json.return_value = mock_response_json
|
|
mock_response.content = mock_audio_bytes
|
|
|
|
# Mock the response wrapper
|
|
from litellm.types.llms.openai import HttpxBinaryResponseContent
|
|
mock_binary_response = HttpxBinaryResponseContent(mock_response)
|
|
mock_tts.return_value = mock_binary_response
|
|
|
|
# This would normally make a real API call
|
|
# but we're mocking it for testing
|
|
response = speech(
|
|
model="minimax/speech-2.6-hd",
|
|
voice="alloy",
|
|
input="Test input",
|
|
api_key="test-key",
|
|
)
|
|
|
|
# Verify the mock was called
|
|
assert mock_tts.called
|
|
|
|
|
|
class TestMinimaxProviderRegistration:
|
|
"""Test that MiniMax is properly registered as a provider"""
|
|
|
|
def test_minimax_in_llm_providers(self):
|
|
"""Test that MINIMAX is in LlmProviders enum"""
|
|
from litellm.types.utils import LlmProviders
|
|
|
|
assert hasattr(LlmProviders, "MINIMAX")
|
|
assert LlmProviders.MINIMAX.value == "minimax"
|
|
|
|
def test_minimax_in_provider_list(self):
|
|
"""Test that minimax is in the provider list"""
|
|
assert litellm.LlmProviders.MINIMAX in litellm.provider_list
|
|
|
|
def test_get_provider_text_to_speech_config(self):
|
|
"""Test that MiniMax TTS config can be retrieved"""
|
|
from litellm.utils import ProviderConfigManager
|
|
|
|
config = ProviderConfigManager.get_provider_text_to_speech_config(
|
|
model="speech-2.6-hd",
|
|
provider=litellm.LlmProviders.MINIMAX,
|
|
)
|
|
|
|
assert config is not None
|
|
assert isinstance(config, MinimaxTextToSpeechConfig)
|
|
|
|
def test_get_llm_provider_minimax(self):
|
|
"""Test that get_llm_provider correctly identifies MiniMax models"""
|
|
from litellm import get_llm_provider
|
|
|
|
model, provider, api_key, api_base = get_llm_provider(
|
|
model="minimax/speech-2.6-hd"
|
|
)
|
|
|
|
assert model == "speech-2.6-hd"
|
|
assert provider == "minimax"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run basic tests
|
|
test_config = TestMinimaxTextToSpeechConfig()
|
|
test_config.test_get_supported_openai_params()
|
|
test_config.test_voice_mapping()
|
|
test_config.test_format_mapping()
|
|
test_config.test_map_openai_params_basic()
|
|
test_config.test_map_openai_params_speed_clamping()
|
|
test_config.test_transform_text_to_speech_request()
|
|
test_config.test_get_complete_url()
|
|
|
|
test_registration = TestMinimaxProviderRegistration()
|
|
test_registration.test_minimax_in_llm_providers()
|
|
test_registration.test_minimax_in_provider_list()
|
|
test_registration.test_get_provider_text_to_speech_config()
|
|
test_registration.test_get_llm_provider_minimax()
|
|
|
|
print("All basic tests passed!")
|
|
|