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
+5
View File
@@ -897,6 +897,11 @@ async def run_loop(runner: F5Runner) -> None:
continue
mtype = msg.get("type", "")
payload = msg.get("payload", {}) or {}
# Redundanz-Routing: gezielt an eine andere Instanz
# adressiert → ignorieren. Ohne targetInstance → wie bisher.
tgt = payload.get("targetInstance")
if tgt and tgt != INSTANCE_ID:
continue
if mtype == "xtts_request":
try:
+5
View File
@@ -246,6 +246,11 @@ async def _run() -> None:
if msg.get("type") != "llm_request":
continue
payload = msg.get("payload", {}) or {}
# Redundanz-Routing: gezielt an eine andere Instanz adressiert
# → ignorieren. Ohne targetInstance → wie bisher (jeder nimmt).
tgt = payload.get("targetInstance")
if tgt and tgt != INSTANCE_ID:
continue
# Jede Anfrage nebenlaeufig — llama.cpp serialisiert intern,
# aber wir blockieren so nicht den Empfang weiterer Messages.
asyncio.create_task(_handle_llm_request(ws, payload))
+6
View File
@@ -751,6 +751,12 @@ async def run_loop(sessions: SessionManager) -> None:
continue
mtype = msg.get("type", "")
payload = msg.get("payload", {}) or {}
# Redundanz-Routing: ist die Anfrage gezielt an eine andere
# Instanz adressiert, ignorieren. Ohne targetInstance (Feld
# fehlt) → wie bisher, jeder Worker nimmt sie an.
tgt = payload.get("targetInstance")
if tgt and tgt != INSTANCE_ID:
continue
if mtype == "stt_stream_start":
sessions.start_session(payload)
elif mtype == "stt_audio_chunk":
+5
View File
@@ -899,6 +899,11 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None:
continue
mtype = msg.get("type", "")
payload = msg.get("payload", {}) or {}
# Redundanz-Routing: gezielt an eine andere Instanz adressiert
# → ignorieren. Ohne targetInstance → wie bisher.
tgt = payload.get("targetInstance")
if tgt and tgt != INSTANCE_ID:
continue
if mtype == "stt_request":
req_id = payload.get("requestId", "?")