diff options
| author | historia <historiavg@proton.me> | 2026-08-25 13:28:42 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-25 13:28:42 -0400 |
| commit | d4dbc1a158d1dd6babcba7333a4ed7d719b65d3e (patch) | |
| tree | ccba2efaef9a265f7cbc754cf70d8071d09e9612 | |
| parent | 0cc01d1da0a629e104202053feb0bb0db91d578d (diff) | |
| download | tts-audiobook-generator-d4dbc1a158d1dd6babcba7333a4ed7d719b65d3e.tar.gz | |
feat: automatically name audio.cpp model ids with long name
| -rwxr-xr-x | app/backends/audiocpp.py | 73 | ||||
| -rw-r--r-- | app/converter/config.py | 27 | ||||
| -rw-r--r-- | app/docs/backend-audiocpp.md | 16 | ||||
| -rw-r--r-- | app/tests/test_backends_audiocpp.py | 38 | ||||
| -rw-r--r-- | app/tests/test_tts.py | 17 | ||||
| -rw-r--r-- | app/ui/hub.py | 7 | ||||
| -rwxr-xr-x | audiobook.py | 2 |
7 files changed, 77 insertions, 103 deletions
diff --git a/app/backends/audiocpp.py b/app/backends/audiocpp.py index f95216e..0b82a4e 100755 --- a/app/backends/audiocpp.py +++ b/app/backends/audiocpp.py @@ -123,16 +123,6 @@ class _GoBack(Exception): # Package names that mark a voice-design model (hosted with task "vdes"). DESIGN_PACKAGE_RE = re.compile(r"voice[\s_\-]?design", re.IGNORECASE) -# Short, friendly default entry ids for selected families. Other families -# derive an id from their family name (see default_model_id). All families -# are listed equally, in alphabetical order. -PREFERRED_IDS = { - "qwen3_tts": "qwen", - "higgs_audio_tts": "higgs", - "voxcpm2": "voxcpm2", - "index_tts2": "indextts2", -} - class _TuiError(Exception): """A fatal error raised from inside the TUI wizard. @@ -305,16 +295,6 @@ def update_config_model_ids(model_id: str, return True -def default_model_id(family: str) -> str: - """Derive a default server entry id from a family name.""" - if family in PREFERRED_IDS: - return PREFERRED_IDS[family] - name = family - if name.endswith("_tts"): - name = name[:-4] - return name.replace("_", "") or family - - def detect_audiocpp_dir() -> Optional[Path]: """Best-effort location of a local audio.cpp checkout with model_specs. @@ -411,9 +391,9 @@ def load_model_catalog(audiocpp_dir: Path) -> List[dict]: Each returned entry has: family, display_name, description, languages, clone_capable, packages (the full list from the spec), install_id - (recommended package id), default_path (``models/<target_directory>``), - and preferred_id. All families are treated equally and listed in - alphabetical order by display name. + (recommended package id), and default_path (``models/<target_directory>``). + All families are treated equally and listed in alphabetical order by + display name. """ specs_dir = audiocpp_dir / "model_specs" if not specs_dir.is_dir(): @@ -449,7 +429,6 @@ def load_model_catalog(audiocpp_dir: Path) -> List[dict]: "packages": packages, "install_id": package.get("id") or family, "default_path": f"models/{target_directory}", - "preferred_id": default_model_id(family), }) # All families are treated equally: alphabetical by display name. @@ -716,17 +695,18 @@ def _offer_config_model_id_sync(model_id: str, accepted: Optional[bool]) -> None def _build_entries(family_keys: List[str], chosen: Dict[str, List[dict]], catalog_by_family: Dict[str, dict], task_picker: Callable[[str], str], - id_picker: Callable[[str, str, str], str], known_tasks: Optional[Dict[Tuple[str, str], str]] = None ) -> Tuple[List[dict], List[str], List[Tuple[str, str]], List[str], bool]: """Build server.json model entries from the selected families/packages. - TASK_PICKER is called for each design package to choose vdes/tts; - ID_PICKER resolves a duplicate server entry id. KNOWN_TASKS maps - ``(family, target_directory)`` to a previously-stored task ("tts" or - "vdes") so a modify run preserves how a design package was hosted - instead of re-asking. Returns (model_entries, entry_ids, + TASK_PICKER is called for each design package to choose vdes/tts. + KNOWN_TASKS maps ``(family, target_directory)`` to a previously-stored + task ("tts" or "vdes") so a modify run preserves how a design package + was hosted instead of re-asking. Each entry's server id is its package + ``target_directory`` (flattened to a token), so packages from the same + family never collide; an id that does collide (across families) is + auto-suffixed without prompting. Returns (model_entries, entry_ids, install_guidance, design_entry_ids, include_clone). """ model_entries: List[dict] = [] @@ -745,12 +725,13 @@ def _build_entries(family_keys: List[str], chosen: Dict[str, List[dict]], task = task_picker(opt["install_id"]) else: task = TASK_TTS - base_id = (f"{entry['preferred_id']}-design" - if task == TASK_VDES else entry["preferred_id"]) + base_id = opt["target_directory"].replace("/", "-") model_id = base_id if model_id in entry_ids: - model_id = id_picker(entry["display_name"], opt["install_id"], - f"{base_id}-2") + n = 2 + while f"{base_id}-{n}" in entry_ids: + n += 1 + model_id = f"{base_id}-{n}" entry_ids.append(model_id) model_entries.append(build_model_entry( family, model_id, f"models/{opt['target_directory']}", @@ -1014,9 +995,8 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser s["family_keys"] = family_keys def _compute_entries() -> None: - # Design task menus and duplicate-id renames. Esc on any of them - # raises _GoBack, which the caller turns into Wizard.BACK (the - # design/duplicate-id prompts are grouped: Esc returns to the + # Design task menu. Esc raises _GoBack, which the caller turns into + # Wizard.BACK (the design prompts are grouped: Esc returns to the # families tree). def task_picker(install_id: str) -> str: result = tui.menu( @@ -1031,20 +1011,10 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser raise _GoBack() return result - def id_picker(display_name: str, install_id: str, - default: str) -> str: - result = tui.line_edit( - stdscr, - f"Server model id for {display_name} package " - f"'{install_id}'", default, back_value=_GO_BACK) - if result is _GO_BACK: - raise _GoBack() - return result - model_entries, entry_ids, install_guidance, \ design_entry_ids, include_clone = _build_entries( s["family_keys"], s["chosen"], s["catalog_by_family"], - task_picker, id_picker, known_tasks=s["existing_tasks"]) + task_picker, known_tasks=s["existing_tasks"]) s.update({ "model_entries": model_entries, "entry_ids": entry_ids, @@ -2373,16 +2343,13 @@ def _collect_from_flags(args: argparse.Namespace, else: chosen[family] = [opt for opt in opts if opt["recommended"]] - # Non-interactive pickers: design packages default to vdes, dup ids get -2. + # Non-interactive picker: design packages default to vdes. def task_picker(install_id: str) -> str: return TASK_VDES - def id_picker(display_name: str, install_id: str, default: str) -> str: - return default - model_entries, entry_ids, install_guidance, design_entry_ids, include_clone = \ _build_entries(family_keys, chosen, catalog_by_family, - task_picker, id_picker) + task_picker) # Server settings. host = args.host or DEFAULT_HOST diff --git a/app/converter/config.py b/app/converter/config.py index 6eacb41..3caa1ab 100644 --- a/app/converter/config.py +++ b/app/converter/config.py @@ -63,19 +63,20 @@ AUDIOCPP_API_URL = "http://127.0.0.1:8080" # audio.cpp audiocpp_server AUDIOCPP_REMOTE_URL = "http://127.0.0.1:8080" # externally-run audiocpp_server ("" disables probing) # Model ids in the audio.cpp server.json config. AUDIOCPP_MODEL_ID may point -# at any TTS model entry the server hosts (qwen3_tts, higgs_audio_tts, -# voxcpm2, index_tts2, ...); the family is detected from the server at -# startup and adapts the request automatically. Only qwen3_tts has built-in -# speakers (speaker mode); every other family needs --voice with a -# server-side voice preset. For single-model servers, set -# AUDIOCPP_CLONE_MODEL_ID to the same id as AUDIOCPP_MODEL_ID (or leave it -# empty); for Qwen3-TTS it typically names a second entry with the Base -# (cloning) model. A multi-model server (one server.json hosting several -# lazily-loaded entries) does not need editing here: leave AUDIOCPP_MODEL_ID -# unset to auto-select when only one entry is hosted, or pick the entry per -# run with the --model CLI flag. -AUDIOCPP_MODEL_ID = "qwen" -AUDIOCPP_CLONE_MODEL_ID = "qwen" +# at any TTS model entry the server hosts; the family is detected from the +# server at startup and adapts the request automatically. Only qwen3_tts has +# built-in speakers (speaker mode); every other family needs --voice with a +# server-side voice preset. The server entry id is the model package's +# target_directory name (e.g. "Qwen3-TTS-12Hz-1.7B-Base-GGUF"). For +# single-model servers, set AUDIOCPP_CLONE_MODEL_ID to the same id as +# AUDIOCPP_MODEL_ID (or leave it empty); for Qwen3-TTS it typically names a +# second entry with the Base (cloning) model. Both default to empty so a +# single-entry server is auto-selected; a multi-model server (one server.json +# hosting several lazily-loaded entries) needs no editing here either: leave +# AUDIOCPP_MODEL_ID unset to auto-select when only one entry is hosted, or +# pick the entry per run with the --model CLI flag. +AUDIOCPP_MODEL_ID = "" +AUDIOCPP_CLONE_MODEL_ID = "" # Voice design / style instruction sent with every audio.cpp request when # the --instructions CLI flag is not given. Required for server entries diff --git a/app/docs/backend-audiocpp.md b/app/docs/backend-audiocpp.md index c9ef883..3323887 100644 --- a/app/docs/backend-audiocpp.md +++ b/app/docs/backend-audiocpp.md @@ -32,7 +32,7 @@ You can run `python tools/model_manager_v2.py list` to see all available models. ### Create server.json -Create a `server.json` config file. One server can host multiple models and multiple cloned voices. The `id:` fields are the model names you will set for `tts-audiobook-generator` with `--model`. +Create a `server.json` config file. One server can host multiple models and multiple cloned voices. The `id:` fields are the model names you will set for `tts-audiobook-generator` with `--model` (the setup wizard names each entry after its model package directory). ```json { @@ -43,21 +43,21 @@ Create a `server.json` config file. One server can host multiple models and mult "voice_dir": "/path/to/clone/wavs", "models": [ { - "id": "higgs", + "id": "Higgs-Audio-v3-TTS-4B-GGUF", "family": "higgs_audio_tts", "path": "models/Higgs-Audio-v3-TTS-4B-GGUF", "task": "tts", "mode": "offline" }, { - "id": "qwen", + "id": "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF", "task": "tts", "mode": "offline" }, { - "id": "qwen-clone", + "id": "Qwen3-TTS-12Hz-1.7B-Base-GGUF", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-Base-GGUF", "task": "tts", @@ -79,16 +79,16 @@ In a different terminal, run `audiobook.py`. Pick the TTS `--model` and `--voice ```bash # Higgs Audio (clone-only) -python audiobook.py --backend audiocpp --model higgs --voice narrator +python audiobook.py --backend audiocpp --model Higgs-Audio-v3-TTS-4B-GGUF --voice narrator # Qwen3-TTS built-in speaker -python audiobook.py --backend audiocpp --model qwen +python audiobook.py --backend audiocpp --model Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF # Qwen3-TTS voice cloning -python audiobook.py --backend audiocpp --model qwen-clone --voice narrator +python audiobook.py --backend audiocpp --model Qwen3-TTS-12Hz-1.7B-Base-GGUF --voice narrator # Qwen-TTS voice design -python audiobook.py --backend audiocpp --model qwen-design \ +python audiobook.py --backend audiocpp --model Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF \ --instructions "A warm adult female narrator with a British accent" ``` diff --git a/app/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py index 2f41131..be8fead 100644 --- a/app/tests/test_backends_audiocpp.py +++ b/app/tests/test_backends_audiocpp.py @@ -371,20 +371,6 @@ class NormalizeDirArgTests(unittest.TestCase): self.assertEqual(result, Path("/tmp/foo").resolve()) -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() @@ -1067,7 +1053,8 @@ class NonInteractiveMainTests(unittest.TestCase): self.assertEqual(data["port"], make_server.config_port()) self.assertEqual(data["backend"], "cuda") self.assertTrue(data["lazy_load"]) - self.assertEqual([m["id"] for m in data["models"]], ["higgs"]) + self.assertEqual([m["id"] for m in data["models"]], + ["Higgs-Audio-v3-TTS-4B-GGUF"]) self.assertNotIn("voice_dir", data) def test_port_sync_accepted_updates_config(self): @@ -1098,8 +1085,9 @@ class NonInteractiveMainTests(unittest.TestCase): 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) + self.assertIn('AUDIOCPP_MODEL_ID = "Higgs-Audio-v3-TTS-4B-GGUF"', text) + self.assertIn('AUDIOCPP_CLONE_MODEL_ID = "Higgs-Audio-v3-TTS-4B-GGUF"', + text) def test_multi_family_lazy_with_voice_dir(self): (self.folder / "narrator.wav").write_bytes(b"x") @@ -1109,7 +1097,9 @@ class NonInteractiveMainTests(unittest.TestCase): 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.assertEqual([m["id"] for m in data["models"]], + ["Qwen3-TTS-12Hz-1.7B-Base-GGUF", + "Higgs-Audio-v3-TTS-4B-GGUF"]) self.assertTrue(data["lazy_load"]) self.assertEqual(data["voice_dir"], str(self.folder.resolve())) prompt = (self.folder / make_server.PROMPT_TEXT_FILENAME).read_text( @@ -1140,11 +1130,13 @@ class NonInteractiveMainTests(unittest.TestCase): 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") + self.assertIn("Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF", by_id) + self.assertEqual(by_id["Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF"]["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"])) + self.assertEqual(by_id["Qwen3-TTS-12Hz-1.7B-Base-GGUF"]["task"], "tts") + self.assertEqual(by_id["Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF"]["task"], + "tts") def test_unknown_family_rejected(self): with self.assertRaises(SystemExit) as ctx: @@ -1765,7 +1757,7 @@ class WizardNavigationTests(unittest.TestCase): self.assertEqual(len(tree_calls), 2) self.assertEqual(settings["host"], "127.0.0.1") self.assertEqual([m["id"] for m in settings["model_entries"]], - ["supertonic"]) + ["Supertonic-GGUF"]) class UninstallTests(unittest.TestCase): diff --git a/app/tests/test_tts.py b/app/tests/test_tts.py index 77f0ee8..b43919d 100644 --- a/app/tests/test_tts.py +++ b/app/tests/test_tts.py @@ -477,6 +477,13 @@ class QwenTTSClientGenerateTests(unittest.TestCase): class AudioCppTTSClientHealthTests(unittest.TestCase): """Connection behavior of the audio.cpp client.""" + def setUp(self): + # The default AUDIOCPP_MODEL_ID is empty (auto-select); these tests + # exercise a configured single-model server, so pin a concrete id. + patcher = patch.object(config, "AUDIOCPP_MODEL_ID", "qwen") + patcher.start() + self.addCleanup(patcher.stop) + @staticmethod def _json_response(payload): response = MagicMock() @@ -670,6 +677,11 @@ class AudioCppTTSClientHealthTests(unittest.TestCase): class AudioCppTaskDetectionTests(unittest.TestCase): """Task auto-detection (tts/clon/vdes) and voice design validation.""" + def setUp(self): + patcher = patch.object(config, "AUDIOCPP_MODEL_ID", "qwen") + patcher.start() + self.addCleanup(patcher.stop) + @staticmethod def _json_response(payload): response = MagicMock() @@ -808,6 +820,11 @@ class AudioCppTaskDetectionTests(unittest.TestCase): class AudioCppFamilyDetectionTests(unittest.TestCase): """Family auto-detection and per-family adaptations.""" + def setUp(self): + patcher = patch.object(config, "AUDIOCPP_MODEL_ID", "qwen") + patcher.start() + self.addCleanup(patcher.stop) + @staticmethod def _json_response(payload): response = MagicMock() diff --git a/app/ui/hub.py b/app/ui/hub.py index 6862b0a..285a1cf 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -1052,8 +1052,7 @@ def _settings_fields() -> list: "value": str(config.CHUNK_SIZE), "validate": _validate_chunk_size}, {"key": "unload_models", "label": "Unload models", "kind": "bool", "value": config.AUDIOCPP_UNLOAD_MODELS, - "note": "audio.cpp only. Ask the server to unload resident models " - "before converting."}, + "note": "audio.cpp: Unload previously-loaded models before converting to prevent VRAM exhaustion."}, {"key": "audiocpp_port", "label": "audio.cpp port", "kind": "text", "value": str(_port_from_url(config.AUDIOCPP_API_URL, 8080)), @@ -1075,9 +1074,7 @@ def _settings_fields() -> list: "kind": "text", "value": config.AUDIOCPP_REMOTE_URL, "validate": _validate_remote_url, - "note": "Remote (externally-run) servers. The hub probes each URL and " - "offers a \"[remote]\" backend entry when one answers. " - "Empty disables probing."}, + "note": "Remote (externally-run) servers. Empty disables probing."}, {"key": "faster_remote_url", "label": "faster-qwen3-tts remote URL", "kind": "text", "value": config.FASTER_REMOTE_URL, diff --git a/audiobook.py b/audiobook.py index 9a0da29..9bdad19 100755 --- a/audiobook.py +++ b/audiobook.py @@ -169,7 +169,7 @@ Examples: python audiobook.py --backend audiocpp --voice narrator # Use the audio.cpp audiocpp_server with a voice design model (task 'vdes') - python audiobook.py --backend audiocpp --model qwen-design \\ + python audiobook.py --backend audiocpp --model Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF \\ --instructions "A warm adult female narrator with a British accent" # Use the qwen-tts demo server with a custom voice |
