fix(app): STT-Endpoint großzügiger + Mikro vor Re-Arm freigeben

#2 Vorzeitiges Absenden: endpointMs war hart 1500ms — im Auto (Sprechpausen)
schnitt das mitten im Satz ab (die 11.8s-Frage wurde bei "…ohne dass ein"
gekappt). Jetzt konfigurierbar (aria_stt_endpoint_ms, Default 2400, 1000-4000).

#3 Ohr bleibt ausgegraut: beim Re-Arm rief der Wake-Word-Service
OpenWakeWord.start(), waehrend die passive Streaming-Aufnahme noch das Mikro
hielt → start() schlug fehl → state=off. Neuer micReleaseHook cancelt die
Aufnahme VOR start() (endConversation / exitPassiveListening /
discardIfFreshlyTriggered). ChatScreen registriert den Hook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 20:29:04 +02:00
co-authored by Claude Opus 4.8
parent a4eee8458b
commit a06ea82532
3 changed files with 55 additions and 5 deletions
+22
View File
@@ -132,6 +132,12 @@ class WakeWordService {
* hier eine streaming-Aufnahme OHNE User-Bubble (passiv lauschen). */
private passiveListenCallbacks: PassiveListenCallback[] = [];
/** Hook, der das Mikro freigibt (laufende Streaming-Aufnahme canceln) BEVOR
* wir OpenWakeWord.start() rufen. Ohne das haelt die passive/conversing
* Aufnahme das Mikro noch, start() schlaegt fehl → Ohr bleibt ausgegraut
* (state=off). ChatScreen registriert den Hook mit audioService.cancel…. */
private micReleaseHook: (() => Promise<void>) | null = null;
private keyword: WakeKeyword = DEFAULT_KEYWORD;
private nativeReady: boolean = false;
private initInProgress: Promise<boolean> | null = null;
@@ -149,6 +155,19 @@ class WakeWordService {
}
}
/** ChatScreen registriert hier einen Hook, der eine laufende Streaming-
* Aufnahme cancelt (Mikro freigeben) — wird vor jedem Re-Arm gerufen. */
setMicReleaseHook(fn: (() => Promise<void>) | null): void {
this.micReleaseHook = fn;
}
private async _freeMic(): Promise<void> {
if (!this.micReleaseHook) return;
try { await this.micReleaseHook(); } catch (e) {
console.warn('[WakeWord] micReleaseHook err:', e);
}
}
/** Settings-Wechsel: anderes Wake-Word. Re-Init des Native-Moduls. */
async configure(keyword: string): Promise<boolean> {
const next: WakeKeyword = (WAKE_KEYWORDS as readonly string[]).includes(keyword)
@@ -454,6 +473,7 @@ class WakeWordService {
// als state extra zu fragen, garantiert dass nach diesem Pfad
// Native auch wirklich an ist falls es out-of-band gestoppt wurde.
try {
await this._freeMic(); // Streaming-Aufnahme canceln → Mikro frei
await OpenWakeWord.start();
console.log('[WakeWord] Konversation zu Ende — zurueck zu armed (wasBarge=%s)', wasBarge);
import('./logger').then(m => m.reportAppDebug('wake.end',
@@ -520,6 +540,7 @@ class WakeWordService {
// timeout oder manual → Wake-Word reaktivieren, armed-State.
if (this.nativeReady && OpenWakeWord) {
try {
await this._freeMic(); // passive Streaming-Aufnahme canceln → Mikro frei
await OpenWakeWord.start();
console.log('[WakeWord] zurueck zu armed nach passive-listen');
ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
@@ -564,6 +585,7 @@ class WakeWordService {
this.lastTriggerAt = 0;
if (this.nativeReady && OpenWakeWord) {
try {
await this._freeMic(); // ggf. laufende Aufnahme canceln → Mikro frei
await OpenWakeWord.start();
ToastAndroid.show('Hintergrund-Trigger verworfen — lausche wieder', ToastAndroid.SHORT);
this.setState('armed');