diff options
Diffstat (limited to 'app/backends')
| -rw-r--r-- | app/backends/audiocpp/__init__.py | 11 | ||||
| -rw-r--r-- | app/backends/audiocpp/catalog.py | 75 | ||||
| -rw-r--r-- | app/backends/audiocpp/constants.py | 3 | ||||
| -rw-r--r-- | app/backends/audiocpp/models.py | 12 | ||||
| -rw-r--r-- | app/backends/audiocpp/wizard.py | 7 |
5 files changed, 99 insertions, 9 deletions
diff --git a/app/backends/audiocpp/__init__.py b/app/backends/audiocpp/__init__.py index d65c36b..64122c9 100644 --- a/app/backends/audiocpp/__init__.py +++ b/app/backends/audiocpp/__init__.py @@ -24,18 +24,22 @@ from .constants import ( DEFAULT_HOST, FALLBACK_PORT, PATCH_DIR, + TASK_CLON, TASK_TTS, TASK_VDES, ) from .catalog import ( detect_backend, + is_clone_only_family, is_design_package, + hosting_task, load_model_catalog, package_dir_options, build_model_entry, build_server_config, load_server_config, server_config_selections, + rehost_clone_only_entries, request_options_families, supports_request_options, ) @@ -89,11 +93,12 @@ from .status import detect __all__ = [ # constants "AUDIOCPP_DIR_NAME", "AUDIOCPP_GIT_URL", "BACKENDS", "DEFAULT_HOST", - "FALLBACK_PORT", "PATCH_DIR", "TASK_TTS", "TASK_VDES", + "FALLBACK_PORT", "PATCH_DIR", "TASK_TTS", "TASK_CLON", "TASK_VDES", # catalog "detect_backend", "load_model_catalog", "is_design_package", - "package_dir_options", "build_model_entry", "build_server_config", - "load_server_config", "server_config_selections", + "is_clone_only_family", "hosting_task", "package_dir_options", + "build_model_entry", "build_server_config", + "load_server_config", "server_config_selections", "rehost_clone_only_entries", "request_options_families", "supports_request_options", # models "missing_model_entries", "installed_model_entries", 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. diff --git a/app/backends/audiocpp/constants.py b/app/backends/audiocpp/constants.py index 957590b..017dd60 100644 --- a/app/backends/audiocpp/constants.py +++ b/app/backends/audiocpp/constants.py @@ -14,6 +14,9 @@ BACKENDS = ("cuda", "vulkan", "hip", "cpu") TASK_TTS = "tts" +TASK_CLON = "clon" + + TASK_VDES = "vdes" diff --git a/app/backends/audiocpp/models.py b/app/backends/audiocpp/models.py index f2c4e5e..d81dcfc 100644 --- a/app/backends/audiocpp/models.py +++ b/app/backends/audiocpp/models.py @@ -313,10 +313,16 @@ def _build_tree_families(catalog: List[dict]) -> List[dict]: """Shape the catalog into the checkbox_tree widget's family list.""" families: List[dict] = [] for entry in catalog: - capabilities = ["tts"] - if "clone" in entry["tasks"]: + tasks = set(entry["tasks"]) + # Clone-only families (e.g. Chatterbox) cannot synthesize without + # a reference voice, so they do not advertise plain "tts". + clone_only = _catalog.is_clone_only_family(entry["family"], tasks) + capabilities = [] + if not clone_only: + capabilities.append("tts") + if "clone" in tasks or clone_only: capabilities.append("cloning") - if "design" in entry["tasks"]: + if "design" in tasks: capabilities.append("design") name = entry["display_name"] options = [] diff --git a/app/backends/audiocpp/wizard.py b/app/backends/audiocpp/wizard.py index f35aac1..97f50a5 100644 --- a/app/backends/audiocpp/wizard.py +++ b/app/backends/audiocpp/wizard.py @@ -27,7 +27,7 @@ from . import models as _models from . import prebuilt as _prebuilt from . import voices as _voices from .catalog import (_backend_options, build_model_entry, - build_server_config, detect_backend, + build_server_config, detect_backend, hosting_task, load_model_catalog, load_server_config, package_dir_options, server_config_selections) from .constants import (AUDIOCPP_DIR_NAME, AUDIOCPP_GIT_URL, BACKENDS, @@ -107,7 +107,10 @@ def _build_entries(family_keys: List[str], chosen: Dict[str, List[dict]], if task is None: task = task_picker(opt["install_id"]) else: - task = TASK_TTS + # Clone-only families (Chatterbox, Confucius4-TTS, + # Echo-TTS) reject plain-TTS sessions, so they are hosted + # with task "clon"; everything else keeps "tts". + task = hosting_task(entry) base_id = opt["target_directory"].replace("/", "-") model_id = base_id if model_id in entry_ids: |
