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.
|
// lich Chunks einer alten Session in eine neue mischen.
|
||||||
private streamRequestId: string = '';
|
private streamRequestId: string = '';
|
||||||
private streamAudioRequestId: 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)
|
// Subscriber-Handles fuer Native-Events + RVS-Listener (cleanup beim stop)
|
||||||
private streamPcmChunkSub: { remove: () => void } | null = null;
|
private streamPcmChunkSub: { remove: () => void } | null = null;
|
||||||
private streamPcmErrorSub: { 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.
|
// Wir stoppen die Aufnahme — whisper hat alles was es braucht.
|
||||||
// Kein stt_stream_end senden: das Endpoint kam von der Bridge,
|
// Kein stt_stream_end senden: das Endpoint kam von der Bridge,
|
||||||
// sie hat schon finalisiert.
|
// sie hat schon finalisiert.
|
||||||
|
this._fireEndpoint(ev);
|
||||||
this._cleanupStreamLocal('endpoint');
|
this._cleanupStreamLocal('endpoint');
|
||||||
this.endpointListeners.forEach(cb => {
|
|
||||||
try { cb(ev); } catch (e) { console.warn('[Audio] endpoint listener err:', e); }
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (t === 'stt_stream_done') {
|
if (t === 'stt_stream_done') {
|
||||||
@@ -979,6 +981,7 @@ class AudioService {
|
|||||||
this.streamRequestId = requestId;
|
this.streamRequestId = requestId;
|
||||||
this.streamAudioRequestId = opts.audioRequestId || '';
|
this.streamAudioRequestId = opts.audioRequestId || '';
|
||||||
this.streamGotPartial = false;
|
this.streamGotPartial = false;
|
||||||
|
this.streamEndpointFired = false;
|
||||||
this.recordingStartTime = Date.now();
|
this.recordingStartTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1066,10 +1069,17 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Sauberer User-initiated Stop. Sendet stt_stream_end an die Bridge,
|
/** 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> {
|
async stopStreamingRecording(reason: string = 'user'): Promise<void> {
|
||||||
const reqId = this.streamRequestId;
|
const reqId = this.streamRequestId;
|
||||||
if (!reqId) return;
|
if (!reqId) return;
|
||||||
|
const audioReqId = this.streamAudioRequestId;
|
||||||
try {
|
try {
|
||||||
rvs.send('stt_stream_end' as any, { requestId: reqId, reason });
|
rvs.send('stt_stream_end' as any, { requestId: reqId, reason });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1078,6 +1088,21 @@ class AudioService {
|
|||||||
// Recorder lokal abschalten — Bridge feuert dann ihrerseits noch
|
// Recorder lokal abschalten — Bridge feuert dann ihrerseits noch
|
||||||
// stt_endpoint + stt_stream_done.
|
// stt_endpoint + stt_stream_done.
|
||||||
this._cleanupStreamLocal(`stop:${reason}`);
|
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
|
/** Abbruch ohne dass Brain den Text verarbeitet — z.B. wenn der User
|
||||||
@@ -1095,15 +1120,23 @@ class AudioService {
|
|||||||
} catch {}
|
} catch {}
|
||||||
this._cleanupStreamLocal(`cancel:${reason}`);
|
this._cleanupStreamLocal(`cancel:${reason}`);
|
||||||
// Listener feuern damit ChatScreen reagieren kann (endConversation etc.)
|
// Listener feuern damit ChatScreen reagieren kann (endConversation etc.)
|
||||||
const ev: SttEndpointEvent = {
|
this._fireEndpoint({
|
||||||
audioRequestId: audioReqId,
|
audioRequestId: audioReqId,
|
||||||
text: '',
|
text: '',
|
||||||
reason: `cancel:${reason}`,
|
reason: `cancel:${reason}`,
|
||||||
durationS: 0,
|
durationS: 0,
|
||||||
sttMs: 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 => {
|
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