feat(voice): zentraler Stimmen-Store + Auto-Provisioning der f5tts-Flotte
Stimmen lagen bisher nur pro Box (xtts/voices/). Jetzt haelt der Diagnostic-
Server sie zentral als /shared/voices/{name}.tar.gz und versorgt jede f5tts-Box
automatisch.
- diagnostic/server.js (Bibliothekar):
* zentraler Store + Helfer (list/read/write/delete tar.gz)
* reconcileVoices() beim worker_hello einer f5tts-Box: push fehlende zentrale
Stimmen (xtts_import_voice, targetInstance) + pull box-eigene, zentral
fehlende (xtts_export_voice) → seedet den Store aus der ersten Box
* autonome RVS-Listener: xtts_voice_saved → zentraler Ingest (auch App-Uploads);
xtts_voice_exported (nur eigene requestId) → in Store schreiben;
xtts_delete_voice → zentrale Kopie mitloeschen
* Delete-Action loescht zentrale Kopie direkt mit
- xtts/f5tts/bridge.py: worker_hello traegt jetzt voices:[names] (ohne
default_ref) fuer den Abgleich; handle_export_voice echot requestId zurueck
(Korrelation zentral vs. browser-initiiert)
Wiederverwendet den vorhandenen export/import-tar.gz-Transport + Stage-3-
targetInstance-Filter. Playback (preview_voice) und Lastrouting (freieste Box)
existierten bereits. Keine App-/aria-bridge-Aenderung.
Deploy: diagnostic + f5tts-Boxen neu bauen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-4
@@ -675,6 +675,18 @@ async def handle_voice_upload(ws, payload: dict) -> None:
|
||||
await _send(ws, "xtts_voice_saved", {"name": name, "error": str(e)[:200]})
|
||||
|
||||
|
||||
def _local_voice_names() -> list:
|
||||
"""Namen der lokal vorhandenen Nutzer-Stimmen (ohne den Box-lokalen
|
||||
default_ref-Fallback) — fuer die Provisioning-Reconciliation im worker_hello."""
|
||||
names = []
|
||||
if VOICES_DIR.exists():
|
||||
for wav in sorted(VOICES_DIR.glob("*.wav")):
|
||||
if wav.stem == "default_ref":
|
||||
continue
|
||||
names.append(wav.stem)
|
||||
return names
|
||||
|
||||
|
||||
async def handle_list_voices(ws) -> None:
|
||||
try:
|
||||
voices = []
|
||||
@@ -711,13 +723,16 @@ async def handle_delete_voice(ws, payload: dict) -> None:
|
||||
async def handle_export_voice(ws, payload: dict) -> None:
|
||||
"""Packt eine Stimme (.wav + .txt) als tar.gz und sendet sie base64 zurueck."""
|
||||
name = (payload.get("name") or "").strip()
|
||||
# requestId aus dem Request zuruueckspiegeln — der Diagnostic-Bibliothekar
|
||||
# korreliert damit zentrale Exports gegen browser-initiierte.
|
||||
req_id = payload.get("requestId", "") or ""
|
||||
if not name:
|
||||
await _send(ws, "xtts_voice_exported", {"ok": False, "error": "name fehlt"})
|
||||
await _send(ws, "xtts_voice_exported", {"ok": False, "requestId": req_id, "error": "name fehlt"})
|
||||
return
|
||||
try:
|
||||
wav, txt = voice_paths(name)
|
||||
if not wav.exists():
|
||||
await _send(ws, "xtts_voice_exported", {"ok": False, "name": name, "error": "Stimme nicht gefunden"})
|
||||
await _send(ws, "xtts_voice_exported", {"ok": False, "requestId": req_id, "name": name, "error": "Stimme nicht gefunden"})
|
||||
return
|
||||
import io, tarfile
|
||||
buf = io.BytesIO()
|
||||
@@ -727,10 +742,10 @@ async def handle_export_voice(ws, payload: dict) -> None:
|
||||
tar.add(txt, arcname=txt.name)
|
||||
data = base64.b64encode(buf.getvalue()).decode("ascii")
|
||||
logger.info("Voice exportiert: %s (%d KB tar.gz)", name, len(buf.getvalue()) // 1024)
|
||||
await _send(ws, "xtts_voice_exported", {"ok": True, "name": name, "data": data})
|
||||
await _send(ws, "xtts_voice_exported", {"ok": True, "requestId": req_id, "name": name, "data": data})
|
||||
except Exception as e:
|
||||
logger.exception("handle_export_voice Fehler")
|
||||
await _send(ws, "xtts_voice_exported", {"ok": False, "name": name, "error": str(e)[:200]})
|
||||
await _send(ws, "xtts_voice_exported", {"ok": False, "requestId": req_id, "name": name, "error": str(e)[:200]})
|
||||
|
||||
|
||||
async def handle_import_voice(ws, payload: dict) -> None:
|
||||
@@ -827,6 +842,7 @@ async def _worker_register(ws, *, model: str = "", busy_fn=None) -> None:
|
||||
await _send(ws, "worker_hello", {
|
||||
"instanceId": INSTANCE_ID, "service": WORKER_SERVICE,
|
||||
"node": NODE_NAME, "gpus": GPU_IDS, "model": model,
|
||||
"voices": _local_voice_names(), # fuer Voice-Provisioning-Reconciliation
|
||||
})
|
||||
while True:
|
||||
await asyncio.sleep(WORKER_PING_INTERVAL_S)
|
||||
|
||||
Reference in New Issue
Block a user