fix(app): UI-Fallback wenn Whisper-Bridge nicht antwortet
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
This commit is contained in:
@@ -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<void> {
|
||||
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); }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user