aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp
diff options
context:
space:
mode:
Diffstat (limited to 'app/backends/audiocpp')
-rw-r--r--app/backends/audiocpp/__init__.py11
-rw-r--r--app/backends/audiocpp/catalog.py75
-rw-r--r--app/backends/audiocpp/constants.py3
-rw-r--r--app/backends/audiocpp/models.py12
-rw-r--r--app/backends/audiocpp/wizard.py7
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: