From 0bf6d4943299f8b3170d06a9ff2a3651fd8cf978 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 22:09:02 +0200 Subject: [PATCH] fix(app): UI-Fallback wenn Whisper-Bridge nicht antwortet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit streamEndpointFired-Latch + neue _fireEndpoint(ev)-Methode konsolidieren die drei Pfade die den Endpoint-Listener feuern (RVS-stt_endpoint, cancel, neuer Fallback). Listener feuert pro Session-Cycle maximal einmal. stopStreamingRecording bekommt einen 3-Sekunden-Watchdog: kommt in dem Fenster keine echte stt_endpoint-Antwort der Bridge, feuert der Listener mit text='' (reason=stop:...:no-response) damit ChatScreen die "wird verarbeitet"-Bubble unstickt + endConversation aufruft. Greift praktisch in zwei Faellen: - Whisper-Bridge laeuft alte/keine Streaming-Version (Stefan Gamebox- Restart vergessen) → wir bleiben sonst bis zur 60s-Hardcap haengen - User-initiated Stop + Whisper langsam/crashed --- android/src/services/audio.ts | 47 +++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index 9f651a3..793db02 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -312,6 +312,10 @@ class AudioService { // lich Chunks einer alten Session in eine neue mischen. private streamRequestId: string = ''; private streamAudioRequestId: string = ''; + // Latch: ist endpointListeners fuer den aktuellen Session-Cycle schon gefeuert + // worden? Wird auf false gesetzt beim startStreamingRecording, auf true beim + // ersten Endpoint (egal ob via RVS oder Fallback). Verhindert Doppel-Fires. + private streamEndpointFired: boolean = false; // Subscriber-Handles fuer Native-Events + RVS-Listener (cleanup beim stop) private streamPcmChunkSub: { remove: () => void } | null = null; private streamPcmErrorSub: { remove: () => void } | null = null; @@ -389,10 +393,8 @@ class AudioService { // Wir stoppen die Aufnahme — whisper hat alles was es braucht. // Kein stt_stream_end senden: das Endpoint kam von der Bridge, // sie hat schon finalisiert. + this._fireEndpoint(ev); this._cleanupStreamLocal('endpoint'); - this.endpointListeners.forEach(cb => { - try { cb(ev); } catch (e) { console.warn('[Audio] endpoint listener err:', e); } - }); return; } if (t === 'stt_stream_done') { @@ -979,6 +981,7 @@ class AudioService { this.streamRequestId = requestId; this.streamAudioRequestId = opts.audioRequestId || ''; this.streamGotPartial = false; + this.streamEndpointFired = false; this.recordingStartTime = Date.now(); try { @@ -1066,10 +1069,17 @@ class AudioService { } /** Sauberer User-initiated Stop. Sendet stt_stream_end an die Bridge, - * die noch ihren Final-Transcribe macht. */ + * die noch ihren Final-Transcribe macht. + * + * Plus: Fallback-Timer (3s). Wenn die Bridge nicht antwortet (z.B. weil + * veraltete Version ohne Streaming-Handler laeuft), feuern wir den + * Endpoint-Listener trotzdem mit text='' damit die App-UI nicht in + * "wird verarbeitet..." haengt. ChatScreen behandelt das wie den + * No-Speech-Fall (Bubble weg + endConversation). */ async stopStreamingRecording(reason: string = 'user'): Promise { const reqId = this.streamRequestId; if (!reqId) return; + const audioReqId = this.streamAudioRequestId; try { rvs.send('stt_stream_end' as any, { requestId: reqId, reason }); } catch (e) { @@ -1078,6 +1088,21 @@ class AudioService { // Recorder lokal abschalten — Bridge feuert dann ihrerseits noch // stt_endpoint + stt_stream_done. this._cleanupStreamLocal(`stop:${reason}`); + // Fallback-Watchdog: nach 3s noch immer kein Endpoint via RVS angekommen + // → _fireEndpoint mit text='' (idempotent via streamEndpointFired-Latch, + // d.h. wenn echtes stt_endpoint zwischen jetzt und +3s ankommt feuert + // dieser Fallback NICHT). + setTimeout(() => { + if (this.streamEndpointFired) return; + console.log('[Audio] stopStreamingRecording: 3s ohne Bridge-Antwort — fallback fire'); + this._fireEndpoint({ + audioRequestId: audioReqId, + text: '', + reason: `stop:${reason}:no-response`, + durationS: 0, + sttMs: 0, + }); + }, 3000); } /** Abbruch ohne dass Brain den Text verarbeitet — z.B. wenn der User @@ -1095,15 +1120,23 @@ class AudioService { } catch {} this._cleanupStreamLocal(`cancel:${reason}`); // Listener feuern damit ChatScreen reagieren kann (endConversation etc.) - const ev: SttEndpointEvent = { + this._fireEndpoint({ audioRequestId: audioReqId, text: '', reason: `cancel:${reason}`, durationS: 0, sttMs: 0, - }; + }); + } + + /** Feuert den Endpoint-Listener — aber nur einmal pro Session-Cycle. + * Wird sowohl vom RVS-stt_endpoint-Pfad als auch vom Fallback-Watchdog + * und cancelStreamingRecording aufgerufen. */ + private _fireEndpoint(ev: SttEndpointEvent): void { + if (this.streamEndpointFired) return; + this.streamEndpointFired = true; this.endpointListeners.forEach(cb => { - try { cb(ev); } catch (e) { console.warn('[Audio] endpoint listener (cancel) err:', e); } + try { cb(ev); } catch (e) { console.warn('[Audio] endpoint listener err:', e); } }); }