feat(speaker-id): Phase 5 — Passive-Listen-Window nach jeder Konversation
Neuer State 'listening' im WakeWordService. Nach endConversation faellt
ARIA nicht direkt zu armed zurueck, sondern ins passive Lauschen fuer
PASSIVE_LISTEN_DEFAULT_MS (Default 30s, in AsyncStorage konfigurierbar).
In dem Fenster braucht Stefan kein Wake-Word mehr — er kann einfach
weitersprechen, Speaker-ID-Gating in der Whisper-Bridge filtert fremde
Stimmen (TV, Frau, Hintergrundgespraeche).
Flow:
armed → wake → conversing → TTS → resume → (Nichts gesagt) →
endConversation → enterPassiveListening('listening' + Timer) →
startPassiveStreamingRecording (kein User-Bubble, kein wake-ready-Sound)
→ Speaker-ID-Gating in Bridge → Speech detected:
exitPassiveListening('speech') → 'conversing' → normaler Flow
→ Nichts in N Sek:
Timer feuert → exitPassiveListening('timeout') → 'armed' (Wake an)
Implementation:
- wakeword.ts: WakeWordState += 'listening'. enterPassiveListening +
exitPassiveListening + onPassiveListen-Callback + Cancel-Timer-Hooks
in stop(). PASSIVE_LISTEN_DEFAULT_MS/STORAGE_KEY + load/save Helpers.
- ChatScreen.tsx: state-Type um 'listening' erweitert. State-Listener
schliesst Conversation-Focus auch in 'listening' (Spotify bleibt
pausiert). onPassiveListen → startPassiveStreamingRecording mit
noSpeechTimeoutMs=passiveMs. STT-Endpoint-Handler: bei text != ''
und state=='listening' → exitPassiveListening('speech'); bei
text == '' und state=='listening' → naechste passive Aufnahme.
Beim Wechsel listening→armed/off: laufende streaming-Aufnahme
cancellen damit OpenWakeWord beim Re-Arm das Mic kriegt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ import MemoryBrowser from '../components/MemoryBrowser';
|
|||||||
import ErrorBoundary from '../components/ErrorBoundary';
|
import ErrorBoundary from '../components/ErrorBoundary';
|
||||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
import wakeWordService from '../services/wakeword';
|
import wakeWordService, { loadPassiveListenMs } from '../services/wakeword';
|
||||||
import phoneCallService from '../services/phoneCall';
|
import phoneCallService from '../services/phoneCall';
|
||||||
import { playWakeReadySound } from '../services/wakeReadySound';
|
import { playWakeReadySound } from '../services/wakeReadySound';
|
||||||
import {
|
import {
|
||||||
@@ -273,7 +273,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
const [gpsEnabled, setGpsEnabled] = useState(false);
|
const [gpsEnabled, setGpsEnabled] = useState(false);
|
||||||
const [wakeWordActive, setWakeWordActive] = useState(false);
|
const [wakeWordActive, setWakeWordActive] = useState(false);
|
||||||
// Genauer State (off/armed/conversing) fuer UI-Feedback am Button
|
// Genauer State (off/armed/conversing) fuer UI-Feedback am Button
|
||||||
const [wakeWordState, setWakeWordState] = useState<'off' | 'armed' | 'conversing'>('off');
|
const [wakeWordState, setWakeWordState] = useState<'off' | 'armed' | 'conversing' | 'listening'>('off');
|
||||||
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||||
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
|
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
|
||||||
const [inboxVisible, setInboxVisible] = useState(false);
|
const [inboxVisible, setInboxVisible] = useState(false);
|
||||||
@@ -487,9 +487,16 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Conversation-Focus an Wake-Word-State koppeln: solange wir aktiv im
|
// Conversation-Focus an Wake-Word-State koppeln: solange wir aktiv im
|
||||||
// Dialog sind, soll Spotify dauerhaft gepaust bleiben (auch ueber
|
// Dialog sind, soll Spotify dauerhaft gepaust bleiben (auch ueber
|
||||||
// Render-Pausen + zwischen Antworten hinweg). Sobald wir zurueck nach
|
// Render-Pausen + zwischen Antworten hinweg). Sobald wir zurueck nach
|
||||||
// 'armed' oder 'off' fallen, darf Spotify wieder.
|
// 'armed' oder 'off' fallen, darf Spotify wieder. 'listening' soll
|
||||||
if (s === 'conversing') audioService.acquireConversationFocus();
|
// Spotify ebenfalls leise halten (User darf jederzeit weitersprechen).
|
||||||
|
if (s === 'conversing' || s === 'listening') audioService.acquireConversationFocus();
|
||||||
else audioService.releaseConversationFocus();
|
else audioService.releaseConversationFocus();
|
||||||
|
// Beim Verlassen von 'listening' (Timer abgelaufen) eine ggf. noch
|
||||||
|
// laufende passive Streaming-Aufnahme killen, sonst hat OpenWakeWord
|
||||||
|
// keinen Zugriff aufs Mic beim Re-Arm.
|
||||||
|
if ((s === 'armed' || s === 'off') && audioService.isStreamingRecording()) {
|
||||||
|
audioService.cancelStreamingRecording('wakeword-state-' + s);
|
||||||
|
}
|
||||||
// Foreground-Service-Slot 'wake' — solange das Ohr ueberhaupt aktiv ist
|
// Foreground-Service-Slot 'wake' — solange das Ohr ueberhaupt aktiv ist
|
||||||
// (armed oder conversing), soll der App-Prozess im Hintergrund am Leben
|
// (armed oder conversing), soll der App-Prozess im Hintergrund am Leben
|
||||||
// bleiben damit Mikro-Lauschen + Aufnahme weiterlaufen.
|
// bleiben damit Mikro-Lauschen + Aufnahme weiterlaufen.
|
||||||
@@ -1346,12 +1353,18 @@ const ChatScreen: React.FC = () => {
|
|||||||
// - text != '' → Whisper-Bridge hat ML-Endpoint erkannt, Text liegt vor.
|
// - text != '' → Whisper-Bridge hat ML-Endpoint erkannt, Text liegt vor.
|
||||||
// aria-bridge bekommt das gleiche Event und triggert Brain
|
// aria-bridge bekommt das gleiche Event und triggert Brain
|
||||||
// direkt. App muss nix mehr senden.
|
// direkt. App muss nix mehr senden.
|
||||||
// - text == '' → cancelStreamingRecording (no-speech / hardcap / error).
|
// - text == '' → cancelStreamingRecording (no-speech / hardcap / error /
|
||||||
// Konversation beenden wie frueher der "kein Speech"-Fall.
|
// speaker_mismatch). Konversation beenden, oder bei
|
||||||
|
// passive-listening: nochmal lauschen.
|
||||||
const unsubEndpoint = audioService.onSttEndpoint((ev) => {
|
const unsubEndpoint = audioService.onSttEndpoint((ev) => {
|
||||||
if (ev.text && ev.text.trim()) {
|
if (ev.text && ev.text.trim()) {
|
||||||
console.log('[Chat] STT-Endpoint: %r (reason=%s, %dms, %.1fs Audio)',
|
console.log('[Chat] STT-Endpoint: %r (reason=%s, %dms, %.1fs Audio)',
|
||||||
ev.text.slice(0, 80), ev.reason, ev.sttMs, ev.durationS);
|
ev.text.slice(0, 80), ev.reason, ev.sttMs, ev.durationS);
|
||||||
|
// Wenn passive lauschend: User hat tatsaechlich was gesagt → uebergang
|
||||||
|
// zu 'conversing' damit der normale Flow greift (TTS, resume, etc.)
|
||||||
|
if (wakeWordService.getState() === 'listening') {
|
||||||
|
wakeWordService.exitPassiveListening('speech').catch(() => {});
|
||||||
|
}
|
||||||
// Brain laeuft via aria-bridge — wir warten auf chat(sender=stt) +
|
// Brain laeuft via aria-bridge — wir warten auf chat(sender=stt) +
|
||||||
// chat(sender=aria) wie im Legacy-Pfad.
|
// chat(sender=aria) wie im Legacy-Pfad.
|
||||||
} else {
|
} else {
|
||||||
@@ -1361,11 +1374,28 @@ const ChatScreen: React.FC = () => {
|
|||||||
if (ev.audioRequestId) {
|
if (ev.audioRequestId) {
|
||||||
setMessages(prev => prev.filter(m => m.audioRequestId !== ev.audioRequestId));
|
setMessages(prev => prev.filter(m => m.audioRequestId !== ev.audioRequestId));
|
||||||
}
|
}
|
||||||
wakeWordService.endConversation();
|
// Bei Passive-Listen + speaker_mismatch oder no-speech: erneut passiv
|
||||||
if (!wakeWordService.isActive()) setWakeWordActive(false);
|
// lauschen (Timer im wakeword-service laeuft weiter, regelt das Ende).
|
||||||
|
// Sonst endConversation wie bisher.
|
||||||
|
if (wakeWordService.getState() === 'listening') {
|
||||||
|
console.log('[Chat] Passive-Listen: leeres Endpoint — naechste passive Aufnahme');
|
||||||
|
startPassiveStreamingRecording();
|
||||||
|
} else {
|
||||||
|
wakeWordService.endConversation();
|
||||||
|
if (!wakeWordService.isActive()) setWakeWordActive(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Passive-Listen-Callback: Wake-Word-Service hat in den passiven Modus
|
||||||
|
// geschaltet (nach endConversation). Wir starten eine streaming-Aufnahme
|
||||||
|
// OHNE User-Bubble + ohne wake-ready-Sound. Speaker-ID-Gating in der
|
||||||
|
// Whisper-Bridge filtert fremde Stimmen weg.
|
||||||
|
const unsubPassive = wakeWordService.onPassiveListen(() => {
|
||||||
|
console.log('[Chat] Passive-Listen aktiviert — starte stille Streaming-Aufnahme');
|
||||||
|
startPassiveStreamingRecording();
|
||||||
|
});
|
||||||
|
|
||||||
// Barge-In via Wake-Word: User sagt "Computer" waehrend ARIA spricht.
|
// Barge-In via Wake-Word: User sagt "Computer" waehrend ARIA spricht.
|
||||||
// Wake-Word-Service hat bei TTS-Start parallel zu lauschen begonnen
|
// Wake-Word-Service hat bei TTS-Start parallel zu lauschen begonnen
|
||||||
// (mit AcousticEchoCanceler damit ARIAs eigene Stimme nicht triggert).
|
// (mit AcousticEchoCanceler damit ARIAs eigene Stimme nicht triggert).
|
||||||
@@ -1430,11 +1460,38 @@ const ChatScreen: React.FC = () => {
|
|||||||
unsubWake();
|
unsubWake();
|
||||||
unsubEndpoint();
|
unsubEndpoint();
|
||||||
unsubBarge();
|
unsubBarge();
|
||||||
|
unsubPassive();
|
||||||
unsubTtsStart();
|
unsubTtsStart();
|
||||||
unsubTtsEnd();
|
unsubTtsEnd();
|
||||||
};
|
};
|
||||||
}, [wakeWordActive]);
|
}, [wakeWordActive]);
|
||||||
|
|
||||||
|
// Passive-Listen-Aufnahme: ohne User-Bubble, ohne Wake-Sound, Speaker-ID-
|
||||||
|
// Gating in der Whisper-Bridge entscheidet ob Stefan spricht oder z.B.
|
||||||
|
// die Frau / TV. Bei text != '' → wakeWordService.exitPassiveListening('speech')
|
||||||
|
// schaltet auf conversing, Brain antwortet, TTS spielt, resume → endConv →
|
||||||
|
// ... und passive listening startet von vorne (mit frischem Timer).
|
||||||
|
// useCallback damit der useEffect oben die Funktion stabil capturen kann.
|
||||||
|
const startPassiveStreamingRecording = useCallback(async () => {
|
||||||
|
const audioRequestId = `audio_passive_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||||
|
const location = await getCurrentLocation();
|
||||||
|
const passiveMs = await loadPassiveListenMs();
|
||||||
|
const { ok } = await audioService.startStreamingRecording({
|
||||||
|
audioRequestId,
|
||||||
|
voice: localXttsVoiceRef.current,
|
||||||
|
speed: ttsSpeedRef.current,
|
||||||
|
interrupted: false,
|
||||||
|
location: location || null,
|
||||||
|
noSpeechTimeoutMs: Math.min(passiveMs, 30000),
|
||||||
|
endpointMs: 1500,
|
||||||
|
hardCapMs: Math.max(passiveMs + 5000, 35000),
|
||||||
|
});
|
||||||
|
if (!ok) {
|
||||||
|
console.warn('[Chat] passive streaming start failed — exit passive listening');
|
||||||
|
wakeWordService.exitPassiveListening('manual').catch(() => {});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Wake Word Toggle Handler
|
// Wake Word Toggle Handler
|
||||||
const toggleWakeWord = useCallback(async () => {
|
const toggleWakeWord = useCallback(async () => {
|
||||||
if (wakeWordActive) {
|
if (wakeWordActive) {
|
||||||
|
|||||||
@@ -26,8 +26,30 @@ import { acquireBackgroundAudio } from './backgroundAudio';
|
|||||||
|
|
||||||
type WakeWordCallback = () => void;
|
type WakeWordCallback = () => void;
|
||||||
type StateCallback = (state: WakeWordState) => void;
|
type StateCallback = (state: WakeWordState) => void;
|
||||||
|
type PassiveListenCallback = () => void;
|
||||||
|
|
||||||
export type WakeWordState = 'off' | 'armed' | 'conversing';
|
export type WakeWordState = 'off' | 'armed' | 'conversing' | 'listening';
|
||||||
|
|
||||||
|
/** Default-Dauer fuer den Passive-Listen-Modus nach einer Konversation —
|
||||||
|
* in dem Fenster braucht's kein Wake-Word, Speaker-ID-Filter haelt
|
||||||
|
* fremde Stimmen raus (TV, Familie). 30s default; konfigurierbar. */
|
||||||
|
export const PASSIVE_LISTEN_DEFAULT_MS = 30_000;
|
||||||
|
export const PASSIVE_LISTEN_STORAGE_KEY = 'aria_passive_listen_ms';
|
||||||
|
|
||||||
|
export async function loadPassiveListenMs(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const raw = await AsyncStorage.getItem(PASSIVE_LISTEN_STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const n = parseInt(raw, 10);
|
||||||
|
if (isFinite(n) && n >= 0 && n <= 120_000) return n;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return PASSIVE_LISTEN_DEFAULT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function savePassiveListenMs(ms: number): Promise<void> {
|
||||||
|
await AsyncStorage.setItem(PASSIVE_LISTEN_STORAGE_KEY, String(ms));
|
||||||
|
}
|
||||||
|
|
||||||
export const WAKE_KEYWORD_STORAGE = 'aria_wake_keyword';
|
export const WAKE_KEYWORD_STORAGE = 'aria_wake_keyword';
|
||||||
|
|
||||||
@@ -103,6 +125,12 @@ class WakeWordService {
|
|||||||
* Ausnahme: bargeListening → Barge-In ist ein legitimer neuer Trigger
|
* Ausnahme: bargeListening → Barge-In ist ein legitimer neuer Trigger
|
||||||
* waehrend ARIA noch redet, NICHT vom Guard blockieren. */
|
* waehrend ARIA noch redet, NICHT vom Guard blockieren. */
|
||||||
private detectionInProgress: boolean = false;
|
private detectionInProgress: boolean = false;
|
||||||
|
/** Passive-Listen-Timer: feuert nach PASSIVE_LISTEN_MS ohne Stefan-Speech,
|
||||||
|
* beendet den listening-State und geht zurueck zu armed. */
|
||||||
|
private passiveListenTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
/** Callbacks fuer den Eintritt in Passive-Listen — ChatScreen startet
|
||||||
|
* hier eine streaming-Aufnahme OHNE User-Bubble (passiv lauschen). */
|
||||||
|
private passiveListenCallbacks: PassiveListenCallback[] = [];
|
||||||
|
|
||||||
private keyword: WakeKeyword = DEFAULT_KEYWORD;
|
private keyword: WakeKeyword = DEFAULT_KEYWORD;
|
||||||
private nativeReady: boolean = false;
|
private nativeReady: boolean = false;
|
||||||
@@ -225,6 +253,7 @@ class WakeWordService {
|
|||||||
/** Komplett ausschalten (Ohr abschalten) */
|
/** Komplett ausschalten (Ohr abschalten) */
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
console.log('[WakeWord] Ohr deaktiviert');
|
console.log('[WakeWord] Ohr deaktiviert');
|
||||||
|
this.cancelPassiveListenTimer();
|
||||||
if (this.nativeReady && OpenWakeWord) {
|
if (this.nativeReady && OpenWakeWord) {
|
||||||
try { await OpenWakeWord.stop(); } catch {}
|
try { await OpenWakeWord.stop(); } catch {}
|
||||||
}
|
}
|
||||||
@@ -407,6 +436,17 @@ class WakeWordService {
|
|||||||
this.bargeListening = false;
|
this.bargeListening = false;
|
||||||
import('./logger').then(m => m.reportAppDebug('wake.end',
|
import('./logger').then(m => m.reportAppDebug('wake.end',
|
||||||
`endConversation called, wasBarge=${wasBarge}, nativeReady=${this.nativeReady}`)).catch(()=>{});
|
`endConversation called, wasBarge=${wasBarge}, nativeReady=${this.nativeReady}`)).catch(()=>{});
|
||||||
|
|
||||||
|
// Passive-Listen aktiv? Dann nicht direkt zu armed — passive lauschen
|
||||||
|
// fuer N Sekunden, dann erst Wake-Word wieder aktivieren. Speaker-ID
|
||||||
|
// (Phase 3) filtert fremde Stimmen weg, der User kann ohne erneute
|
||||||
|
// Anrede weitersprechen.
|
||||||
|
const passiveMs = await loadPassiveListenMs();
|
||||||
|
if (passiveMs > 0 && this.nativeReady) {
|
||||||
|
this.enterPassiveListening(passiveMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.nativeReady && OpenWakeWord) {
|
if (this.nativeReady && OpenWakeWord) {
|
||||||
// Wenn wakeword schon laeuft (war Barge-Listener waehrend TTS):
|
// Wenn wakeword schon laeuft (war Barge-Listener waehrend TTS):
|
||||||
// OpenWakeWord.start() ist idempotent (Kotlin checkt running.get()
|
// OpenWakeWord.start() ist idempotent (Kotlin checkt running.get()
|
||||||
@@ -435,6 +475,80 @@ class WakeWordService {
|
|||||||
this.setState('off');
|
this.setState('off');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Eintritt in den Passive-Listen-Modus: state='listening', Timer fuer
|
||||||
|
* Auto-Ende setzen, Callbacks feuern damit ChatScreen die passive
|
||||||
|
* Streaming-Aufnahme startet. OpenWakeWord bleibt AUS (Mic-Exklusivitaet —
|
||||||
|
* audioService braucht das Mikro fuer die passive Aufnahme).
|
||||||
|
* Speaker-ID-Gating (Phase 3) filtert fremde Stimmen auf der Bridge. */
|
||||||
|
private enterPassiveListening(durationMs: number): void {
|
||||||
|
this.cancelPassiveListenTimer();
|
||||||
|
this.setState('listening');
|
||||||
|
const seconds = Math.round(durationMs / 1000);
|
||||||
|
console.log('[WakeWord] Passive-Listen aktiv (%ds) — Speaker-ID gefiltert', seconds);
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.passive',
|
||||||
|
`entered listening for ${seconds}s, cb-count=${this.passiveListenCallbacks.length}`)).catch(()=>{});
|
||||||
|
ToastAndroid.show(`🎧 ${seconds}s lauscht — sprich einfach weiter`, ToastAndroid.SHORT);
|
||||||
|
this.passiveListenTimer = setTimeout(() => {
|
||||||
|
this.passiveListenTimer = null;
|
||||||
|
this.exitPassiveListening('timeout').catch(() => {});
|
||||||
|
}, durationMs);
|
||||||
|
this.passiveListenCallbacks.forEach(cb => {
|
||||||
|
try { cb(); } catch (e) { console.warn('[WakeWord] passive cb err:', e); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verlassen des Passive-Listen-Modus.
|
||||||
|
* reason='speech' → User hat was gesagt (STT-Endpoint mit text) → uebergang
|
||||||
|
* in 'conversing' (Brain antwortet, TTS spielt, dann resume → endConversation
|
||||||
|
* → wieder passive listening, repeat).
|
||||||
|
* reason='timeout' → 30s nichts gehoert → zurueck zu armed (Wake-Word wieder an).
|
||||||
|
* reason='manual' → User hat App geschlossen / stopped → zurueck zu armed. */
|
||||||
|
async exitPassiveListening(reason: 'timeout' | 'speech' | 'manual'): Promise<void> {
|
||||||
|
if (this.state !== 'listening') return;
|
||||||
|
this.cancelPassiveListenTimer();
|
||||||
|
console.log('[WakeWord] Passive-Listen Ende (reason=%s)', reason);
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.passive',
|
||||||
|
`exit reason=${reason}`)).catch(()=>{});
|
||||||
|
|
||||||
|
if (reason === 'speech') {
|
||||||
|
// Wechsel zu 'conversing' damit das Standard-Conversation-Flow greift
|
||||||
|
// (Brain-Response, TTS, resume etc.). Wake-Word bleibt aus (Mic belegt).
|
||||||
|
this.setState('conversing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeout oder manual → Wake-Word reaktivieren, armed-State.
|
||||||
|
if (this.nativeReady && OpenWakeWord) {
|
||||||
|
try {
|
||||||
|
await OpenWakeWord.start();
|
||||||
|
console.log('[WakeWord] zurueck zu armed nach passive-listen');
|
||||||
|
ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
|
||||||
|
this.setState('armed');
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[WakeWord] re-arm nach passive-listen failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState('off');
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelPassiveListenTimer(): void {
|
||||||
|
if (this.passiveListenTimer) {
|
||||||
|
clearTimeout(this.passiveListenTimer);
|
||||||
|
this.passiveListenTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subscribe auf Passive-Listen-Events: feuert wenn der Service in den
|
||||||
|
* passiven Modus eintritt. ChatScreen startet hier eine streaming-
|
||||||
|
* Aufnahme OHNE User-Bubble (passiv lauschen). */
|
||||||
|
onPassiveListen(callback: PassiveListenCallback): () => void {
|
||||||
|
this.passiveListenCallbacks.push(callback);
|
||||||
|
return () => {
|
||||||
|
this.passiveListenCallbacks = this.passiveListenCallbacks.filter(c => c !== callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Wenn ein conversing-State auf einem Wake-Word-Trigger juenger als
|
/** Wenn ein conversing-State auf einem Wake-Word-Trigger juenger als
|
||||||
* maxAgeMs basiert: false-positive verwerfen, zurueck zu armed.
|
* maxAgeMs basiert: false-positive verwerfen, zurueck zu armed.
|
||||||
* Wird vom ChatScreen aufgerufen wenn die App aus laengerem Hintergrund
|
* Wird vom ChatScreen aufgerufen wenn die App aus laengerem Hintergrund
|
||||||
|
|||||||
Reference in New Issue
Block a user