|
|
"""Unit and integration tests for the MiniMax TTS provider."""
|
|
|
import os
|
|
|
import tempfile
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# Helpers – fake out the settings module so we can import without a config file
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
FAKE_SETTINGS = {
|
|
|
"settings": {
|
|
|
"tts": {
|
|
|
"minimax_api_key": "test-api-key",
|
|
|
"minimax_api_url": "https://api.minimax.io",
|
|
|
"minimax_voice_name": "English_Graceful_Lady",
|
|
|
"minimax_tts_model": "speech-2.8-hd",
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
def _patch_settings(config=None):
|
|
|
"""Return a patcher that replaces utils.settings.config."""
|
|
|
mock_settings = MagicMock()
|
|
|
mock_settings.config = config or FAKE_SETTINGS
|
|
|
return patch.dict("sys.modules", {"utils": MagicMock(settings=mock_settings)})
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# Unit tests
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
class TestMiniMaxTTSInit:
|
|
|
"""Provider instantiation and configuration parsing."""
|
|
|
|
|
|
def test_creates_instance_with_valid_config(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
assert tts is not None
|
|
|
|
|
|
def test_raises_when_api_key_missing(self):
|
|
|
config = {"settings": {"tts": {}}}
|
|
|
with _patch_settings(config), patch.dict(os.environ, {}, clear=False):
|
|
|
# Make sure env var is absent too
|
|
|
env = {k: v for k, v in os.environ.items() if k != "MINIMAX_API_KEY"}
|
|
|
with patch.dict(os.environ, env, clear=True):
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
with pytest.raises(ValueError, match="No MiniMax API key"):
|
|
|
MiniMaxTTS()
|
|
|
|
|
|
def test_reads_api_key_from_env(self):
|
|
|
config = {"settings": {"tts": {}}}
|
|
|
with _patch_settings(config), patch.dict(os.environ, {"MINIMAX_API_KEY": "env-key"}):
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
assert tts.api_key == "env-key"
|
|
|
|
|
|
def test_default_base_url(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
assert tts.base_url == "https://api.minimax.io"
|
|
|
|
|
|
def test_available_voices_not_empty(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
assert len(tts.available_voices) > 0
|
|
|
|
|
|
def test_max_chars(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
assert tts.max_chars == 4096
|
|
|
|
|
|
|
|
|
class TestMiniMaxTTSRandomVoice:
|
|
|
def test_randomvoice_returns_valid_voice(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS, MINIMAX_TTS_VOICES # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
voice = tts.randomvoice()
|
|
|
assert voice in MINIMAX_TTS_VOICES
|
|
|
|
|
|
|
|
|
class TestMiniMaxTTSRun:
|
|
|
"""Tests for the run() method using a mocked requests.post."""
|
|
|
|
|
|
def _make_mock_response(self, audio_hex="494433"):
|
|
|
mock_resp = MagicMock()
|
|
|
mock_resp.status_code = 200
|
|
|
mock_resp.json.return_value = {
|
|
|
"data": {"audio": audio_hex, "status": 2},
|
|
|
"base_resp": {"status_code": 0, "status_msg": "success"},
|
|
|
}
|
|
|
return mock_resp
|
|
|
|
|
|
def test_sends_request_to_correct_url(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
with patch("requests.post", return_value=self._make_mock_response()) as mock_post:
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
|
|
tts.run("Hello world.", f.name)
|
|
|
mock_post.assert_called_once()
|
|
|
call_url = mock_post.call_args[0][0]
|
|
|
assert "/v1/t2a_v2" in call_url
|
|
|
assert "api.minimax.io" in call_url
|
|
|
|
|
|
def test_sends_correct_payload(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
with patch("requests.post", return_value=self._make_mock_response()) as mock_post:
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
|
|
tts.run("Hello world.", f.name, random_voice=False)
|
|
|
payload = mock_post.call_args[1]["json"]
|
|
|
assert payload["model"] == "speech-2.8-hd"
|
|
|
assert payload["text"] == "Hello world."
|
|
|
assert payload["voice_setting"]["voice_id"] == "English_Graceful_Lady"
|
|
|
assert payload["audio_setting"]["format"] == "mp3"
|
|
|
|
|
|
def test_writes_audio_bytes_to_file(self):
|
|
|
audio_hex = "494433" # hex for 'ID3' — valid-ish mp3 header start
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
with patch("requests.post", return_value=self._make_mock_response(audio_hex)):
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
|
|
tmp_path = f.name
|
|
|
tts.run("Hello.", tmp_path)
|
|
|
with open(tmp_path, "rb") as f:
|
|
|
content = f.read()
|
|
|
assert content == bytes.fromhex(audio_hex)
|
|
|
|
|
|
def test_raises_on_http_error(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
mock_resp = MagicMock()
|
|
|
mock_resp.status_code = 401
|
|
|
mock_resp.text = "Unauthorized"
|
|
|
with patch("requests.post", return_value=mock_resp):
|
|
|
with pytest.raises(RuntimeError, match="MiniMax TTS API error: 401"):
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3") as f:
|
|
|
tts.run("Hello.", f.name)
|
|
|
|
|
|
def test_raises_on_api_status_error(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
mock_resp = MagicMock()
|
|
|
mock_resp.status_code = 200
|
|
|
mock_resp.json.return_value = {
|
|
|
"data": {},
|
|
|
"base_resp": {"status_code": 2013, "status_msg": "invalid voice_id"},
|
|
|
}
|
|
|
with patch("requests.post", return_value=mock_resp):
|
|
|
with pytest.raises(RuntimeError, match="invalid voice_id"):
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3") as f:
|
|
|
tts.run("Hello.", f.name)
|
|
|
|
|
|
def test_uses_random_voice_when_requested(self):
|
|
|
with _patch_settings():
|
|
|
from TTS.minimax_tts import MiniMaxTTS, MINIMAX_TTS_VOICES # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
captured_payloads = []
|
|
|
|
|
|
def fake_post(url, **kwargs):
|
|
|
captured_payloads.append(kwargs.get("json", {}))
|
|
|
return self._make_mock_response()
|
|
|
|
|
|
with patch("requests.post", side_effect=fake_post):
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
|
|
tts.run("Hello.", f.name, random_voice=True)
|
|
|
voice_used = captured_payloads[0]["voice_setting"]["voice_id"]
|
|
|
assert voice_used in MINIMAX_TTS_VOICES
|
|
|
|
|
|
|
|
|
class TestTTSProvidersRegistry:
|
|
|
"""Verify MiniMax is registered in voices.py."""
|
|
|
|
|
|
def test_minimax_in_providers_source(self):
|
|
|
"""Verify voices.py source contains MiniMax registration."""
|
|
|
import pathlib
|
|
|
|
|
|
voices_path = pathlib.Path(__file__).parent.parent / "video_creation" / "voices.py"
|
|
|
source = voices_path.read_text()
|
|
|
assert "MiniMax" in source, "MiniMax not found in TTSProviders registry"
|
|
|
assert "MiniMaxTTS" in source, "MiniMaxTTS class not imported in voices.py"
|
|
|
assert "minimax_tts" in source, "minimax_tts module not imported in voices.py"
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# Integration test – calls the real MiniMax API (skipped if no key set)
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY")
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not MINIMAX_API_KEY, reason="MINIMAX_API_KEY not set")
|
|
|
class TestMiniMaxTTSIntegration:
|
|
|
"""Live API calls — only run when MINIMAX_API_KEY is available."""
|
|
|
|
|
|
def test_synthesizes_speech_to_file(self):
|
|
|
config = {
|
|
|
"settings": {
|
|
|
"tts": {
|
|
|
"minimax_api_key": MINIMAX_API_KEY,
|
|
|
"minimax_api_url": "https://api.minimax.io",
|
|
|
"minimax_voice_name": "English_Graceful_Lady",
|
|
|
"minimax_tts_model": "speech-2.8-hd",
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
with _patch_settings(config):
|
|
|
from TTS.minimax_tts import MiniMaxTTS # noqa: PLC0415
|
|
|
|
|
|
tts = MiniMaxTTS()
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
|
|
tmp_path = f.name
|
|
|
tts.run("Hello, this is a MiniMax TTS test.", tmp_path)
|
|
|
size = os.path.getsize(tmp_path)
|
|
|
assert size > 100, f"Audio file too small ({size} bytes), likely empty or error"
|
|
|
os.unlink(tmp_path)
|