diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 766154b..5a0f43b 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -35,7 +35,7 @@ import MemoryBrowser from '../components/MemoryBrowser'; import ErrorBoundary from '../components/ErrorBoundary'; import rvs, { RVSMessage, ConnectionState } from '../services/rvs'; import audioService from '../services/audio'; -import wakeWordService, { loadPassiveListenMs } from '../services/wakeword'; +import wakeWordService from '../services/wakeword'; import ProjectsBrowser from '../components/ProjectsBrowser'; import brainApi, { Project as BrainProject } from '../services/brainApi'; import projectFocus from '../services/projectFocus'; @@ -50,7 +50,7 @@ import VoiceButton from '../components/VoiceButton'; import FileUpload, { FileData } from '../components/FileUpload'; import CameraUpload, { PhotoData } from '../components/CameraUpload'; import MessageText from '../components/MessageText'; -import { loadConvWindowMs, loadTtsSpeed, TTS_SPEED_DEFAULT, loadSttEndpointMs, loadMaxRecordingMs, loadBargeInEnabled } from '../services/audio'; +import { loadTtsSpeed, TTS_SPEED_DEFAULT, loadSttEndpointMs, loadMaxRecordingMs, loadBargeInEnabled } from '../services/audio'; import Geolocation from '@react-native-community/geolocation'; // --- Typen --- diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 7bd9e76..b3a702f 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -63,10 +63,6 @@ import { VAD_SILENCE_MIN_SEC, VAD_SILENCE_MAX_SEC, VAD_SILENCE_STORAGE_KEY, - CONV_WINDOW_DEFAULT_SEC, - CONV_WINDOW_MIN_SEC, - CONV_WINDOW_MAX_SEC, - CONV_WINDOW_STORAGE_KEY, STT_ENDPOINT_DEFAULT_MS, STT_ENDPOINT_MIN_MS, STT_ENDPOINT_MAX_MS, @@ -116,9 +112,6 @@ import wakeWordService, { WAKE_THRESHOLD_MAX, loadWakeThreshold, saveWakeThreshold, - PASSIVE_LISTEN_DEFAULT_MS, - loadPassiveListenMs, - savePassiveListenMs, } from '../services/wakeword'; import ModeSelector from '../components/ModeSelector'; import QRScanner from '../components/QRScanner'; @@ -204,7 +197,6 @@ const SettingsScreen: React.FC = () => { // Aktive Streaming-Pausen-Toleranz (STT_ENDPOINT) — der "Stille-Toleranz"-Regler // steuert jetzt DIESEN Wert (der alte vadSilenceSec war der tote Legacy-dB-Pfad). const [sttEndpointSec, setSttEndpointSec] = useState(STT_ENDPOINT_DEFAULT_MS / 1000); - const [convWindowSec, setConvWindowSec] = useState(CONV_WINDOW_DEFAULT_SEC); const [maxRecordingSec, setMaxRecordingSec] = useState(MAX_RECORDING_DEFAULT_SEC); // Barge-in: ARIA waehrend ihrer Antwort unterbrechen duerfen. Default aus (Halb-Duplex). const [bargeIn, setBargeIn] = useState(false); @@ -220,7 +212,6 @@ const SettingsScreen: React.FC = () => { const [wakeStatus, setWakeStatus] = useState(''); const [wakeReadySound, setWakeReadySound] = useState(true); const [wakeThreshold, setWakeThreshold] = useState(WAKE_THRESHOLD_DEFAULT); - const [passiveSec, setPassiveSec] = useState(Math.round(PASSIVE_LISTEN_DEFAULT_MS / 1000)); const [editingPath, setEditingPath] = useState(false); const [xttsVoice, setXttsVoice] = useState(''); const [loadingVoice, setLoadingVoice] = useState(null); @@ -309,14 +300,6 @@ const SettingsScreen: React.FC = () => { } } }); - AsyncStorage.getItem(CONV_WINDOW_STORAGE_KEY).then(saved => { - if (saved != null) { - const n = parseFloat(saved); - if (isFinite(n) && n >= CONV_WINDOW_MIN_SEC && n <= CONV_WINDOW_MAX_SEC) { - setConvWindowSec(n); - } - } - }); AsyncStorage.getItem(STT_ENDPOINT_STORAGE_KEY).then(saved => { if (saved != null) { const n = parseInt(saved, 10); @@ -353,7 +336,6 @@ const SettingsScreen: React.FC = () => { }); isWakeReadySoundEnabled().then(setWakeReadySound); loadWakeThreshold().then(setWakeThreshold).catch(() => {}); - loadPassiveListenMs().then(ms => setPassiveSec(Math.round(ms / 1000))).catch(() => {}); updateService.getApkCacheSize().then(setApkCacheInfo).catch(() => {}); audioService.getTtsCacheSize().then(setTtsCacheInfo).catch(() => {}); AsyncStorage.getItem('aria_xtts_voice').then(saved => { @@ -1721,39 +1703,6 @@ const SettingsScreen: React.FC = () => { - Konversations-Fenster - - Im Gespraechsmodus (Ohr-Button): nach ARIA's Antwort hast du so lange - Zeit, weiter zu sprechen, bevor die Konversation automatisch beendet wird. - Sprichst du nichts → Mikrofon zu. - Default: {CONV_WINDOW_DEFAULT_SEC.toFixed(1)}s. - - - { - const next = Math.max(CONV_WINDOW_MIN_SEC, Math.round((convWindowSec - 1) * 10) / 10); - setConvWindowSec(next); - AsyncStorage.setItem(CONV_WINDOW_STORAGE_KEY, String(next)); - }} - disabled={convWindowSec <= CONV_WINDOW_MIN_SEC} - > - −1 - - {convWindowSec.toFixed(0)} s - { - const next = Math.min(CONV_WINDOW_MAX_SEC, Math.round((convWindowSec + 1) * 10) / 10); - setConvWindowSec(next); - AsyncStorage.setItem(CONV_WINDOW_STORAGE_KEY, String(next)); - }} - disabled={convWindowSec >= CONV_WINDOW_MAX_SEC} - > - +1 - - - Maximale Aufnahmedauer Notbremse: nach so vielen Minuten wird die Aufnahme automatisch beendet, @@ -1945,38 +1894,14 @@ const SettingsScreen: React.FC = () => { /> - Weiterreden-Fenster (Gespraech) + Weiterreden nach der Antwort - Nach einer gesprochenen ARIA-Antwort kannst du so lange einfach - weiterreden — ohne Wake-Word — bevor zurueck aufs Wake-Word geschaltet - wird. Reine Steuerbefehle (z.B. „nächster Titel") beenden sofort. - Default: {Math.round(PASSIVE_LISTEN_DEFAULT_MS / 1000)}s. + Nach einer gesprochenen ARIA-Antwort geht das Mikro auf — du kannst ohne + Wake-Word weiterreden. Fängst du nicht innerhalb der „Stille-Toleranz" + (Sektion Spracheingabe) an, geht's zurück aufs Wake-Word. Reine + Steuerbefehle beenden sofort. Ein separates Zeitfenster gibt es nicht + mehr — es zählt überall derselbe Stille-Wert. - - { - const next = Math.max(10, passiveSec - 5); - setPassiveSec(next); - savePassiveListenMs(next * 1000); - }} - disabled={passiveSec <= 10} - > - −5 - - {passiveSec} s - { - const next = Math.min(60, passiveSec + 5); - setPassiveSec(next); - savePassiveListenMs(next * 1000); - }} - disabled={passiveSec >= 60} - > - +5 - - )} diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index 9f0fb08..d0cb973 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -143,14 +143,6 @@ export const VAD_SILENCE_MIN_SEC = 1.0; export const VAD_SILENCE_MAX_SEC = 8.0; export const VAD_SILENCE_STORAGE_KEY = 'aria_vad_silence_sec'; -// Konversations-Fenster (in Sekunden) — nach ARIA's Antwort hat der User so -// lange Zeit, im Gespraechsmodus weiter zu sprechen, ohne dass die Konversation -// beendet wird. Sprichst du im Fenster nichts → Konversation aus. -export const CONV_WINDOW_DEFAULT_SEC = 8.0; -export const CONV_WINDOW_MIN_SEC = 3.0; -export const CONV_WINDOW_MAX_SEC = 20.0; -export const CONV_WINDOW_STORAGE_KEY = 'aria_conv_window_sec'; - // STT-Endpoint (ms Stille bis "fertig gesprochen"). Zu kurz = schneidet mitten // im Satz ab, besonders im Auto oder wenn man zum Nachdenken pausiert. 1500 war // zu aggressiv; 2400 default, bis 8s hoch stellbar (Denkpausen). In den Settings @@ -208,18 +200,6 @@ export async function loadTtsSpeed(): Promise { return TTS_SPEED_DEFAULT; } -export async function loadConvWindowMs(): Promise { - try { - const raw = await AsyncStorage.getItem(CONV_WINDOW_STORAGE_KEY); - if (raw != null) { - const n = parseFloat(raw); - if (isFinite(n) && n >= CONV_WINDOW_MIN_SEC && n <= CONV_WINDOW_MAX_SEC) { - return Math.round(n * 1000); - } - } - } catch {} - return Math.round(CONV_WINDOW_DEFAULT_SEC * 1000); -} async function loadVadSilenceMs(): Promise { try { diff --git a/android/src/services/wakeword.ts b/android/src/services/wakeword.ts index b3071bd..6a83f48 100644 --- a/android/src/services/wakeword.ts +++ b/android/src/services/wakeword.ts @@ -30,26 +30,12 @@ type PassiveListenCallback = () => void; 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 { - 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 { - await AsyncStorage.setItem(PASSIVE_LISTEN_STORAGE_KEY, String(ms)); -} +/** Backstop-Dauer fuer den Passive-Listen-Modus nach einer Antwort. Das echte + * Ende regelt die Stille-Toleranz (die passive Aufnahme endet nach dem einen + * No-Speech-Fenster → ChatScreen ruft exitPassiveListening). Dieser Timer ist + * nur die Notbremse, falls die Aufnahme mal nicht sauber endpointet — daher + * fix und kurz, kein separater Nutzer-Wert mehr (das alte 30s-Fenster ist raus). */ +const PASSIVE_BACKSTOP_MS = 15_000; export const WAKE_KEYWORD_STORAGE = 'aria_wake_keyword'; @@ -152,8 +138,9 @@ class WakeWordService { * Ausnahme: bargeListening → Barge-In ist ein legitimer neuer Trigger * waehrend ARIA noch redet, NICHT vom Guard blockieren. */ private detectionInProgress: boolean = false; - /** Passive-Listen-Timer: feuert nach PASSIVE_LISTEN_MS ohne Stefan-Speech, - * beendet den listening-State und geht zurueck zu armed. */ + /** Passive-Listen-Backstop-Timer: Notbremse (PASSIVE_BACKSTOP_MS). Normal endet + * das Fenster ueber die Stille-Toleranz der Aufnahme; feuert dieser Timer + * trotzdem, zurueck zu armed. */ private passiveListenTimer: ReturnType | null = null; /** Callbacks fuer den Eintritt in Passive-Listen — ChatScreen startet * hier eine streaming-Aufnahme OHNE User-Bubble (passiv lauschen). */ @@ -489,13 +476,12 @@ class WakeWordService { import('./logger').then(m => m.reportAppDebug('wake.end', `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 (!skipPassive && passiveMs > 0 && this.nativeReady) { - this.enterPassiveListening(passiveMs); + // Kein skipPassive? Dann EIN Stille-Fenster zum Weiterreden (kein Wake-Word + // noetig). Das echte Ende regelt die Stille-Toleranz der passiven Aufnahme; + // der Backstop-Timer ist nur die Notbremse. Der User kann ohne erneute + // Anrede weitersprechen; sagt er nichts → zurueck aufs Wake-Word. + if (!skipPassive && this.nativeReady) { + this.enterPassiveListening(PASSIVE_BACKSTOP_MS); return; } @@ -537,10 +523,10 @@ class WakeWordService { this.cancelPassiveListenTimer(); this.setState('listening'); const seconds = Math.round(durationMs / 1000); - console.log('[WakeWord] Passive-Listen aktiv (%ds) — Speaker-ID gefiltert', seconds); + console.log('[WakeWord] Passive-Listen aktiv (Backstop %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); + `entered listening (backstop ${seconds}s), cb-count=${this.passiveListenCallbacks.length}`)).catch(()=>{}); + ToastAndroid.show('🎧 sprich einfach weiter', ToastAndroid.SHORT); this.passiveListenTimer = setTimeout(() => { this.passiveListenTimer = null; this.exitPassiveListening('timeout').catch(() => {});