From f00249db9d1ea051d29aa1bcca869fc4b88e83eb Mon Sep 17 00:00:00 2001 From: historia Date: Mon, 24 Aug 2026 02:59:26 -0400 Subject: refactor: add app directory, dir structure change --- app/tests/test_backends_audiocpp.py | 1062 +++++++++++++++++++++++++++++++++++ 1 file changed, 1062 insertions(+) create mode 100644 app/tests/test_backends_audiocpp.py (limited to 'app/tests/test_backends_audiocpp.py') diff --git a/app/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py new file mode 100644 index 0000000..9882ce1 --- /dev/null +++ b/app/tests/test_backends_audiocpp.py @@ -0,0 +1,1062 @@ +"""Tests for the audio.cpp backend setup module (backends/audiocpp.py).""" + +import io +import json +import sys +import tempfile +import unittest +from contextlib import redirect_stdout +from pathlib import Path +from unittest.mock import MagicMock, patch + +from converter import config +from backends import audiocpp as make_server + +FAKE_CONFIG = ( + 'LANGUAGE = "English"\n' + "\n" + 'AUDIOCPP_API_URL = "http://127.0.0.1:9999" # audio.cpp audiocpp_server\n' + "\n" + "CHUNK_SIZE = 250\n" +) + +FAKE_CONFIG_WITH_MODEL_IDS = ( + 'AUDIOCPP_API_URL = "http://127.0.0.1:9999" # audio.cpp audiocpp_server\n' + "\n" + 'AUDIOCPP_MODEL_ID = "qwen" # server entry for speaker mode\n' + 'AUDIOCPP_CLONE_MODEL_ID = "qwen-clone"\n' +) + + +def _write_spec(checkout: Path, family: str, *, display_name=None, + tasks=("tts", "clone"), languages=("en",), packages=None, + category="tts"): + """Write a minimal model_specs/.json into a fake checkout.""" + specs = checkout / "model_specs" + specs.mkdir(parents=True, exist_ok=True) + if packages is None: + packages = [{ + "id": f"{family}_q8_0", "default": True, "format": "gguf", + "target_directory": f"{family}-GGUF", + }] + spec = { + "family": family, + "display_name": display_name or family, + "category": category, + "tasks": list(tasks), + "languages": list(languages), + "packages": packages, + } + (specs / f"{family}.json").write_text(json.dumps(spec), encoding="utf-8") + return spec + + +def _make_checkout(tmp: Path) -> Path: + """Create a fake audio.cpp checkout with a realistic model_specs set.""" + checkout = tmp / "audio.cpp" + checkout.mkdir() + _write_spec(checkout, "qwen3_tts", display_name="Qwen3-TTS", + tasks=("tts", "clone", "design"), + languages=("zh", "en", "ja"), + packages=[ + {"id": "qwen3_tts_1_7b_base_q8_0", "default": True, + "format": "gguf", + "target_directory": "Qwen3-TTS-12Hz-1.7B-Base-GGUF"}, + {"id": "qwen3_tts_1_7b_customvoice_q8_0", + "format": "gguf", + "target_directory": "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF"}, + {"id": "qwen3_tts_1_7b_voicedesign_q8_0", + "format": "gguf", + "target_directory": "Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF"}, + ]) + _write_spec(checkout, "higgs_audio_tts", display_name="Higgs Audio v3 TTS 4B", + languages=("auto",), + packages=[{ + "id": "higgs_audio_tts_4b_q8_0", "default": True, + "format": "gguf", + "target_directory": "Higgs-Audio-v3-TTS-4B-GGUF", + }]) + _write_spec(checkout, "voxcpm2", display_name="VoxCPM2-2B", + languages=("en", "zh"), + packages=[{ + "id": "voxcpm2_q8_0", "default": True, "format": "gguf", + "target_directory": "VoxCPM2-GGUF", + }]) + _write_spec(checkout, "index_tts2", display_name="IndexTTS-2", + languages=("zh", "en"), + packages=[{ + "id": "index_tts2_q8_0", "default": True, "format": "gguf", + "target_directory": "IndexTTS2-GGUF", + }]) + _write_spec(checkout, "pocket_tts", display_name="PocketTTS-100M", + tasks=("tts", "clone"), languages=("en", "de"), + packages=[{ + "id": "pocket_tts_q8_0", "default": True, "format": "gguf", + "target_directory": "PocketTTS-GGUF", + }]) + _write_spec(checkout, "supertonic", display_name="Supertonic 3", + tasks=("tts",), languages=("en", "ko"), + packages=[{ + "id": "supertonic_q8_0", "default": True, "format": "gguf", + "target_directory": "Supertonic-GGUF", + }]) + # An ASR family that must be filtered out. + _write_spec(checkout, "qwen3_asr", display_name="Qwen3-ASR", + tasks=("asr",), category="asr") + # A TTS family with no installable packages (must be skipped). + _write_spec(checkout, "empty_tts", display_name="Empty TTS", + tasks=("tts",), packages=[]) + return checkout + + +class FindWavFilesTests(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.folder = Path(self._tmp.name) + + def tearDown(self): + self._tmp.cleanup() + + def _touch(self, name): + path = self.folder / name + path.write_bytes(b"x") + return path + + def test_finds_only_wavs_case_insensitive(self): + self._touch("b.wav") + self._touch("a.WAV") + self._touch("notes.txt") + (self.folder / "sub").mkdir() + (self.folder / "sub" / "c.wav").write_bytes(b"x") + names = [path.name for path in make_server.find_wav_files(self.folder)] + self.assertEqual(names, ["a.WAV", "b.wav"]) + + def test_sorted_alphabetically_case_insensitive(self): + for name in ("Zed.wav", "alpha.wav", "Beta.wav"): + self._touch(name) + names = [path.name for path in make_server.find_wav_files(self.folder)] + self.assertEqual(names, ["alpha.wav", "Beta.wav", "Zed.wav"]) + + def test_empty_directory_returns_empty_list(self): + self.assertEqual(make_server.find_wav_files(self.folder), []) + + +class DetectWavDirTests(unittest.TestCase): + """Shallow .wav-directory discovery across the two checkout roots.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.root = Path(self._td.name) + self.audiocpp = self.root / "audio.cpp" + self.tts_root = self.root / "tts-audiobook-generator" + self.audiocpp.mkdir() + self.tts_root.mkdir() + + def tearDown(self): + self._td.cleanup() + + def _wav_dir(self, where, name="voices"): + directory = where / name + directory.mkdir(parents=True, exist_ok=True) + (directory / "voice.wav").write_bytes(b"x") + return directory + + def test_unique_wav_dir_in_tts_root_returned(self): + found = self._wav_dir(self.tts_root, "voices") + self.assertEqual(make_server.detect_wav_dir(self.audiocpp, + self.tts_root), + found) + + def test_unique_wav_dir_in_audiocpp_root_returned(self): + found = self._wav_dir(self.audiocpp, "reference") + self.assertEqual(make_server.detect_wav_dir(self.audiocpp, + self.tts_root), + found) + + def test_root_itself_containing_wavs_returned(self): + (self.tts_root / "direct.wav").write_bytes(b"x") + self.assertEqual(make_server.detect_wav_dir(self.audiocpp, + self.tts_root), + self.tts_root) + + def test_multiple_wav_dirs_returns_none(self): + self._wav_dir(self.tts_root, "one") + self._wav_dir(self.audiocpp, "two") + self.assertIsNone(make_server.detect_wav_dir(self.audiocpp, + self.tts_root)) + + def test_output_dir_of_tts_root_excluded(self): + self._wav_dir(self.tts_root, "output") + self.assertIsNone(make_server.detect_wav_dir(self.audiocpp, + self.tts_root)) + + def test_no_wavs_returns_none(self): + self.assertIsNone(make_server.detect_wav_dir(self.audiocpp, + self.tts_root)) + + def test_nested_wav_dir_not_seen(self): + nested = self.tts_root / "outer" / "inner" + nested.mkdir(parents=True) + (nested / "voice.wav").write_bytes(b"x") + self.assertIsNone(make_server.detect_wav_dir(self.audiocpp, + self.tts_root)) + + +class ConfigPortTests(unittest.TestCase): + def test_port_parsed_from_config_url(self): + with patch.object(config, "AUDIOCPP_API_URL", + "http://127.0.0.1:8080"): + self.assertEqual(make_server.config_port(), 8080) + + def test_missing_port_falls_back(self): + with patch.object(config, "AUDIOCPP_API_URL", "http://127.0.0.1"): + self.assertEqual(make_server.config_port(), + make_server.FALLBACK_PORT) + + def test_invalid_url_falls_back(self): + with patch.object(config, "AUDIOCPP_API_URL", "not a url"): + self.assertEqual(make_server.config_port(), + make_server.FALLBACK_PORT) + + def test_url_with_port_replaces_port(self): + self.assertEqual( + make_server._url_with_port("http://127.0.0.1:8080", 9000), + "http://127.0.0.1:9000") + + def test_url_without_port_adds_port(self): + self.assertEqual( + make_server._url_with_port("http://localhost", 8080), + "http://localhost:8080") + + +class UpdateConfigPortTests(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.config_path = Path(self._tmp.name) / "config.py" + self.config_path.write_text(FAKE_CONFIG, encoding="utf-8") + + def tearDown(self): + self._tmp.cleanup() + + def test_rewrites_port_preserving_comment(self): + changed = make_server.update_config_api_url_port( + 8080, config_path=self.config_path) + self.assertTrue(changed) + text = self.config_path.read_text(encoding="utf-8") + self.assertIn( + 'AUDIOCPP_API_URL = "http://127.0.0.1:8080" # audio.cpp audiocpp_server', + text) + self.assertIn('LANGUAGE = "English"', text) + self.assertIn("CHUNK_SIZE = 250", text) + + def test_returns_false_when_no_url_line(self): + path = Path(self._tmp.name) / "other.py" + path.write_text('CHUNK_SIZE = 250\n', encoding="utf-8") + self.assertFalse(make_server.update_config_api_url_port( + 8080, config_path=path)) + + def test_returns_false_when_port_unchanged(self): + self.assertFalse(make_server.update_config_api_url_port( + 9999, config_path=self.config_path)) + self.assertEqual(self.config_path.read_text(encoding="utf-8"), + FAKE_CONFIG) + + def test_returns_false_when_file_missing(self): + self.assertFalse(make_server.update_config_api_url_port( + 8080, config_path=Path(self._tmp.name) / "nope.py")) + + +class UpdateConfigModelIdsTests(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.config_path = Path(self._tmp.name) / "config.py" + self.config_path.write_text(FAKE_CONFIG_WITH_MODEL_IDS, + encoding="utf-8") + + def tearDown(self): + self._tmp.cleanup() + + def test_rewrites_both_ids_preserving_lines(self): + changed = make_server.update_config_model_ids( + "higgs", "higgs", config_path=self.config_path) + self.assertTrue(changed) + text = self.config_path.read_text(encoding="utf-8") + self.assertIn('AUDIOCPP_MODEL_ID = "higgs" # server entry for speaker mode', + text) + self.assertIn('AUDIOCPP_CLONE_MODEL_ID = "higgs"', text) + self.assertIn('AUDIOCPP_API_URL = "http://127.0.0.1:9999"', text) + + def test_clone_id_optional(self): + changed = make_server.update_config_model_ids( + "voxcpm2", config_path=self.config_path) + self.assertTrue(changed) + text = self.config_path.read_text(encoding="utf-8") + self.assertIn('AUDIOCPP_MODEL_ID = "voxcpm2"', text) + self.assertIn('AUDIOCPP_CLONE_MODEL_ID = "qwen-clone"', text) + + def test_returns_false_when_ids_unchanged(self): + changed = make_server.update_config_model_ids( + "qwen", "qwen-clone", config_path=self.config_path) + self.assertFalse(changed) + self.assertEqual(self.config_path.read_text(encoding="utf-8"), + FAKE_CONFIG_WITH_MODEL_IDS) + + def test_returns_false_when_lines_missing(self): + path = Path(self._tmp.name) / "other.py" + path.write_text('CHUNK_SIZE = 250\n', encoding="utf-8") + self.assertFalse(make_server.update_config_model_ids( + "higgs", "higgs", config_path=path)) + + def test_returns_false_when_file_missing(self): + self.assertFalse(make_server.update_config_model_ids( + "higgs", "higgs", + config_path=Path(self._tmp.name) / "nope.py")) + + +class ResolveWavDirArgTests(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.folder = Path(self._tmp.name) + + def tearDown(self): + self._tmp.cleanup() + + def test_resolves_to_absolute(self): + self.assertEqual(make_server.resolve_wav_dir_arg(str(self.folder)), + self.folder.resolve()) + + def test_strips_surrounding_quotes(self): + quoted = f'"{self.folder}"' + self.assertEqual(make_server.resolve_wav_dir_arg(quoted), + self.folder.resolve()) + + def test_strips_single_quotes(self): + quoted = f"'{self.folder}'" + self.assertEqual(make_server.resolve_wav_dir_arg(quoted), + self.folder.resolve()) + + def test_strips_whitespace(self): + self.assertEqual(make_server.resolve_wav_dir_arg(f" {self.folder} "), + self.folder.resolve()) + + def test_expands_tilde(self): + with patch.object(make_server.os.path, "expanduser", + return_value=str(self.folder)) as mock_expand: + result = make_server.resolve_wav_dir_arg("~/voices") + mock_expand.assert_called_once_with("~/voices") + self.assertEqual(result, self.folder.resolve()) + + def test_trailing_slash_preserved_as_dir(self): + self.assertEqual(make_server.resolve_wav_dir_arg(f"{self.folder}/"), + self.folder.resolve()) + + +class NormalizeDirArgTests(unittest.TestCase): + """Path normalization for the audio.cpp checkout argument.""" + + def test_expands_tilde_and_resolves(self): + with patch.object(make_server.os.path, "expanduser", + return_value="/home/u/audio.cpp") as mock_expand: + result = make_server.normalize_dir_arg("~/audio.cpp") + mock_expand.assert_called_once_with("~/audio.cpp") + self.assertEqual(result, Path("/home/u/audio.cpp").resolve()) + + def test_strips_quotes_and_whitespace(self): + with patch.object(make_server.os.path, "expanduser", + side_effect=lambda s: s): + result = make_server.normalize_dir_arg(' "/tmp/foo" ') + self.assertEqual(result, Path("/tmp/foo").resolve()) + + +class CheckoutAutoSelectTests(unittest.TestCase): + """TUI browser auto-accept callback for an audio.cpp checkout.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.root = Path(self._td.name) + + def tearDown(self): + self._td.cleanup() + + def test_accepts_audio_cpp_containing_model_specs(self): + checkout = self.root / "audio.cpp" + checkout.mkdir() + (checkout / "model_specs").mkdir() + self.assertEqual(make_server._checkout_auto_select(checkout), + checkout) + + def test_rejects_audio_cpp_without_model_specs(self): + checkout = self.root / "audio.cpp" + checkout.mkdir() + self.assertIsNone(make_server._checkout_auto_select(checkout)) + + def test_rejects_other_name_even_with_model_specs(self): + other = self.root / "not-audiocpp" + other.mkdir() + (other / "model_specs").mkdir() + self.assertIsNone(make_server._checkout_auto_select(other)) + + def test_rejects_plain_directory(self): + plain = self.root / "somewhere" + plain.mkdir() + self.assertIsNone(make_server._checkout_auto_select(plain)) + + +class DefaultModelIdTests(unittest.TestCase): + def test_preferred_ids_for_tested_families(self): + self.assertEqual(make_server.default_model_id("qwen3_tts"), "qwen") + self.assertEqual(make_server.default_model_id("higgs_audio_tts"), "higgs") + self.assertEqual(make_server.default_model_id("voxcpm2"), "voxcpm2") + self.assertEqual(make_server.default_model_id("index_tts2"), "indextts2") + + def test_derived_id_strips_trailing_tts_and_underscores(self): + self.assertEqual(make_server.default_model_id("pocket_tts"), "pocket") + self.assertEqual(make_server.default_model_id("dots_tts"), "dots") + self.assertEqual(make_server.default_model_id("moss_tts_local"), + "mossttslocal") + + +class LoadModelCatalogTests(unittest.TestCase): + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.checkout = _make_checkout(Path(self._td.name)) + + def tearDown(self): + self._td.cleanup() + + def test_includes_tts_families_excludes_asr(self): + catalog = make_server.load_model_catalog(self.checkout) + families = [entry["family"] for entry in catalog] + self.assertIn("qwen3_tts", families) + self.assertIn("higgs_audio_tts", families) + self.assertIn("pocket_tts", families) + self.assertIn("supertonic", families) + self.assertNotIn("qwen3_asr", families) + + def test_skips_families_with_no_packages(self): + catalog = make_server.load_model_catalog(self.checkout) + self.assertNotIn("empty_tts", + [entry["family"] for entry in catalog]) + + def test_families_sorted_alphabetically_by_display_name(self): + catalog = make_server.load_model_catalog(self.checkout) + names = [entry["display_name"].lower() for entry in catalog] + self.assertEqual(names, sorted(names)) + self.assertNotIn("tested", catalog[0]) + self.assertNotIn("TESTED_FAMILIES", dir(make_server)) + + def test_default_package_and_target_directory_resolved(self): + catalog = make_server.load_model_catalog(self.checkout) + by_family = {entry["family"]: entry for entry in catalog} + higgs = by_family["higgs_audio_tts"] + self.assertEqual(higgs["install_id"], "higgs_audio_tts_4b_q8_0") + self.assertEqual(higgs["default_path"], + "models/Higgs-Audio-v3-TTS-4B-GGUF") + + def test_picks_first_gguf_when_no_default_flag(self): + _write_spec(self.checkout, "voxcpm2", display_name="VoxCPM2-2B", + packages=[ + {"id": "voxcpm2_bf16", "format": "gguf", + "target_directory": "VoxCPM2-GGUF"}, + {"id": "voxcpm2_q8_0", "format": "gguf", + "target_directory": "VoxCPM2-GGUF"}, + ]) + catalog = make_server.load_model_catalog(self.checkout) + by_family = {entry["family"]: entry for entry in catalog} + self.assertEqual(by_family["voxcpm2"]["install_id"], "voxcpm2_bf16") + + def test_clone_capability_from_tasks(self): + catalog = make_server.load_model_catalog(self.checkout) + by_family = {entry["family"]: entry for entry in catalog} + self.assertTrue(by_family["higgs_audio_tts"]["clone_capable"]) + self.assertFalse(by_family["supertonic"]["clone_capable"]) + + def test_missing_model_specs_dir_raises(self): + empty = Path(self._td.name) / "empty" + empty.mkdir() + with self.assertRaises(NotADirectoryError): + make_server.load_model_catalog(empty) + + +class DetectBackendTests(unittest.TestCase): + """Backend detection from audio.cpp build directory names.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.checkout = Path(self._td.name) / "audio.cpp" + self.checkout.mkdir() + + def tearDown(self): + self._td.cleanup() + + def _build(self, name, binary="audiocpp_server"): + build_dir = self.checkout / "build" / name + bin_dir = build_dir / "bin" + bin_dir.mkdir(parents=True) + (bin_dir / binary).write_bytes(b"x") + return build_dir + + def test_no_build_dir_returns_none(self): + self.assertIsNone(make_server.detect_backend(self.checkout)) + + def test_unique_linux_backend_detected(self): + self._build("linux-cuda-release") + self.assertEqual(make_server.detect_backend(self.checkout), "cuda") + + def test_windows_exe_backend_detected(self): + self._build("windows-vulkan-debug", binary="audiocpp_server.exe") + self.assertEqual(make_server.detect_backend(self.checkout), "vulkan") + + def test_hip_backend_detected(self): + self._build("linux-hip-release") + self.assertEqual(make_server.detect_backend(self.checkout), "hip") + + def test_cpu_backend_detected(self): + self._build("linux-cpu-release") + self.assertEqual(make_server.detect_backend(self.checkout), "cpu") + + def test_metal_maps_to_cpu(self): + self._build("macos-metal-release") + self.assertEqual(make_server.detect_backend(self.checkout), "cpu") + + def test_multiple_backends_returns_none(self): + self._build("linux-cuda-release") + self._build("linux-cpu-release") + self.assertIsNone(make_server.detect_backend(self.checkout)) + + def test_multiple_builds_same_backend_detected(self): + self._build("linux-cuda-release") + self._build("windows-cuda-debug") + self.assertEqual(make_server.detect_backend(self.checkout), "cuda") + + def test_build_dir_without_binary_ignored(self): + (self.checkout / "build" / "linux-cuda-release").mkdir(parents=True) + self.assertIsNone(make_server.detect_backend(self.checkout)) + + def test_non_matching_build_dir_name_ignored(self): + self._build("linux-mybuild-release") + self.assertIsNone(make_server.detect_backend(self.checkout)) + + +class BackendOptionsTests(unittest.TestCase): + """Aligned backend menu labels and the [auto-detected] default.""" + + def test_options_have_aligned_dashes(self): + options, default_index = make_server._backend_options() + dash_columns = {label.index(" - ") for label, _ in options} + self.assertEqual(len(dash_columns), 1) + self.assertEqual(default_index, 0) + + def test_detected_backend_marked_and_defaulted(self): + options, default_index = make_server._backend_options("vulkan") + labels = [label for label, _ in options] + self.assertEqual(default_index, labels.index(next( + label for label, value in options + if value == "vulkan" and label.endswith("[auto-detected]")))) + self.assertTrue(labels[default_index].endswith("[auto-detected]")) + self.assertEqual(options[default_index][1], "vulkan") + + def test_unknown_detected_backend_is_ignored(self): + options, default_index = make_server._backend_options("opencl") + self.assertEqual(default_index, 0) + self.assertFalse(any("[auto-detected]" in label + for label, _ in options)) + + def test_labels_keep_backend_values(self): + options, _ = make_server._backend_options() + self.assertEqual([value for _, value in options], + list(make_server.BACKENDS)) + + +class BuildServerConfigTests(unittest.TestCase): + def test_single_entry_without_voice_dir(self): + entry = make_server.build_model_entry( + "higgs_audio_tts", "higgs", "models/Higgs-GGUF") + cfg = make_server.build_server_config( + "127.0.0.1", 8080, "cuda", False, [entry]) + self.assertEqual(cfg["host"], "127.0.0.1") + self.assertEqual(cfg["port"], 8080) + self.assertEqual(cfg["backend"], "cuda") + self.assertFalse(cfg["lazy_load"]) + self.assertEqual(cfg["models"], [entry]) + self.assertNotIn("voice_dir", cfg) + + def test_voice_dir_added_when_given(self): + entry = make_server.build_model_entry("voxcpm2", "voxcpm2", "models/V") + cfg = make_server.build_server_config( + "0.0.0.0", 9000, "cpu", True, [entry], + voice_dir="/abs/voices") + self.assertTrue(cfg["lazy_load"]) + self.assertEqual(cfg["voice_dir"], "/abs/voices") + + def test_model_entry_shape(self): + entry = make_server.build_model_entry("index_tts2", "indextts2", "p") + self.assertEqual(entry["id"], "indextts2") + self.assertEqual(entry["family"], "index_tts2") + self.assertEqual(entry["path"], "p") + self.assertEqual(entry["task"], "tts") + self.assertEqual(entry["mode"], "offline") + + def test_model_entry_design_task(self): + entry = make_server.build_model_entry( + "qwen3_tts", "qwen-design", "p", task="vdes") + self.assertEqual(entry["task"], "vdes") + self.assertEqual(entry["mode"], "offline") + + +class InstallModelsTests(unittest.TestCase): + """Printing or auto-running the model install commands.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.checkout = Path(self._td.name) / "audio.cpp" + self.checkout.mkdir() + self.manager = self.checkout / "tools" / "model_manager_v2.py" + self.manager.parent.mkdir() + self.manager.write_text("#!/usr/bin/env python3\n", encoding="utf-8") + self.guidance = [("Higgs Audio v3 TTS 4B", "higgs_audio_tts_4b_q8_0"), + ("Qwen3-TTS", "qwen3_tts_1_7b_base_q8_0"), + ("Qwen3-TTS", "qwen3_tts_1_7b_base_q8_0")] + + def tearDown(self): + self._td.cleanup() + + def test_declined_download_prints_commands_deduped(self): + buf = io.StringIO() + with redirect_stdout(buf), \ + patch.object(make_server.subprocess, "run") as run: + make_server._install_models(self.checkout, self.guidance, + download=False) + out = buf.getvalue() + self.assertEqual(out.count("install higgs_audio_tts_4b_q8_0"), 1) + self.assertEqual(out.count("install qwen3_tts_1_7b_base_q8_0"), 1) + run.assert_not_called() + + def test_accepted_download_runs_each_command(self): + with patch.object(make_server.subprocess, "run", + return_value=MagicMock(returncode=0)) as run: + make_server._install_models(self.checkout, self.guidance, + download=True) + self.assertEqual(run.call_count, 2) + commands = [call[0][0] for call in run.call_args_list] + self.assertEqual(commands[0], + [sys.executable, str(self.manager), "install", + "higgs_audio_tts_4b_q8_0"]) + self.assertEqual(commands[1], + [sys.executable, str(self.manager), "install", + "qwen3_tts_1_7b_base_q8_0"]) + for call in run.call_args_list: + self.assertEqual(call[1]["cwd"], str(self.checkout)) + + def test_missing_manager_falls_back_to_printing(self): + self.manager.unlink() + buf = io.StringIO() + with redirect_stdout(buf), \ + patch.object(make_server.subprocess, "run") as run: + make_server._install_models(self.checkout, self.guidance, + download=True) + self.assertIn("install higgs_audio_tts_4b_q8_0", buf.getvalue()) + run.assert_not_called() + + def test_failed_install_reports_warning_and_continues(self): + results = iter([MagicMock(returncode=1), MagicMock(returncode=0)]) + buf = io.StringIO() + with redirect_stdout(buf), \ + patch.object(make_server.subprocess, "run", + side_effect=lambda *a, **k: next(results)) as run: + make_server._install_models(self.checkout, self.guidance, + download=True) + self.assertEqual(run.call_count, 2) + self.assertIn("exited with code 1", buf.getvalue()) + + def test_decide_download_skips_prompt_without_manager(self): + self.manager.unlink() + confirm = MagicMock() + self.assertFalse(make_server._decide_download(self.checkout, confirm)) + confirm.assert_not_called() + + def test_decide_download_asks_when_manager_present(self): + confirm = MagicMock(return_value=True) + self.assertTrue(make_server._decide_download(self.checkout, confirm)) + confirm.assert_called_once() + + +class TranscribeWavDirTests(unittest.TestCase): + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.folder = Path(self._td.name) + self.narrator = self.folder / "narrator.wav" + self.narrator.write_bytes(b"x") + self.other = self.folder / "other.wav" + self.other.write_bytes(b"x") + + def tearDown(self): + self._td.cleanup() + + def test_transcribes_to_stem_map_with_absolute_paths(self): + transcripts = {str(self.narrator): "First.", + str(self.other): "Second."} + with patch.object(make_server, "transcribe_reference_audio", + side_effect=lambda path, model_name="base": + transcripts[path]): + result = make_server.transcribe_wav_dir( + [self.narrator, self.other], "base") + self.assertEqual(list(result), ["narrator", "other"]) + self.assertEqual(result["narrator"], "First.") + + def test_failed_transcription_keeps_empty_string(self): + with patch.object(make_server, "transcribe_reference_audio", + return_value=None): + result = make_server.transcribe_wav_dir([self.narrator], "base") + self.assertEqual(result["narrator"], "") + + def test_whisper_model_name_passed_through(self): + with patch.object(make_server, "transcribe_reference_audio", + return_value="text") as mock_transcribe: + make_server.transcribe_wav_dir([self.narrator], "large-v3") + self.assertEqual(mock_transcribe.call_args.kwargs["model_name"], + "large-v3") + + def test_write_prompt_text_format(self): + path = make_server.write_prompt_text( + self.folder, {"narrator": "Hello.", "other": "World."}) + self.assertEqual(path, self.folder / make_server.PROMPT_TEXT_FILENAME) + text = path.read_text(encoding="utf-8") + self.assertIn("narrator|Hello.", text) + self.assertIn("other|World.", text) + + +class DesignPackageTests(unittest.TestCase): + """Voice-design package detection.""" + + def test_detects_voicedesign_in_id(self): + self.assertTrue(make_server.is_design_package( + {"id": "qwen3_tts_1_7b_voicedesign_q8_0"})) + + def test_detects_voicedesign_in_directory(self): + self.assertTrue(make_server.is_design_package( + {"target_directory": "Foo-VoiceDesign-GGUF"})) + + def test_detects_separated_voice_design(self): + self.assertTrue(make_server.is_design_package( + {"display_name": "Voice Design Q8_0"})) + + def test_ignores_other_packages(self): + self.assertFalse(make_server.is_design_package( + {"id": "higgs_audio_tts_4b_q8_0"})) + self.assertFalse(make_server.is_design_package({})) + + +class PackageDirOptionsTests(unittest.TestCase): + """Grouping a family's packages into distinct target directories.""" + + def test_groups_precisions_and_marks_recommended(self): + entry = { + "family": "qwen3_tts", + "packages": [ + {"id": "base_q8", "default": True, "format": "gguf", + "target_directory": "Base-GGUF"}, + {"id": "base_bf16", "format": "gguf", + "target_directory": "Base-GGUF"}, + {"id": "voicedesign_q8", "format": "gguf", + "target_directory": "VoiceDesign-GGUF"}, + ], + } + options = make_server.package_dir_options(entry) + self.assertEqual([o["target_directory"] for o in options], + ["Base-GGUF", "VoiceDesign-GGUF"]) + self.assertTrue(options[0]["recommended"]) + self.assertFalse(options[0]["design"]) + self.assertFalse(options[1]["recommended"]) + self.assertTrue(options[1]["design"]) + self.assertEqual(options[0]["install_id"], "base_q8") + + def test_recommended_comes_first_even_if_listed_later(self): + entry = { + "family": "demo_tts", + "packages": [ + {"id": "demo_other", "format": "gguf", + "target_directory": "Other-GGUF"}, + {"id": "demo_default", "default": True, "format": "gguf", + "target_directory": "Default-GGUF"}, + ], + } + options = make_server.package_dir_options(entry) + self.assertEqual([o["target_directory"] for o in options], + ["Default-GGUF", "Other-GGUF"]) + + +class FindAudiocppServerBinTests(unittest.TestCase): + """Locating the built audiocpp_server binary.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.checkout = Path(self._td.name) / "audio.cpp" + self.checkout.mkdir() + + def tearDown(self): + self._td.cleanup() + + def _build(self, name, binary="audiocpp_server"): + bin_dir = self.checkout / "build" / name / "bin" + bin_dir.mkdir(parents=True) + (bin_dir / binary).write_bytes(b"x") + + def test_no_build_dir_returns_none(self): + self.assertIsNone(make_server.find_audiocpp_server_bin(self.checkout)) + + def test_finds_built_binary(self): + self._build("linux-cuda-release") + self.assertEqual( + make_server.find_audiocpp_server_bin(self.checkout), + self.checkout / "build" / "linux-cuda-release" / "bin" + / "audiocpp_server") + + def test_finds_windows_exe(self): + self._build("windows-vulkan-debug", binary="audiocpp_server.exe") + self.assertEqual( + make_server.find_audiocpp_server_bin(self.checkout).name, + "audiocpp_server.exe") + + def test_build_dir_without_binary_returns_none(self): + (self.checkout / "build" / "linux-cuda-release" / "bin").mkdir( + parents=True) + self.assertIsNone(make_server.find_audiocpp_server_bin(self.checkout)) + + +class BuildAudiocppTests(unittest.TestCase): + """Running the audio.cpp build helper script.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.checkout = Path(self._td.name) / "audio.cpp" + self.checkout.mkdir() + self.scripts = self.checkout / "scripts" + self.scripts.mkdir() + (self.scripts / "build_linux.sh").write_text("#!/bin/sh\n", + encoding="utf-8") + + def tearDown(self): + self._td.cleanup() + + def test_runs_build_script_with_backend_and_target(self): + with patch.object(make_server.common, "run_console_subprocess", + return_value=0) as run: + rc = make_server.build_audiocpp(self.checkout, "cuda") + self.assertEqual(rc, 0) + argv = run.call_args[0][0] + self.assertEqual(argv[:3], ["sh", str(self.scripts / "build_linux.sh"), + "--backend"]) + self.assertIn("cuda", argv) + self.assertIn("--target", argv) + self.assertIn("audiocpp_server", argv) + self.assertEqual(run.call_args[1]["cwd"], self.checkout) + + def test_missing_script_returns_nonzero(self): + for f in self.scripts.iterdir(): + f.unlink() + rc = make_server.build_audiocpp(self.checkout, "cuda") + self.assertNotEqual(rc, 0) + + +class AudiocppDetectTests(unittest.TestCase): + """backends.audiocpp.detect() status reporting.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.root = Path(self._td.name) + self.checkout = _make_checkout(self.root) + + def tearDown(self): + self._td.cleanup() + + def test_not_cloned(self): + with patch.object(make_server, "find_local_checkout", return_value=None): + status = make_server.detect() + self.assertFalse(status.installed) + self.assertFalse(status.configured) + self.assertIn("not cloned", status.details[0]) + + def test_cloned_not_built_not_configured(self): + with patch.object(make_server, "find_local_checkout", + return_value=self.checkout), \ + patch.object(make_server, "find_audiocpp_server_bin", + return_value=None): + status = make_server.detect() + self.assertFalse(status.installed) + self.assertFalse(status.configured) + self.assertEqual(status.launch_hint, "") + + def test_built_and_configured_ready(self): + binary = self.checkout / "build" / "linux-cuda-release" / "bin" \ + / "audiocpp_server" + binary.parent.mkdir(parents=True) + binary.write_bytes(b"x") + server_json = self.checkout / "server.json" + server_json.write_text('{"models":[]}', encoding="utf-8") + with patch.object(make_server, "find_local_checkout", + return_value=self.checkout): + status = make_server.detect() + self.assertTrue(status.installed) + self.assertTrue(status.configured) + self.assertIn(str(binary), status.launch_hint) + self.assertIn(str(server_json), status.launch_hint) + + +class NonInteractiveMainTests(unittest.TestCase): + """The flag-only (non-TUI) path through main(), end to end.""" + + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.root = Path(self._td.name) + self.folder = self.root / "wavs" + self.folder.mkdir() + self.output = self.root / "server.json" + self.checkout = _make_checkout(self.root) + # Isolate config.py rewrites so no test touches the real one. + self.fake_config = self.root / "config.py" + self.fake_config.write_text(FAKE_CONFIG, encoding="utf-8") + patcher = patch.object(make_server, "CONFIG_PATH", self.fake_config) + patcher.start() + self.addCleanup(patcher.stop) + # Tests run without a tty -> main() takes the non-interactive path. + patcher = patch.object(make_server, "_interactive", return_value=False) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + self._td.cleanup() + + def _run(self, argv, transcribe=None, whisper="faster_whisper"): + argv = ["backends/audiocpp.py"] + argv + transcribe_effect = transcribe if transcribe is not None \ + else MagicMock() + with patch.object(sys, "argv", argv), \ + patch.object(make_server, "transcribe_reference_audio", + side_effect=transcribe_effect), \ + patch.object(make_server, "whisper_backend_available", + return_value=whisper): + return make_server.main() + + def _args(self, *extra): + return ["--wavs", str(self.folder), "--output", str(self.output), + "--audiocpp-dir", str(self.checkout)] + list(extra) + + def test_default_run_hosts_recommended_entry(self): + exit_code = self._run( + self._args("--families", "higgs_audio_tts", "--no-sync-model-ids")) + self.assertEqual(exit_code, 0) + data = json.loads(self.output.read_text(encoding="utf-8")) + self.assertEqual(data["host"], "127.0.0.1") + self.assertEqual(data["port"], make_server.config_port()) + self.assertEqual(data["backend"], "cuda") + self.assertFalse(data["lazy_load"]) + self.assertEqual([m["id"] for m in data["models"]], ["higgs"]) + self.assertNotIn("voice_dir", data) + + def test_port_sync_accepted_updates_config(self): + with patch.object(config, "AUDIOCPP_API_URL", + "http://127.0.0.1:9999"): + exit_code = self._run( + self._args("--families", "higgs_audio_tts", "--port", "8080", + "--no-sync-model-ids")) + self.assertEqual(exit_code, 0) + self.assertIn('"http://127.0.0.1:8080"', + self.fake_config.read_text(encoding="utf-8")) + data = json.loads(self.output.read_text(encoding="utf-8")) + self.assertEqual(data["port"], 8080) + + def test_port_sync_declined_keeps_config(self): + with patch.object(config, "AUDIOCPP_API_URL", + "http://127.0.0.1:9999"): + exit_code = self._run( + self._args("--families", "higgs_audio_tts", "--port", "8080", + "--no-sync-port", "--no-sync-model-ids")) + self.assertEqual(exit_code, 0) + self.assertIn('"http://127.0.0.1:9999"', + self.fake_config.read_text(encoding="utf-8")) + + def test_model_id_sync_accepted_updates_config(self): + self.fake_config.write_text(FAKE_CONFIG_WITH_MODEL_IDS, + encoding="utf-8") + exit_code = self._run(self._args("--families", "higgs_audio_tts")) + self.assertEqual(exit_code, 0) + text = self.fake_config.read_text(encoding="utf-8") + self.assertIn('AUDIOCPP_MODEL_ID = "higgs"', text) + self.assertIn('AUDIOCPP_CLONE_MODEL_ID = "higgs"', text) + + def test_multi_family_lazy_with_voice_dir(self): + (self.folder / "narrator.wav").write_bytes(b"x") + exit_code = self._run( + self._args("--families", "qwen3_tts,higgs_audio_tts", + "--no-sync-model-ids"), + transcribe=lambda path, model_name="base": "a transcript") + self.assertEqual(exit_code, 0) + data = json.loads(self.output.read_text(encoding="utf-8")) + self.assertEqual([m["id"] for m in data["models"]], ["qwen", "higgs"]) + self.assertTrue(data["lazy_load"]) + self.assertEqual(data["voice_dir"], str(self.folder.resolve())) + prompt = (self.folder / make_server.PROMPT_TEXT_FILENAME).read_text( + encoding="utf-8") + self.assertIn("narrator|a transcript", prompt) + + def test_force_overwrites_existing_output(self): + self.output.write_text('{"old": true}', encoding="utf-8") + exit_code = self._run( + self._args("--families", "higgs_audio_tts", "--force", + "--no-sync-model-ids")) + self.assertEqual(exit_code, 0) + data = json.loads(self.output.read_text(encoding="utf-8")) + self.assertEqual(len(data["models"]), 1) + + def test_existing_output_declined_keeps_file(self): + self.output.write_text('{"old": true}', encoding="utf-8") + exit_code = self._run( + self._args("--families", "higgs_audio_tts", "--no-sync-model-ids")) + self.assertEqual(exit_code, 1) + self.assertEqual(json.loads(self.output.read_text(encoding="utf-8")), + {"old": True}) + + def test_all_packages_hosts_design_as_vdes(self): + exit_code = self._run( + self._args("--families", "qwen3_tts", "--all-packages", + "--no-sync-model-ids")) + self.assertEqual(exit_code, 0) + data = json.loads(self.output.read_text(encoding="utf-8")) + by_id = {m["id"]: m for m in data["models"]} + self.assertIn("qwen-design", by_id) + self.assertEqual(by_id["qwen-design"]["task"], "vdes") + # The non-design packages are hosted with task "tts". + self.assertTrue(any(m["id"] in ("qwen", "qwen-2") and m["task"] == "tts" + for m in data["models"])) + + def test_unknown_family_rejected(self): + with self.assertRaises(SystemExit) as ctx: + self._run(self._args("--families", "not_a_family", + "--no-sync-model-ids")) + self.assertEqual(ctx.exception.code, 2) + + def test_missing_checkout_rejected(self): + with patch.object(make_server, "find_local_checkout", + return_value=None), \ + self.assertRaises(SystemExit) as ctx: + self._run(["--families", "higgs_audio_tts", "--output", + str(self.output), "--no-sync-model-ids"]) + self.assertEqual(ctx.exception.code, 2) + + def test_missing_wav_dir_rejected(self): + missing = self.root / "nope" + with self.assertRaises(SystemExit) as ctx: + self._run(["--wavs", str(missing), "--output", str(self.output), + "--audiocpp-dir", str(self.checkout), + "--families", "higgs_audio_tts", "--no-sync-model-ids"]) + self.assertEqual(ctx.exception.code, 2) + + def test_families_required_in_noninteractive_run(self): + with self.assertRaises(SystemExit) as ctx: + self._run(self._args("--no-sync-model-ids")) + self.assertEqual(ctx.exception.code, 2) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3