RootAccess4Life's picture
Upload folder using huggingface_hub
7be2f44 verified
Raw
History Blame Contribute Delete
53.6 kB
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()