import contextlib import importlib import io import os import sys import tempfile import unittest from pathlib import Path from types import SimpleNamespace from unittest import mock REQUIRED_MODEL_FILES = [ "config.yaml", "bpe.model", "gpt.pth", "s2mel.pth", "wav2vec2bert_stats.pt", "feat1.pt", "feat2.pt", ] REQUIRED_MODEL_DIRS = [ "qwen0.6bemo4-merge", ] AUX_MODEL_FILES = [ "hf_cache/semantic_codec_model.safetensors", "hf_cache/campplus_cn_common.bin", "hf_cache/bigvgan/config.json", "hf_cache/bigvgan/bigvgan_generator.pt", ] AUX_MODEL_DIRS = [ "hf_cache/w2v-bert-2.0", ] def make_model_dir(base_dir, include_aux=True): model_dir = base_dir / "checkpoints" model_dir.mkdir() for filename in REQUIRED_MODEL_FILES: (model_dir / filename).write_text("placeholder", encoding="utf-8") for dirname in REQUIRED_MODEL_DIRS: (model_dir / dirname).mkdir() if include_aux: make_aux_model_cache(model_dir) return model_dir def make_aux_model_cache(model_dir): for filename in AUX_MODEL_FILES: target = model_dir / filename target.parent.mkdir(parents=True, exist_ok=True) target.write_text("placeholder", encoding="utf-8") for dirname in AUX_MODEL_DIRS: target = model_dir / dirname target.mkdir(parents=True, exist_ok=True) (target / "config.json").write_text("placeholder", encoding="utf-8") def assert_model_resource_help(test_case, stderr, model_dir): test_case.assertIn(f"Model directory: {model_dir}", stderr) test_case.assertIn("Missing resources:", stderr) test_case.assertIn("huggingface-cli download IndexTeam/IndexTTS-2", stderr) test_case.assertIn("modelscope download --model IndexTeam/IndexTTS-2", stderr) test_case.assertIn(f"indextts2 config set model_dir {model_dir}", stderr) def user_state_env(temp_path): if sys.platform == "win32": return { "APPDATA": str(temp_path / "roaming"), "LOCALAPPDATA": str(temp_path / "local"), } if sys.platform == "darwin": return {"HOME": str(temp_path)} return { "XDG_CONFIG_HOME": str(temp_path / "config"), "XDG_DATA_HOME": str(temp_path / "data"), } def fake_torch(cuda=False, xpu=False, mps=False, cuda_device_count=0, xpu_device_count=0): return SimpleNamespace( cuda=SimpleNamespace(is_available=lambda: cuda, device_count=lambda: cuda_device_count), xpu=SimpleNamespace(is_available=lambda: xpu, device_count=lambda: xpu_device_count), backends=SimpleNamespace( mps=SimpleNamespace(is_available=lambda: mps), ), ) def patched_imports(torch_module): real_import_module = importlib.import_module def import_module(name, package=None): if name == "torch": return torch_module if name in {"torchaudio", "indextts"}: return SimpleNamespace(__name__=name) return real_import_module(name, package) return mock.patch("importlib.import_module", side_effect=import_module) def patched_missing_import(missing_package, torch_module): real_import_module = importlib.import_module def import_module(name, package=None): if name == missing_package: raise ImportError(name) if name == "torch": return torch_module if name in {"torchaudio", "indextts"}: return SimpleNamespace(__name__=name) return real_import_module(name, package) return mock.patch("importlib.import_module", side_effect=import_module) class CheckCommandTests(unittest.TestCase): def setUp(self): self.user_state = tempfile.TemporaryDirectory() self.env_patch = mock.patch.dict(os.environ, user_state_env(Path(self.user_state.name)), clear=False) self.env_patch.start() def tearDown(self): self.env_patch.stop() self.user_state.cleanup() def test_pyproject_registers_indextts2_without_replacing_existing_indextts_command(self): pyproject = Path("pyproject.toml").read_text(encoding="utf-8") self.assertIn('indextts = "indextts.cli:main"', pyproject) self.assertIn('indextts2 = "indextts.cli_v2:main"', pyproject) def test_check_returns_success_when_resources_packages_and_requested_device_are_available(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = make_model_dir(Path(temp_dir)) with patched_imports(fake_torch(cuda=True)): from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir), "--device", "cuda"]) self.assertEqual(exit_code, 0) self.assertIn(f"Checking model directory: {model_dir}", stdout.getvalue()) self.assertIn("OK: model directory", stdout.getvalue()) self.assertIn("OK: required model files", stdout.getvalue()) self.assertIn("OK: python packages", stdout.getvalue()) self.assertIn("cuda: available", stdout.getvalue()) self.assertEqual(stderr.getvalue(), "") def test_check_returns_resource_error_when_model_directory_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: missing_model_dir = Path(temp_dir) / "missing" from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(missing_model_dir)]) self.assertEqual(exit_code, 2) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: model directory does not exist", stderr.getvalue()) self.assertIn(str(missing_model_dir), stderr.getvalue()) def test_check_returns_resource_error_when_required_model_files_are_missing(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = Path(temp_dir) / "checkpoints" model_dir.mkdir() (model_dir / "config.yaml").write_text("placeholder", encoding="utf-8") from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir)]) self.assertEqual(exit_code, 2) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: missing required model files", stderr.getvalue()) self.assertIn("bpe.model", stderr.getvalue()) self.assertIn("gpt.pth", stderr.getvalue()) assert_model_resource_help(self, stderr.getvalue(), model_dir) def test_check_requires_the_full_key_model_resource_set(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = Path(temp_dir) / "checkpoints" model_dir.mkdir() for filename in [ "config.yaml", "bpe.model", "gpt.pth", "s2mel.pth", "wav2vec2bert_stats.pt", ]: (model_dir / filename).write_text("placeholder", encoding="utf-8") from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir)]) self.assertEqual(exit_code, 2) self.assertEqual(stdout.getvalue(), "") self.assertIn("feat1.pt", stderr.getvalue()) self.assertIn("feat2.pt", stderr.getvalue()) self.assertIn("qwen0.6bemo4-merge", stderr.getvalue()) def test_check_requires_the_auxiliary_model_cache_resources(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = make_model_dir(Path(temp_dir), include_aux=False) from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir)]) self.assertEqual(exit_code, 2) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: missing required model files", stderr.getvalue()) self.assertIn("hf_cache/w2v-bert-2.0", stderr.getvalue()) self.assertIn("hf_cache/semantic_codec_model.safetensors", stderr.getvalue()) self.assertIn("hf_cache/campplus_cn_common.bin", stderr.getvalue()) self.assertIn("hf_cache/bigvgan/config.json", stderr.getvalue()) self.assertIn("hf_cache/bigvgan/bigvgan_generator.pt", stderr.getvalue()) def test_check_requires_file_resources_and_directory_resources(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = Path(temp_dir) / "checkpoints" model_dir.mkdir() for filename in REQUIRED_MODEL_FILES: if filename == "gpt.pth": (model_dir / filename).mkdir() else: (model_dir / filename).write_text("placeholder", encoding="utf-8") (model_dir / "qwen0.6bemo4-merge").write_text("placeholder", encoding="utf-8") from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir)]) self.assertEqual(exit_code, 2) self.assertEqual(stdout.getvalue(), "") self.assertIn("gpt.pth", stderr.getvalue()) self.assertIn("qwen0.6bemo4-merge", stderr.getvalue()) def test_check_returns_runtime_error_when_required_python_package_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = make_model_dir(Path(temp_dir)) with patched_missing_import("torchaudio", fake_torch(cuda=True)): from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir)]) self.assertEqual(exit_code, 3) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: missing required Python packages", stderr.getvalue()) self.assertIn("torchaudio", stderr.getvalue()) def test_check_returns_runtime_error_when_requested_device_is_unavailable(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = make_model_dir(Path(temp_dir)) with patched_imports(fake_torch(cuda=False)): from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir), "--device", "cuda:0"]) self.assertEqual(exit_code, 3) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: requested device is not available: cuda:0", stderr.getvalue()) def test_check_returns_runtime_error_when_requested_cuda_index_does_not_exist(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = make_model_dir(Path(temp_dir)) with patched_imports(fake_torch(cuda=True, cuda_device_count=1)): from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir), "--device", "cuda:1"]) self.assertEqual(exit_code, 3) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: requested device is not available: cuda:1", stderr.getvalue()) def test_check_returns_runtime_error_when_requested_xpu_index_does_not_exist(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = make_model_dir(Path(temp_dir)) with patched_imports(fake_torch(xpu=True, xpu_device_count=1)): from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(["check", "--model-dir", str(model_dir), "--device", "xpu:1"]) self.assertEqual(exit_code, 3) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: requested device is not available: xpu:1", stderr.getvalue()) class SynthCommandTests(unittest.TestCase): def setUp(self): self.user_state = tempfile.TemporaryDirectory() self.env_patch = mock.patch.dict(os.environ, user_state_env(Path(self.user_state.name)), clear=False) self.env_patch.start() def tearDown(self): self.env_patch.stop() self.user_state.cleanup() def run_synth( self, temp_path, args, stdin=None, fail_init=False, fail_infer=False, noisy=False, add_model_dir=True, ): calls = [] if add_model_dir and "--model-dir" not in args: model_dir = make_model_dir(temp_path) args = [*args, "--model-dir", str(model_dir)] class FakeIndexTTS2: def __init__(self, **kwargs): calls.append(("init", kwargs)) if noisy: print("model init noise") if fail_init: raise RuntimeError("load boom") def infer(self, **kwargs): calls.append(("infer", kwargs)) if noisy: print("model infer noise") if fail_infer: raise RuntimeError("boom") from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main(args, tts_factory=FakeIndexTTS2, stdin=stdin) return exit_code, stdout.getvalue(), stderr.getvalue(), calls def test_synth_generates_audio_from_inline_text(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", " hello ", "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual( calls, [ ( "init", { "cfg_path": str(temp_path / "checkpoints" / "config.yaml"), "model_dir": str(temp_path / "checkpoints"), "use_fp16": False, "device": None, "use_cuda_kernel": False, "use_deepspeed": False, }, ), ( "infer", { "spk_audio_prompt": str(voice_path), "text": "hello", "output_path": str(output_path), "verbose": False, }, ), ], ) def test_synth_generates_audio_from_utf8_text_file(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" text_path = temp_path / "input.txt" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") text_path.write_text(" 你好, IndexTTS2 ", encoding="utf-8") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text-file", str(text_path), "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["text"], "你好, IndexTTS2") def test_synth_generates_audio_from_stdin(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--stdin", "--voice", str(voice_path), "--output", str(output_path), ], stdin=io.StringIO(" stdin text "), ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["text"], "stdin text") def test_synth_uses_emotion_audio_and_weight(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" emotion_path = temp_path / "emotion.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") emotion_path.write_bytes(b"emotion") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-audio", str(emotion_path), "--emotion-weight", "0.75", "--output", str(output_path), ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["emo_audio_prompt"], str(emotion_path)) self.assertEqual(calls[1][1]["emo_alpha"], 0.75) self.assertNotIn("use_emo_text", calls[1][1]) self.assertNotIn("emo_text", calls[1][1]) def test_synth_uses_emotion_text_and_weight(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-text", "warm and calm", "--emotion-weight", "0.6", "--output", str(output_path), ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertNotIn("emo_audio_prompt", calls[1][1]) self.assertEqual(calls[1][1]["use_emo_text"], True) self.assertEqual(calls[1][1]["emo_text"], "warm and calm") self.assertEqual(calls[1][1]["emo_alpha"], 0.6) def test_synth_uses_emotion_vector_and_weight(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-vector", "0,0,0.8,0,0,0,0,0", "--emotion-weight", "0.7", "--output", str(output_path), ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["emo_vector"], [0.0, 0.0, 0.8, 0.0, 0.0, 0.0, 0.0, 0.0]) self.assertEqual(calls[1][1]["emo_alpha"], 0.7) self.assertNotIn("emo_audio_prompt", calls[1][1]) self.assertNotIn("use_emo_text", calls[1][1]) self.assertNotIn("emo_text", calls[1][1]) def test_synth_accepts_python_list_style_emotion_vector(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-vector", "[0, 0, 0.8, 0, 0, 0, 0, 0]", "--output", str(output_path), ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["emo_vector"], [0.0, 0.0, 0.8, 0.0, 0.0, 0.0, 0.0, 0.0]) self.assertEqual(calls[1][1]["emo_alpha"], 1.0) def test_synth_does_not_rewrite_valid_emotion_vector(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-vector", "0.12,0.03,0.25,0.04,0,0.11,0.07,0.02", "--output", str(output_path), ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["emo_vector"], [0.12, 0.03, 0.25, 0.04, 0.0, 0.11, 0.07, 0.02]) def test_synth_returns_input_error_when_emotion_vector_is_empty(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-vector", "", "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: --emotion-vector must not be empty", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_emotion_vector_is_invalid(self): cases = [ ("0,0,nope,0,0,0,0,0", "entries must be numeric"), ("0,0,0.8,0,0,0,0", "exactly 8 values"), ("0,0,0.8,0,0,0,0,0,0", "exactly 8 values"), ("0,0,-0.1,0,0,0,0,0", "between 0.0 and 1.0"), ("0,0,1.1,0,0,0,0,0", "between 0.0 and 1.0"), ("0.2,0.2,0.2,0.2,0.1,0,0,0", "sum must be <= 0.8"), ] for vector, expected_error in cases: with self.subTest(vector=vector): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-vector", vector, "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: --emotion-vector", stderr) self.assertIn(expected_error, stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_emotion_vector_conflicts_with_other_emotion_sources(self): for other_emotion_args in (["--emotion-audio"], ["--emotion-text", "warm and calm"]): with self.subTest(other_emotion_args=other_emotion_args): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" emotion_path = temp_path / "emotion.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") emotion_path.write_bytes(b"emotion") args = [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-vector", "0,0,0.8,0,0,0,0,0", "--output", str(output_path), ] if other_emotion_args == ["--emotion-audio"]: args.extend(["--emotion-audio", str(emotion_path)]) else: args.extend(other_emotion_args) exit_code, stdout, stderr, calls = self.run_synth(temp_path, args) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("--emotion-vector, --emotion-audio and --emotion-text are mutually exclusive", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_emotion_text_is_empty(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-text", "", "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: --emotion-text must not be empty", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_emotion_sources_conflict(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" emotion_path = temp_path / "emotion.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") emotion_path.write_bytes(b"emotion") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-audio", str(emotion_path), "--emotion-text", "warm and calm", "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: --emotion-audio and --emotion-text are mutually exclusive", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_empty_emotion_audio_conflicts_with_emotion_text(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-audio", "", "--emotion-text", "warm and calm", "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: --emotion-audio and --emotion-text are mutually exclusive", stderr) self.assertEqual(calls, []) def test_synth_returns_resource_error_when_emotion_audio_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" emotion_path = temp_path / "missing-emotion.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-audio", str(emotion_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 2) self.assertEqual(stdout, "") self.assertIn("ERROR: emotion reference audio does not exist", stderr) self.assertIn(str(emotion_path), stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_emotion_weight_is_not_a_float(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" emotion_path = temp_path / "emotion.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") emotion_path.write_bytes(b"emotion") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--emotion-audio", str(emotion_path), "--emotion-weight", "strong", "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: --emotion-weight must be a float", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_text_source_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: provide exactly one text source", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_text_sources_conflict(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" text_path = temp_path / "input.txt" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") text_path.write_text("file text", encoding="utf-8") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "inline", "--text-file", str(text_path), "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: provide exactly one text source", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_empty_text_source_conflicts_with_stdin(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "", "--stdin", "--voice", str(voice_path), "--output", str(output_path), ], stdin=io.StringIO("stdin text"), ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: provide exactly one text source", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_text_is_empty_after_trimming(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", " \t\r\n ", "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: text is empty", stderr) self.assertEqual(calls, []) def test_synth_returns_resource_error_when_text_file_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" text_path = temp_path / "missing.txt" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text-file", str(text_path), "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 2) self.assertEqual(stdout, "") self.assertIn("ERROR: text file does not exist", stderr) self.assertIn(str(text_path), stderr) self.assertEqual(calls, []) def test_synth_returns_resource_error_when_voice_file_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "missing.wav" output_path = temp_path / "out.wav" exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 2) self.assertEqual(stdout, "") self.assertIn("ERROR: voice reference audio does not exist", stderr) self.assertIn(str(voice_path), stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_voice_argument_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) output_path = temp_path / "out.wav" exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--output", str(output_path), ], ) self.assertEqual(exit_code, 2) self.assertEqual(stdout, "") self.assertIn("ERROR: --voice is required", stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_output_exists_without_force(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") output_path.write_bytes(b"existing") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: output file already exists", stderr) self.assertIn(str(output_path), stderr) self.assertEqual(calls, []) def test_synth_returns_input_error_when_output_argument_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), ], ) self.assertEqual(exit_code, 1) self.assertEqual(stdout, "") self.assertIn("ERROR: --output is required", stderr) self.assertEqual(calls, []) def test_synth_allows_existing_output_when_force_is_set(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") output_path.write_bytes(b"existing") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), "--force", ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["output_path"], str(output_path)) def test_synth_creates_output_parent_directory(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "nested" / "audio" / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), ], ) output_parent_exists = output_path.parent.is_dir() self.assertEqual(exit_code, 0) self.assertTrue(output_parent_exists) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual(calls[1][1]["output_path"], str(output_path)) def test_synth_maps_runtime_options_to_indextts2(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) model_dir = make_model_dir(temp_path) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), "--model-dir", str(model_dir), "--device", "cuda:0", "--fp16", "--deepspeed", "--cuda-kernel", "--verbose", ], ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertEqual(stderr, "") self.assertEqual( calls[0][1], { "cfg_path": str(model_dir / "config.yaml"), "model_dir": str(model_dir), "use_fp16": True, "device": "cuda:0", "use_cuda_kernel": True, "use_deepspeed": True, }, ) self.assertTrue(calls[1][1]["verbose"]) def test_synth_returns_inference_error_when_indextts2_infer_fails(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), ], fail_infer=True, ) self.assertEqual(exit_code, 4) self.assertEqual(stdout, "") self.assertIn("ERROR: inference failed: boom", stderr) self.assertEqual(calls[0][0], "init") self.assertEqual(calls[1][0], "infer") def test_synth_returns_inference_error_when_indextts2_initialization_fails(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), ], fail_init=True, ) self.assertEqual(exit_code, 4) self.assertEqual(stdout, "") self.assertIn("ERROR: inference failed: load boom", stderr) self.assertEqual(calls[0][0], "init") def test_synth_returns_resource_error_when_model_directory_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" missing_model_dir = temp_path / "missing-models" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), "--model-dir", str(missing_model_dir), ], add_model_dir=False, ) self.assertEqual(exit_code, 2) self.assertEqual(stdout, "") self.assertIn("ERROR: model directory does not exist", stderr) self.assertIn(str(missing_model_dir), stderr) self.assertEqual(calls, []) def test_synth_returns_resource_error_when_model_file_is_missing(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) model_dir = temp_path / "models" voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" model_dir.mkdir() (model_dir / "config.yaml").write_text("placeholder", encoding="utf-8") voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), "--model-dir", str(model_dir), ], add_model_dir=False, ) self.assertEqual(exit_code, 2) self.assertEqual(stdout, "") self.assertIn("ERROR: missing required model files", stderr) self.assertIn("bpe.model", stderr) assert_model_resource_help(self, stderr, model_dir) self.assertEqual(calls, []) def test_synth_returns_runtime_error_when_indextts2_import_fails(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) model_dir = make_model_dir(temp_path) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") from indextts.cli_v2 import main stdout = io.StringIO() stderr = io.StringIO() with mock.patch("indextts.cli_v2._load_indextts2", side_effect=ImportError("torch")): with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): exit_code = main( [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), "--model-dir", str(model_dir), ] ) self.assertEqual(exit_code, 3) self.assertEqual(stdout.getvalue(), "") self.assertIn("ERROR: runtime unavailable: torch", stderr.getvalue()) def test_load_indextts2_points_huggingface_cache_at_model_resource_directory(self): with tempfile.TemporaryDirectory() as temp_dir: model_dir = Path(temp_dir) / "models" class FakeIndexTTS2: pass from indextts.cli_v2 import _load_indextts2 with mock.patch.dict(os.environ, {"HF_HUB_CACHE": "legacy-cache"}, clear=False): with mock.patch.dict( sys.modules, {"indextts.infer_v2": SimpleNamespace(IndexTTS2=FakeIndexTTS2)}, clear=False, ): loaded = _load_indextts2(model_dir) hf_hub_cache = os.environ["HF_HUB_CACHE"] self.assertIs(loaded, FakeIndexTTS2) self.assertEqual(hf_hub_cache, str(model_dir / "hf_cache")) def test_synth_suppresses_model_stdout_when_not_verbose(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), ], noisy=True, ) self.assertEqual(exit_code, 0) self.assertEqual(stdout, f"Generated: {output_path}\n") self.assertNotIn("model init noise", stdout) self.assertNotIn("model infer noise", stdout) self.assertEqual(stderr, "") self.assertEqual(calls[1][0], "infer") def test_synth_allows_model_stdout_when_verbose(self): with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) voice_path = temp_path / "voice.wav" output_path = temp_path / "out.wav" voice_path.write_bytes(b"voice") exit_code, stdout, stderr, calls = self.run_synth( temp_path, [ "synth", "--text", "hello", "--voice", str(voice_path), "--output", str(output_path), "--verbose", ], noisy=True, ) self.assertEqual(exit_code, 0) self.assertIn("model init noise", stdout) self.assertIn("model infer noise", stdout) self.assertIn(f"Generated: {output_path}\n", stdout) self.assertEqual(stderr, "") self.assertEqual(calls[1][0], "infer") if __name__ == "__main__": unittest.main()