fix(audio): Placeholder-Race per audioRequestId + Mikro-Offen-Toast erst nach Start

Bug: Bei zwei Sprachnachrichten kurz hintereinander wurde der STT-Text
der zweiten in die Bubble der ersten geschrieben. Ursache: findIndex
matchte ueber Substring "Spracheingabe wird verarbeitet" → bei zwei
offenen Placeholders nahm er immer die ERSTE, egal welches STT-Result
gerade kam.

Fix: jede Aufnahme bekommt eine eindeutige audioRequestId, App pusht
sie in die Placeholder-Bubble + ans audio-Event. Bridge gibt sie
unveraendert ans STT-Result zurueck. App matcht primaer per ID, fallback
auf Substring (Kompatibilitaet zu alten Bridge-Versionen).

Bonus: Toast "Wake-Word erkannt" entfernt, dafuer "🎤 Mikro offen —
sprich jetzt" erst wenn audioService.startRecording wirklich erfolgreich
war. So weiss der User exakt ab wann er reden darf — vorher war der Toast
schon ~400ms vorher da.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 22:33:26 +02:00
parent a9a87f12df
commit 77e927ffcd
3 changed files with 71 additions and 47 deletions
+20 -9
View File
@@ -1510,10 +1510,12 @@ class ARIABridge:
except (TypeError, ValueError):
self._next_speed_override = None
interrupted = bool(payload.get("interrupted", False))
logger.info("[rvs] Audio empfangen: %s, %dms, %dKB%s",
audio_request_id = payload.get("audioRequestId", "") or ""
logger.info("[rvs] Audio empfangen: %s, %dms, %dKB%s%s",
mime_type, duration_ms, len(audio_b64) // 1365,
" [BARGE-IN]" if interrupted else "")
asyncio.create_task(self._process_app_audio(audio_b64, mime_type, interrupted))
" [BARGE-IN]" if interrupted else "",
f" reqId={audio_request_id[:16]}" if audio_request_id else "")
asyncio.create_task(self._process_app_audio(audio_b64, mime_type, interrupted, audio_request_id))
elif msg_type == "stt_response":
# Antwort der whisper-bridge auf unseren stt_request
@@ -1569,13 +1571,19 @@ class ARIABridge:
_STT_REMOTE_TIMEOUT_READY_S = 45.0
_STT_REMOTE_TIMEOUT_LOADING_S = 300.0
async def _process_app_audio(self, audio_b64: str, mime_type: str, interrupted: bool = False) -> None:
async def _process_app_audio(self, audio_b64: str, mime_type: str,
interrupted: bool = False,
audio_request_id: str = "") -> None:
"""App-Audio → STT → aria-core. Primaer via whisper-bridge (RVS), Fallback lokal.
interrupted=True wenn der User waehrend ARIA noch sprach/dachte aufgenommen hat
(Barge-In). Wird als Hinweis-Praefix an aria-core mitgegeben damit ARIA die
Korrektur/Unterbrechung in den Kontext einordnen kann statt als reine
Folgefrage zu behandeln."""
Folgefrage zu behandeln.
audio_request_id: Korrelations-ID die die App im audio-Event mitschickt — wird
unveraendert ans STT-Result zurueckgegeben damit die App die EXAKT richtige
'wird verarbeitet'-Bubble ersetzen kann (auch bei mehreren parallelen Aufnahmen)."""
# Erst Remote versuchen
text = await self._stt_remote(audio_b64, mime_type)
if text is None:
@@ -1601,12 +1609,15 @@ class ARIABridge:
# STT-Text an RVS senden (fuer Anzeige in App + Diagnostic)
# sender="stt" damit Bridge es ignoriert (kein Loop)
try:
stt_payload = {
"text": text,
"sender": "stt",
}
if audio_request_id:
stt_payload["audioRequestId"] = audio_request_id
ok = await self._send_to_rvs({
"type": "chat",
"payload": {
"text": text,
"sender": "stt",
},
"payload": stt_payload,
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
if ok: