aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp/catalog.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-30 20:42:02 -0400
committerhistoria <historiavg@proton.me>2026-08-30 20:42:02 -0400
commita0e3050c6e1e43df3941077afa4ade9a1c4d6ce4 (patch)
treed8492bbcbbf6850afc127bae862abe68e1198c0c /app/backends/audiocpp/catalog.py
parent93f106aac2d6411c80a911adac62cd12f80e58be (diff)
downloadtts-audiobook-generator-a0e3050c6e1e43df3941077afa4ade9a1c4d6ce4.tar.gz
fix: non-clone models correctly supported in tui, restart server when needed
Diffstat (limited to 'app/backends/audiocpp/catalog.py')
-rw-r--r--app/backends/audiocpp/catalog.py75
1 files changed, 74 insertions, 1 deletions
diff --git a/app/backends/audiocpp/catalog.py b/app/backends/audiocpp/catalog.py
index 65b525b..78908a3 100644
--- a/app/backends/audiocpp/catalog.py
+++ b/app/backends/audiocpp/catalog.py
@@ -6,7 +6,9 @@ import sys
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
-from .constants import TASK_TTS
+from converter.clients import AUDIOCPP_CLONE_ONLY_FAMILIES, audiocpp_family_spec_tasks
+
+from .constants import TASK_CLON, TASK_TTS
DESIGN_PACKAGE_RE = re.compile(r"voice[\s_\-]?design", re.IGNORECASE)
@@ -220,6 +222,77 @@ def is_design_package(package: dict) -> bool:
return bool(DESIGN_PACKAGE_RE.search(text))
+def is_clone_only_family(family: str, tasks: Optional[Set[str]] = None
+ ) -> bool:
+ """True when FAMILY's audio.cpp implementation rejects plain TTS.
+
+ Such families can only synthesize by cloning a reference voice, so
+ their server entries must be hosted with task "clon" — hosting them
+ with "tts" fails every speech request at session-creation time.
+ Families are classified from the explicit known-clone-only set
+ (AUDIOCPP_CLONE_ONLY_FAMILIES, which also covers specs that wrongly
+ claim "tts" — Chatterbox) or from a spec task list that names only
+ "clone" (TASKS, when the caller has it; without one the specs are
+ read best-effort).
+ """
+ if family in AUDIOCPP_CLONE_ONLY_FAMILIES:
+ return True
+ if tasks is None:
+ tasks = audiocpp_family_spec_tasks(family)
+ return bool(tasks) and set(tasks) == {"clone"}
+
+
+def hosting_task(entry: dict) -> str:
+ """The server.json task a family's non-design packages are hosted with.
+
+ Clone-only families (see ``is_clone_only_family``) get "clon" so their
+ cloning sessions can be created at all; every other family keeps
+ "tts", which serves plain TTS and — where the family supports it —
+ cloning through the request's voice field alike.
+ """
+ if is_clone_only_family(str(entry.get("family") or ""),
+ tasks=set(entry.get("tasks") or []) or None):
+ return TASK_CLON
+ return TASK_TTS
+
+
+def rehost_clone_only_entries(server_json: Path, data: dict) -> List[str]:
+ """Re-host "tts"-tasked clone-only entries in DATA as "clon", in place.
+
+ Server.json files written before clone-only hosting existed carry
+ task "tts" for families whose audio.cpp implementation rejects plain
+ TTS sessions (e.g. Chatterbox), so every speech request fails with
+ HTTP 500. Each such entry's task is rewritten to "clon"; when anything
+ changed, the document is written back to SERVER_JSON (same layout the
+ wizard writes). Returns the repaired entries' ids, in order — empty
+ when nothing needed changing (or the document is unusable).
+ """
+ models = data.get("models")
+ if not isinstance(models, list):
+ return []
+ repaired: List[str] = []
+ for entry in models:
+ if not isinstance(entry, dict):
+ continue
+ if str(entry.get("task") or "") != TASK_TTS:
+ continue
+ family = str(entry.get("family") or "")
+ if not family or not is_clone_only_family(family):
+ continue
+ entry["task"] = TASK_CLON
+ repaired.append(str(entry.get("id") or family))
+ if repaired:
+ try:
+ with server_json.open("w", encoding="utf-8") as handle:
+ json.dump(data, handle, indent=2, ensure_ascii=False)
+ handle.write("\n")
+ except OSError:
+ # The in-memory document is fixed either way; a failed write
+ # only means the fix does not survive the process.
+ pass
+ return repaired
+
+
def package_dir_options(entry: dict) -> List[dict]:
"""Return one option per distinct target_directory of a family's packages.