aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 06:04:38 -0400
committerhistoria <historiavg@proton.me>2026-08-24 06:04:38 -0400
commit4db8ea7a63107297450819d227497ebbb121ff38 (patch)
tree250b646195f98a4b4c42b04589c0ca8951d13fbb /app/backends/audiocpp.py
parent4f3d16e9d532efd6a6bda283925efeb8c93b51c7 (diff)
downloadtts-audiobook-generator-4db8ea7a63107297450819d227497ebbb121ff38.tar.gz
fix: don't expect/use local config for remote servers
Diffstat (limited to 'app/backends/audiocpp.py')
-rwxr-xr-xapp/backends/audiocpp.py52
1 files changed, 52 insertions, 0 deletions
diff --git a/app/backends/audiocpp.py b/app/backends/audiocpp.py
index 673d344..a8c5568 100755
--- a/app/backends/audiocpp.py
+++ b/app/backends/audiocpp.py
@@ -35,6 +35,7 @@ import re
import subprocess
import sys
import urllib.parse
+import urllib.request
from pathlib import Path
from typing import Callable, Dict, List, Optional, Set, Tuple
@@ -1301,6 +1302,57 @@ def find_local_checkout() -> Optional[Path]:
return None
+def fetch_server_models(api_url: str) -> Optional[List[Dict[str, str]]]:
+ """List a running audiocpp_server's model entries via GET /v1/models.
+
+ Returns ``[{id, family, task}, ...]`` — the same shape the converter's
+ client resolves at startup — or None when URL does not answer with a
+ valid document (wrong server, still starting, older audio.cpp). Used by
+ the hub to drive the convert menus against a remote server that has no
+ local server.json describing it.
+ """
+ try:
+ with urllib.request.urlopen(
+ f"{api_url.rstrip('/')}/v1/models", timeout=10) as response:
+ payload = json.loads(response.read().decode("utf-8"))
+ except (OSError, ValueError):
+ # URLError/HTTPError/socket errors are OSErrors; a non-JSON body is
+ # a ValueError. Anything else means "not an audiocpp_server".
+ return None
+ entries = payload.get("data") if isinstance(payload, dict) else None
+ models: List[Dict[str, str]] = []
+ for entry in entries or []:
+ if isinstance(entry, dict) and entry.get("id"):
+ models.append({
+ "id": str(entry["id"]),
+ "family": str(entry.get("family") or ""),
+ "task": str(entry.get("task") or ""),
+ })
+ return models
+
+
+def fetch_server_voices(api_url: str, model_id: str) -> Optional[List[str]]:
+ """List a running audiocpp_server's voices for MODEL_ID.
+
+ Queries ``GET /v1/audio/voices?model=<id>`` — the endpoint the converter
+ validates ``--voice`` against — and returns its voice-name list, or None
+ when the server cannot be queried. Lets the hub offer a remote server's
+ voices without reading its configuration locally.
+ """
+ query = urllib.parse.urlencode({"model": model_id})
+ try:
+ with urllib.request.urlopen(
+ f"{api_url.rstrip('/')}/v1/audio/voices?{query}",
+ timeout=10) as response:
+ payload = json.loads(response.read().decode("utf-8"))
+ except (OSError, ValueError):
+ return None
+ voices = payload.get("voices") if isinstance(payload, dict) else None
+ if not isinstance(voices, list):
+ return None
+ return [str(voice) for voice in voices]
+
+
def find_audiocpp_server_bin(audiocpp_dir: Path) -> Optional[Path]:
"""Return the built audiocpp_server binary, or None when not built.