From c11d38e1ac2bdb57e97f8347798ab661a0ad9b56 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sat, 21 Mar 2026 00:24:35 +0800 Subject: [PATCH] feat: add MiniMax Cloud TTS as a built-in engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MiniMax as a cloud-based TTS engine alongside existing local engines (Qwen, LuxTTS, Chatterbox, TADA, Kokoro). MiniMax requires only an API key (MINIMAX_API_KEY) — no model downloads needed. Backend: - New MiniMaxTTSBackend in backend/backends/minimax_backend.py - Calls MiniMax TTS API (POST https://api.minimax.io/v1/t2a_v2) - Default model: speech-2.8-hd, 24kHz PCM output - 12 preset voice IDs (English_Graceful_Lady, Deep_Voice_Man, etc.) - Registered in TTS_ENGINES and backend factory - Preset voice API endpoint returns MiniMax voices Frontend: - Added to engine selector dropdown, profile form, and type definitions - Language support for 10 languages - Listed as preset-only engine (no voice cloning) Tests: - 16 unit tests covering backend lifecycle, voice prompts, API mocking, payload verification, error handling, and engine registration - All 38 tests pass (22 existing + 16 new) Co-Authored-By: Octopus --- .../Generation/EngineModelSelector.tsx | 2 + .../components/VoiceProfiles/ProfileForm.tsx | 4 +- app/src/lib/api/types.ts | 2 +- app/src/lib/constants/languages.ts | 1 + app/src/lib/hooks/useGenerationForm.ts | 14 +- backend/backends/__init__.py | 5 + backend/backends/minimax_backend.py | 210 ++++++++++++++++ backend/models.py | 2 +- backend/routes/profiles.py | 15 ++ backend/tests/test_minimax_backend.py | 230 ++++++++++++++++++ 10 files changed, 477 insertions(+), 8 deletions(-) create mode 100644 backend/backends/minimax_backend.py create mode 100644 backend/tests/test_minimax_backend.py diff --git a/app/src/components/Generation/EngineModelSelector.tsx b/app/src/components/Generation/EngineModelSelector.tsx index 80fd82af..04b4b905 100644 --- a/app/src/components/Generation/EngineModelSelector.tsx +++ b/app/src/components/Generation/EngineModelSelector.tsx @@ -25,6 +25,7 @@ const ENGINE_OPTIONS = [ { value: 'tada:1B', label: 'TADA 1B', engine: 'tada' }, { value: 'tada:3B', label: 'TADA 3B Multilingual', engine: 'tada' }, { value: 'kokoro', label: 'Kokoro 82M', engine: 'kokoro' }, + { value: 'minimax', label: 'MiniMax Cloud TTS', engine: 'minimax' }, ] as const; const ENGINE_DESCRIPTIONS: Record = { @@ -34,6 +35,7 @@ const ENGINE_DESCRIPTIONS: Record = { chatterbox_turbo: 'English, [laugh] [cough] tags', tada: 'HumeAI, 700s+ coherent audio', kokoro: '82M params, CPU realtime, 8 langs', + minimax: 'Cloud TTS, no download needed', }; /** Engines that only support English and should force language to 'en' on select. */ diff --git a/app/src/components/VoiceProfiles/ProfileForm.tsx b/app/src/components/VoiceProfiles/ProfileForm.tsx index 0c4987b5..96eec99c 100644 --- a/app/src/components/VoiceProfiles/ProfileForm.tsx +++ b/app/src/components/VoiceProfiles/ProfileForm.tsx @@ -60,7 +60,7 @@ import { AudioSampleUpload } from './AudioSampleUpload'; import { SampleList } from './SampleList'; const MAX_AUDIO_DURATION_SECONDS = 30; -const PRESET_ONLY_ENGINES = new Set(['kokoro']); +const PRESET_ONLY_ENGINES = new Set(['kokoro', 'minimax']); const DEFAULT_ENGINE_OPTIONS = [ { value: 'qwen', label: 'Qwen3-TTS' }, { value: 'luxtts', label: 'LuxTTS' }, @@ -68,6 +68,7 @@ const DEFAULT_ENGINE_OPTIONS = [ { value: 'chatterbox_turbo', label: 'Chatterbox Turbo' }, { value: 'tada', label: 'TADA' }, { value: 'kokoro', label: 'Kokoro 82M' }, + { value: 'minimax', label: 'MiniMax Cloud TTS' }, ] as const; const baseProfileSchema = z.object({ @@ -849,6 +850,7 @@ export function ProfileForm() { Kokoro 82M + MiniMax Cloud TTS diff --git a/app/src/lib/api/types.ts b/app/src/lib/api/types.ts index 34e8038c..ab729d87 100644 --- a/app/src/lib/api/types.ts +++ b/app/src/lib/api/types.ts @@ -62,7 +62,7 @@ export interface GenerationRequest { language: LanguageCode; seed?: number; model_size?: '1.7B' | '0.6B' | '1B' | '3B'; - engine?: 'qwen' | 'luxtts' | 'chatterbox' | 'chatterbox_turbo' | 'tada' | 'kokoro'; + engine?: 'qwen' | 'luxtts' | 'chatterbox' | 'chatterbox_turbo' | 'tada' | 'kokoro' | 'minimax'; instruct?: string; max_chunk_chars?: number; crossfade_ms?: number; diff --git a/app/src/lib/constants/languages.ts b/app/src/lib/constants/languages.ts index 1a5c5f26..1c99bdcb 100644 --- a/app/src/lib/constants/languages.ts +++ b/app/src/lib/constants/languages.ts @@ -69,6 +69,7 @@ export const ENGINE_LANGUAGES: Record = { chatterbox_turbo: ['en'], tada: ['en', 'ar', 'zh', 'de', 'es', 'fr', 'it', 'ja', 'pl', 'pt'], kokoro: ['en', 'es', 'fr', 'hi', 'it', 'pt', 'ja', 'zh'], + minimax: ['en', 'zh', 'ja', 'ko', 'de', 'fr', 'ru', 'pt', 'es', 'it'], } as const; /** Helper: get language options for a given engine. */ diff --git a/app/src/lib/hooks/useGenerationForm.ts b/app/src/lib/hooks/useGenerationForm.ts index 894d9333..aa07b4e4 100644 --- a/app/src/lib/hooks/useGenerationForm.ts +++ b/app/src/lib/hooks/useGenerationForm.ts @@ -17,7 +17,7 @@ const generationSchema = z.object({ seed: z.number().int().optional(), modelSize: z.enum(['1.7B', '0.6B', '1B', '3B']).optional(), instruct: z.string().max(500).optional(), - engine: z.enum(['qwen', 'luxtts', 'chatterbox', 'chatterbox_turbo', 'tada', 'kokoro']).optional(), + engine: z.enum(['qwen', 'luxtts', 'chatterbox', 'chatterbox_turbo', 'tada', 'kokoro', 'minimax']).optional(), }); export type GenerationFormValues = z.infer; @@ -85,7 +85,9 @@ export function useGenerationForm(options: UseGenerationFormOptions = {}) { : 'tada-1b' : engine === 'kokoro' ? 'kokoro' - : `qwen-tts-${data.modelSize}`; + : engine === 'minimax' + ? 'minimax-cloud-tts' + : `qwen-tts-${data.modelSize}`; const displayName = engine === 'luxtts' ? 'LuxTTS' @@ -99,9 +101,11 @@ export function useGenerationForm(options: UseGenerationFormOptions = {}) { : 'TADA 1B' : engine === 'kokoro' ? 'Kokoro 82M' - : data.modelSize === '1.7B' - ? 'Qwen TTS 1.7B' - : 'Qwen TTS 0.6B'; + : engine === 'minimax' + ? 'MiniMax Cloud TTS' + : data.modelSize === '1.7B' + ? 'Qwen TTS 1.7B' + : 'Qwen TTS 0.6B'; // Check if model needs downloading try { diff --git a/backend/backends/__init__.py b/backend/backends/__init__.py index 33ae57d8..2d773a28 100644 --- a/backend/backends/__init__.py +++ b/backend/backends/__init__.py @@ -168,6 +168,7 @@ def is_loaded(self) -> bool: "chatterbox_turbo": "Chatterbox Turbo", "tada": "TADA", "kokoro": "Kokoro", + "minimax": "MiniMax Cloud TTS", } @@ -528,6 +529,10 @@ def get_tts_backend_for_engine(engine: str) -> TTSBackend: from .kokoro_backend import KokoroTTSBackend backend = KokoroTTSBackend() + elif engine == "minimax": + from .minimax_backend import MiniMaxTTSBackend + + backend = MiniMaxTTSBackend() else: raise ValueError(f"Unknown TTS engine: {engine}. Supported: {list(TTS_ENGINES.keys())}") diff --git a/backend/backends/minimax_backend.py b/backend/backends/minimax_backend.py new file mode 100644 index 00000000..669f57dd --- /dev/null +++ b/backend/backends/minimax_backend.py @@ -0,0 +1,210 @@ +""" +MiniMax Cloud TTS backend implementation. + +Wraps MiniMax's Text-to-Speech API for cloud-based voice synthesis. +Two model variants: + - speech-2.8-hd: High-quality, maximized timbre similarity (default) + - speech-2.8-turbo: Faster, more affordable version + +Unlike local backends, this requires a MINIMAX_API_KEY environment variable +and makes HTTP requests to the MiniMax API. No local model downloads needed. + +24kHz output, PCM audio format. +""" + +import asyncio +import json +import logging +import os +import urllib.request +import urllib.error +from typing import List, Optional, Tuple + +import numpy as np + +logger = logging.getLogger(__name__) + +MINIMAX_API_BASE = "https://api.minimax.io/v1" +MINIMAX_TTS_ENDPOINT = f"{MINIMAX_API_BASE}/t2a_v2" + +MINIMAX_DEFAULT_MODEL = "speech-2.8-hd" +MINIMAX_SAMPLE_RATE = 24000 + +# Available preset voice IDs +MINIMAX_VOICES = [ + ("English_Graceful_Lady", "Graceful Lady", "female", "en"), + ("English_Insightful_Speaker", "Insightful Speaker", "male", "en"), + ("English_radiant_girl", "Radiant Girl", "female", "en"), + ("English_Persuasive_Man", "Persuasive Man", "male", "en"), + ("English_Lucky_Robot", "Lucky Robot", "male", "en"), + ("Wise_Woman", "Wise Woman", "female", "en"), + ("cute_boy", "Cute Boy", "male", "en"), + ("lovely_girl", "Lovely Girl", "female", "en"), + ("Friendly_Person", "Friendly Person", "male", "en"), + ("Inspirational_girl", "Inspirational Girl", "female", "en"), + ("Deep_Voice_Man", "Deep Voice Man", "male", "en"), + ("sweet_girl", "Sweet Girl", "female", "en"), +] + +DEFAULT_VOICE_ID = "English_Graceful_Lady" + + +class MiniMaxTTSBackend: + """MiniMax Cloud TTS backend for cloud-based voice synthesis.""" + + def __init__(self): + self._api_key: Optional[str] = None + self._model: str = MINIMAX_DEFAULT_MODEL + self._ready = False + + def is_loaded(self) -> bool: + return self._ready + + def _get_model_path(self, model_size: str = "default") -> str: + return MINIMAX_DEFAULT_MODEL + + def _is_model_cached(self, model_size: str = "default") -> bool: + # Cloud backend — always "cached" (no download needed) + return True + + async def load_model(self, model_size: str = "default") -> None: + """Validate API key availability. No model download needed.""" + if self._ready: + return + + api_key = os.environ.get("MINIMAX_API_KEY") + if not api_key: + raise RuntimeError( + "MINIMAX_API_KEY environment variable is required for MiniMax TTS. " + "Get your API key from https://platform.minimax.io" + ) + self._api_key = api_key + self._ready = True + logger.info("MiniMax Cloud TTS ready (model: %s)", self._model) + + def unload_model(self) -> None: + """Clear API key reference.""" + self._api_key = None + self._ready = False + logger.info("MiniMax Cloud TTS unloaded") + + async def create_voice_prompt( + self, + audio_path: str, + reference_text: str, + use_cache: bool = True, + ) -> Tuple[dict, bool]: + """ + MiniMax TTS uses preset voice IDs, not reference audio cloning. + + Returns a preset voice prompt using the default voice ID. + The reference audio is ignored. + """ + return { + "voice_type": "preset", + "preset_engine": "minimax", + "preset_voice_id": DEFAULT_VOICE_ID, + }, False + + async def combine_voice_prompts( + self, + audio_paths: List[str], + reference_texts: List[str], + ) -> Tuple[np.ndarray, str]: + """Not supported — MiniMax uses preset voices, not audio cloning.""" + raise NotImplementedError( + "MiniMax Cloud TTS uses preset voice IDs and does not support " + "voice cloning from reference audio." + ) + + async def generate( + self, + text: str, + voice_prompt: dict, + language: str = "en", + seed: Optional[int] = None, + instruct: Optional[str] = None, + ) -> Tuple[np.ndarray, int]: + """ + Generate audio via MiniMax TTS API. + + Args: + text: Text to synthesize (max 10,000 chars) + voice_prompt: Dict with voice_type and preset_voice_id + language: Language code (MiniMax auto-detects language) + seed: Not supported by MiniMax TTS (ignored) + instruct: Not supported by MiniMax TTS (ignored) + + Returns: + Tuple of (audio_array, sample_rate=24000) + """ + await self.load_model() + + voice_id = DEFAULT_VOICE_ID + if isinstance(voice_prompt, dict): + voice_id = voice_prompt.get("preset_voice_id", DEFAULT_VOICE_ID) + + def _generate_sync(): + payload = { + "model": self._model, + "text": text, + "stream": False, + "voice_setting": { + "voice_id": voice_id, + "speed": 1.0, + "vol": 1.0, + "pitch": 0, + }, + "audio_setting": { + "format": "pcm", + "sample_rate": MINIMAX_SAMPLE_RATE, + }, + } + + req = urllib.request.Request( + MINIMAX_TTS_ENDPOINT, + data=json.dumps(payload).encode("utf-8"), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self._api_key}", + }, + method="POST", + ) + + logger.info( + "[MiniMax TTS] Generating (%s), voice: %s, text length: %d", + language, + voice_id, + len(text), + ) + + try: + with urllib.request.urlopen(req, timeout=120) as resp: + body = json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + error_body = e.read().decode("utf-8", errors="replace") + raise RuntimeError( + f"MiniMax TTS API error ({e.code}): {error_body}" + ) from e + + # Check for API-level errors + base_resp = body.get("base_resp", {}) + if base_resp.get("status_code", 0) != 0: + raise RuntimeError( + f"MiniMax TTS API error: {base_resp.get('status_msg', 'unknown')}" + ) + + # Extract hex-encoded audio + audio_hex = body.get("data", {}).get("audio", "") + if not audio_hex: + raise RuntimeError("MiniMax TTS API returned empty audio data") + + # Decode hex → raw PCM bytes → float32 numpy array + audio_bytes = bytes.fromhex(audio_hex) + audio = ( + np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0 + ) + + return audio, MINIMAX_SAMPLE_RATE + + return await asyncio.to_thread(_generate_sync) diff --git a/backend/models.py b/backend/models.py index f568e0fa..2c94a3d1 100644 --- a/backend/models.py +++ b/backend/models.py @@ -78,7 +78,7 @@ class GenerationRequest(BaseModel): seed: Optional[int] = Field(None, ge=0) model_size: Optional[str] = Field(default="1.7B", pattern="^(1\\.7B|0\\.6B|1B|3B)$") instruct: Optional[str] = Field(None, max_length=500) - engine: Optional[str] = Field(default="qwen", pattern="^(qwen|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$") + engine: Optional[str] = Field(default="qwen", pattern="^(qwen|luxtts|chatterbox|chatterbox_turbo|tada|kokoro|minimax)$") max_chunk_chars: int = Field( default=800, ge=100, le=5000, description="Max characters per chunk for long text splitting" ) diff --git a/backend/routes/profiles.py b/backend/routes/profiles.py index a13b02fc..cffd7036 100644 --- a/backend/routes/profiles.py +++ b/backend/routes/profiles.py @@ -90,6 +90,21 @@ async def list_preset_voices(engine: str): for vid, name, gender, lang in KOKORO_VOICES ], } + if engine == "minimax": + from ..backends.minimax_backend import MINIMAX_VOICES + + return { + "engine": engine, + "voices": [ + { + "voice_id": vid, + "name": name, + "gender": gender, + "language": lang, + } + for vid, name, gender, lang in MINIMAX_VOICES + ], + } return {"engine": engine, "voices": []} diff --git a/backend/tests/test_minimax_backend.py b/backend/tests/test_minimax_backend.py new file mode 100644 index 00000000..15ea8e86 --- /dev/null +++ b/backend/tests/test_minimax_backend.py @@ -0,0 +1,230 @@ +"""Unit tests for MiniMax Cloud TTS backend.""" + +import json +import os +import unittest +from unittest.mock import MagicMock, patch + +import numpy as np + + +class TestMiniMaxTTSBackend(unittest.TestCase): + """Tests for MiniMaxTTSBackend.""" + + def _make_backend(self): + from backend.backends.minimax_backend import MiniMaxTTSBackend + return MiniMaxTTSBackend() + + def test_initial_state(self): + backend = self._make_backend() + self.assertFalse(backend.is_loaded()) + self.assertTrue(backend._is_model_cached()) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"}) + def test_load_model_sets_ready(self): + import asyncio + backend = self._make_backend() + asyncio.get_event_loop().run_until_complete(backend.load_model()) + self.assertTrue(backend.is_loaded()) + self.assertEqual(backend._api_key, "test-key-123") + + @patch.dict(os.environ, {}, clear=True) + def test_load_model_without_api_key_raises(self): + import asyncio + backend = self._make_backend() + # Remove MINIMAX_API_KEY if present + os.environ.pop("MINIMAX_API_KEY", None) + with self.assertRaises(RuntimeError) as ctx: + asyncio.get_event_loop().run_until_complete(backend.load_model()) + self.assertIn("MINIMAX_API_KEY", str(ctx.exception)) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"}) + def test_unload_model(self): + import asyncio + backend = self._make_backend() + asyncio.get_event_loop().run_until_complete(backend.load_model()) + self.assertTrue(backend.is_loaded()) + backend.unload_model() + self.assertFalse(backend.is_loaded()) + self.assertIsNone(backend._api_key) + + def test_create_voice_prompt_returns_preset(self): + import asyncio + backend = self._make_backend() + prompt, cached = asyncio.get_event_loop().run_until_complete( + backend.create_voice_prompt("/fake/audio.wav", "test text") + ) + self.assertEqual(prompt["voice_type"], "preset") + self.assertEqual(prompt["preset_engine"], "minimax") + self.assertIn("preset_voice_id", prompt) + self.assertFalse(cached) + + def test_combine_voice_prompts_raises(self): + import asyncio + backend = self._make_backend() + with self.assertRaises(NotImplementedError): + asyncio.get_event_loop().run_until_complete( + backend.combine_voice_prompts(["/a.wav"], ["text"]) + ) + + def test_get_model_path(self): + backend = self._make_backend() + self.assertEqual(backend._get_model_path(), "speech-2.8-hd") + + def test_is_model_cached_always_true(self): + backend = self._make_backend() + self.assertTrue(backend._is_model_cached()) + self.assertTrue(backend._is_model_cached("anything")) + + +class TestMiniMaxVoices(unittest.TestCase): + """Tests for MiniMax voice definitions.""" + + def test_voices_structure(self): + from backend.backends.minimax_backend import MINIMAX_VOICES + self.assertGreater(len(MINIMAX_VOICES), 0) + for voice_id, name, gender, lang in MINIMAX_VOICES: + self.assertIsInstance(voice_id, str) + self.assertIsInstance(name, str) + self.assertIn(gender, ("male", "female")) + self.assertIsInstance(lang, str) + + def test_default_voice_id_in_list(self): + from backend.backends.minimax_backend import MINIMAX_VOICES, DEFAULT_VOICE_ID + voice_ids = [v[0] for v in MINIMAX_VOICES] + self.assertIn(DEFAULT_VOICE_ID, voice_ids) + + +class TestMiniMaxGenerate(unittest.TestCase): + """Tests for MiniMax TTS generate with mocked API.""" + + def _make_mock_response(self, audio_samples=None): + """Create a mock API response with valid PCM audio.""" + if audio_samples is None: + # Generate 1 second of silence at 24kHz + audio_samples = np.zeros(24000, dtype=np.int16) + audio_hex = audio_samples.tobytes().hex() + return json.dumps({ + "base_resp": {"status_code": 0, "status_msg": "success"}, + "data": {"audio": audio_hex}, + }).encode("utf-8") + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"}) + @patch("urllib.request.urlopen") + def test_generate_returns_audio(self, mock_urlopen): + import asyncio + + mock_resp = MagicMock() + mock_resp.read.return_value = self._make_mock_response() + mock_resp.__enter__ = lambda s: s + mock_resp.__exit__ = MagicMock(return_value=False) + mock_urlopen.return_value = mock_resp + + backend = self._make_backend() + audio, sr = asyncio.get_event_loop().run_until_complete( + backend.generate( + "Hello world", + {"voice_type": "preset", "preset_voice_id": "English_Graceful_Lady"}, + ) + ) + self.assertEqual(sr, 24000) + self.assertIsInstance(audio, np.ndarray) + self.assertEqual(audio.dtype, np.float32) + self.assertEqual(len(audio), 24000) # 1 second + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"}) + @patch("urllib.request.urlopen") + def test_generate_sends_correct_payload(self, mock_urlopen): + import asyncio + + mock_resp = MagicMock() + mock_resp.read.return_value = self._make_mock_response() + mock_resp.__enter__ = lambda s: s + mock_resp.__exit__ = MagicMock(return_value=False) + mock_urlopen.return_value = mock_resp + + backend = self._make_backend() + asyncio.get_event_loop().run_until_complete( + backend.generate( + "Test text", + {"preset_voice_id": "Deep_Voice_Man"}, + ) + ) + + # Verify the request was made with correct parameters + call_args = mock_urlopen.call_args + request = call_args[0][0] + payload = json.loads(request.data.decode("utf-8")) + + self.assertEqual(payload["model"], "speech-2.8-hd") + self.assertEqual(payload["text"], "Test text") + self.assertFalse(payload["stream"]) + self.assertEqual(payload["voice_setting"]["voice_id"], "Deep_Voice_Man") + self.assertEqual(payload["audio_setting"]["format"], "pcm") + self.assertEqual(payload["audio_setting"]["sample_rate"], 24000) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"}) + @patch("urllib.request.urlopen") + def test_generate_api_error_raises(self, mock_urlopen): + import asyncio + + mock_resp = MagicMock() + mock_resp.read.return_value = json.dumps({ + "base_resp": {"status_code": 1001, "status_msg": "Invalid API key"}, + "data": {}, + }).encode("utf-8") + mock_resp.__enter__ = lambda s: s + mock_resp.__exit__ = MagicMock(return_value=False) + mock_urlopen.return_value = mock_resp + + backend = self._make_backend() + with self.assertRaises(RuntimeError) as ctx: + asyncio.get_event_loop().run_until_complete( + backend.generate("test", {"preset_voice_id": "English_Graceful_Lady"}) + ) + self.assertIn("Invalid API key", str(ctx.exception)) + + @patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"}) + @patch("urllib.request.urlopen") + def test_generate_uses_default_voice_id(self, mock_urlopen): + import asyncio + + mock_resp = MagicMock() + mock_resp.read.return_value = self._make_mock_response() + mock_resp.__enter__ = lambda s: s + mock_resp.__exit__ = MagicMock(return_value=False) + mock_urlopen.return_value = mock_resp + + backend = self._make_backend() + asyncio.get_event_loop().run_until_complete( + backend.generate("test", {}) + ) + + call_args = mock_urlopen.call_args + request = call_args[0][0] + payload = json.loads(request.data.decode("utf-8")) + self.assertEqual(payload["voice_setting"]["voice_id"], "English_Graceful_Lady") + + def _make_backend(self): + from backend.backends.minimax_backend import MiniMaxTTSBackend + return MiniMaxTTSBackend() + + +class TestMiniMaxEngineRegistration(unittest.TestCase): + """Tests for MiniMax engine registration in backends __init__.""" + + def test_minimax_in_tts_engines(self): + from backend.backends import TTS_ENGINES + self.assertIn("minimax", TTS_ENGINES) + + def test_get_tts_backend_for_engine(self): + from backend.backends import get_tts_backend_for_engine, reset_backends + reset_backends() + backend = get_tts_backend_for_engine("minimax") + from backend.backends.minimax_backend import MiniMaxTTSBackend + self.assertIsInstance(backend, MiniMaxTTSBackend) + reset_backends() + + +if __name__ == "__main__": + unittest.main()