Compare commits
5 Commits
493cba36a2
...
v0.1.8.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 91760dd2e1 | |||
| 3c2e537420 | |||
| 97b6ea1b3e | |||
| 94ee0455a2 | |||
| 0bf6d49432 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10800
|
||||
versionName "0.1.8.0"
|
||||
versionCode 10802
|
||||
versionName "0.1.8.2"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.8.0",
|
||||
"version": "0.1.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -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); }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -390,15 +390,35 @@ class WakeWordService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten */
|
||||
/** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten.
|
||||
*
|
||||
* WICHTIG: setTimeout(800ms) kann im Hintergrund (Display aus) verspaetet
|
||||
* feuern — JS-Thread ist geparkt. Wenn der Timer >2s ueberfaellig ist,
|
||||
* hat der User offensichtlich die App verlassen und kommt erst spaeter
|
||||
* wieder — wir oeffnen das Mikro dann NICHT, sondern beenden die
|
||||
* Konversation. Sonst sieht der User nach dem App-Resume "Mikro plus-
|
||||
* aufnahme laeuft" obwohl er gar nichts gesagt hat → wirkt wie Phantom-
|
||||
* Wake-Word. Klassische Doze-Throttling-Falle wie bei wake.detect frueher. */
|
||||
async resume(): Promise<void> {
|
||||
if (this.state !== 'conversing') return;
|
||||
const scheduledAt = Date.now();
|
||||
// Kurze Pause damit TTS-Audio nicht ins Mikrofon geht
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
if (this.state === 'conversing') {
|
||||
console.log('[WakeWord] TTS fertig — naechste Aufnahme im Conversation-Window');
|
||||
this.wakeCallbacks.forEach(cb => cb());
|
||||
if (this.state !== 'conversing') return;
|
||||
const delay = Date.now() - scheduledAt;
|
||||
if (delay > 2800) {
|
||||
// Timer war stark verspaetet — JS-Thread war im Hintergrund geparkt.
|
||||
// Conversation als beendet behandeln statt das Mikro zu oeffnen.
|
||||
console.log('[WakeWord] resume(): %dms statt ~800ms — App war im Background. endConversation statt mic-open', delay);
|
||||
import('./logger').then(m => m.reportAppDebug('wake.resume',
|
||||
`delayed ${delay}ms (>2800) — endConversation statt mic-open`)).catch(()=>{});
|
||||
// Asynchroner Aufruf — endConversation ist async, kein await damit wir
|
||||
// hier nicht in einem Promise-Chain haengen.
|
||||
this.endConversation().catch(() => {});
|
||||
return;
|
||||
}
|
||||
console.log('[WakeWord] TTS fertig — naechste Aufnahme im Conversation-Window (delay=%dms)', delay);
|
||||
this.wakeCallbacks.forEach(cb => cb());
|
||||
}
|
||||
|
||||
/** True solange das Ohr aktiv ist (armed ODER conversing). */
|
||||
|
||||
@@ -38,6 +38,10 @@ const ALLOWED_TYPES = new Set([
|
||||
"xtts_delete_voice",
|
||||
"voice_preload", "voice_ready",
|
||||
"stt_request", "stt_response",
|
||||
// Streaming-STT (Phase 1+2): App schickt PCM live an whisper-bridge,
|
||||
// die feuert stt_endpoint mit dem finalen Text — kein Audio-Roundtrip.
|
||||
"stt_stream_start", "stt_audio_chunk", "stt_stream_end",
|
||||
"stt_partial", "stt_endpoint", "stt_stream_done",
|
||||
"service_status",
|
||||
"config_request",
|
||||
"flux_request", "flux_response",
|
||||
|
||||
Reference in New Issue
Block a user