|
|
|
@@ -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); }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|