""" 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!")