feat(compute): Redundanz-Routing per targetInstance (Stage 3)

Mehrere Instanzen pro Dienst nutzbar: Anfragen werden gezielt an eine freie
Instanz adressiert statt an alle gebroadcastet. Mehrere f5tts → naechstes
freies; mehrere LLM → parallele Turns (Multitasking); STT-Redundanz ueber
mehrere Apps via Lease.

- Worker (alle vier): filtern am Loop-Eingang — targetInstance gesetzt und
  != eigener INSTANCE_ID → Nachricht ignorieren. Feld fehlt → wie bisher.
- bridge (TTS/LLM, emittiert die Bridge selbst): _pick_worker() waehlt eine
  online+freie Instanz (Round-Robin), stempelt targetInstance auf
  xtts_request / llm_request. Keine Instanz bekannt → Broadcast.
- bridge (STT-Lease, emittiert die App): neuer stt_lease_request-Handler →
  _pick_stt_worker() (voxtral vor whisper) → stt_lease {instanceId}.
- app (audio.ts): requestSttLease() vor dem Stream, stempelt targetInstance
  auf stt_stream_start / stt_audio_chunk / stt_stream_end (+cancel). Kurzer
  Timeout → '' (Broadcast), Aufnahme haengt nie.

Voll rueckwaertskompatibel: Routing aktiviert sich erst, wenn Worker sich per
worker_hello (Stage 2) registriert haben — sonst bleibt alles Broadcast.
Braucht APK-Rebuild fuer die STT-Lease-Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 12:27:51 +02:00
co-authored by Claude Opus 4.8
parent f2ead1242f
commit ec78eb8efe
6 changed files with 118 additions and 14 deletions
+48 -12
View File
@@ -1684,20 +1684,27 @@ class ARIABridge:
if len(self._xtts_request_to_message) > 100:
oldest = next(iter(self._xtts_request_to_message))
self._xtts_request_to_message.pop(oldest, None)
# Redundanz: freie f5tts-Instanz waehlen und gezielt adressieren.
# None (keine Instanz bekannt / alle offline) → kein targetInstance,
# Broadcast wie bisher (Single-Node laeuft unveraendert).
tts_target = self._pick_worker("f5tts")
tts_payload = {
"text": tts_text,
"voice": xtts_voice,
"speed": xtts_speed,
"language": "de",
"requestId": xtts_request_id,
"messageId": message_id,
}
if tts_target:
tts_payload["targetInstance"] = tts_target
await self._send_to_rvs({
"type": "xtts_request",
"payload": {
"text": tts_text,
"voice": xtts_voice,
"speed": xtts_speed,
"language": "de",
"requestId": xtts_request_id,
"messageId": message_id,
},
"payload": tts_payload,
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
logger.info("[core] XTTS-Request gesendet (voice=%s, speed=%.2fx): '%s'",
xtts_voice or "default", xtts_speed, tts_text[:60])
logger.info("[core] XTTS-Request gesendet (voice=%s, speed=%.2fx, target=%s): '%s'",
xtts_voice or "default", xtts_speed, tts_target or "(broadcast)", tts_text[:60])
except Exception as e:
logger.error("[core] XTTS-Request fehlgeschlagen: %s — kein Audio", e)
@@ -3551,6 +3558,23 @@ class ARIABridge:
self._workers[iid]["gpus"] or "?", self._workers[iid]["model"] or "?")
return
elif msg_type == "stt_lease_request":
# Die App fragt vor dem Aufnahme-Stream, welche STT-Instanz sie
# adressieren soll (Redundanz ueber mehrere Apps/Nodes). Wir waehlen
# eine freie STT-Instanz und antworten per stt_lease. Ist keine
# Instanz bekannt (instanceId leer), streamt die App wie bisher an
# ALLE (Broadcast) — Single-Node bleibt unveraendert.
req_id = (payload.get("requestId") or "").strip()
iid = self._pick_stt_worker() or ""
await self._send_to_rvs({
"type": "stt_lease",
"payload": {"requestId": req_id, "instanceId": iid},
"timestamp": int(time.time() * 1000),
})
logger.info("[stt-lease] req=%s%s", req_id[:8] if req_id else "?",
iid or "(broadcast)")
return
elif msg_type == "worker_ping":
iid = (payload.get("instanceId") or "").strip()
if iid:
@@ -3982,8 +4006,14 @@ class ARIABridge:
req_payload["tools"] = tools
if model:
req_payload["model"] = model
logger.info("[rvs] llm_request → llm-adapter (id=%s, msgs=%d, max_tokens=%d, tools=%d, model=%s)",
request_id[:8], len(messages), max_tokens, len(tools) if tools else 0, model or "-")
# Redundanz/Multitasking: freie llm-Instanz gezielt adressieren; None
# → Broadcast wie bisher. Mehrere Instanzen → parallele Turns.
llm_target = self._pick_worker("llm")
if llm_target:
req_payload["targetInstance"] = llm_target
logger.info("[rvs] llm_request → llm-adapter (id=%s, msgs=%d, max_tokens=%d, tools=%d, model=%s, target=%s)",
request_id[:8], len(messages), max_tokens, len(tools) if tools else 0,
model or "-", llm_target or "(broadcast)")
ok = await self._send_to_rvs({
"type": "llm_request",
"payload": req_payload,
@@ -4764,6 +4794,12 @@ class ARIABridge:
chosen["busy"] = True # optimistisch, bis der naechste Ping korrigiert
return chosen["instanceId"]
def _pick_stt_worker(self) -> Optional[str]:
"""Waehlt eine STT-Instanz fuer ein App-Lease. Voxtral (Default-STT) hat
Vorrang, Whisper ist der Fallback. None → keine online (App streamt dann
ohne targetInstance = heutiges Broadcast-Verhalten)."""
return self._pick_worker("voxtral") or self._pick_worker("whisper")
async def _satellite_request(self, op: str, satellite: str = "",
device: str = "", action: str = "",
params: Optional[dict] = None,