refactor(voice): tote Fenster-Settings ausgeräumt (Konv-Fenster/Passiv-Lauschen)

Nachdem die Stille-Toleranz jetzt überall der einzige Wert ist, sind die alten
Settings obsolet — raus damit:

- audio.ts: CONV_WINDOW_* Konstanten + loadConvWindowMs() entfernt.
- wakeword.ts: PASSIVE_LISTEN_* + load/savePassiveListenMs() entfernt; der Passiv-
  Master-Timer nutzt jetzt einen festen internen Backstop (PASSIVE_BACKSTOP_MS,
  15s) statt eines Nutzer-Werts — das echte Ende regelt die Stille-Toleranz.
- SettingsScreen: "Konversations-Fenster"- und "Weiterreden-Fenster"-Slider raus;
  Letzterer durch eine kurze Erklärung ersetzt (verweist auf Stille-Toleranz).
- ChatScreen: tote Imports weg.

Kein Verhaltensänderung ggü. ebe0e80 — nur Aufräumen. tsc grün.

Deploy: neue APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 01:53:55 +02:00
co-authored by Claude Opus 4.8
parent 5f09e2bca3
commit 9636a702d3
4 changed files with 26 additions and 135 deletions
+2 -2
View File
@@ -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, { loadPassiveListenMs } from '../services/wakeword'; import wakeWordService from '../services/wakeword';
import ProjectsBrowser from '../components/ProjectsBrowser'; import ProjectsBrowser from '../components/ProjectsBrowser';
import brainApi, { Project as BrainProject } from '../services/brainApi'; import brainApi, { Project as BrainProject } from '../services/brainApi';
import projectFocus from '../services/projectFocus'; import projectFocus from '../services/projectFocus';
@@ -50,7 +50,7 @@ import VoiceButton from '../components/VoiceButton';
import FileUpload, { FileData } from '../components/FileUpload'; import FileUpload, { FileData } from '../components/FileUpload';
import CameraUpload, { PhotoData } from '../components/CameraUpload'; import CameraUpload, { PhotoData } from '../components/CameraUpload';
import MessageText from '../components/MessageText'; 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'; import Geolocation from '@react-native-community/geolocation';
// --- Typen --- // --- Typen ---
+6 -81
View File
@@ -63,10 +63,6 @@ import {
VAD_SILENCE_MIN_SEC, VAD_SILENCE_MIN_SEC,
VAD_SILENCE_MAX_SEC, VAD_SILENCE_MAX_SEC,
VAD_SILENCE_STORAGE_KEY, 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_DEFAULT_MS,
STT_ENDPOINT_MIN_MS, STT_ENDPOINT_MIN_MS,
STT_ENDPOINT_MAX_MS, STT_ENDPOINT_MAX_MS,
@@ -116,9 +112,6 @@ import wakeWordService, {
WAKE_THRESHOLD_MAX, WAKE_THRESHOLD_MAX,
loadWakeThreshold, loadWakeThreshold,
saveWakeThreshold, saveWakeThreshold,
PASSIVE_LISTEN_DEFAULT_MS,
loadPassiveListenMs,
savePassiveListenMs,
} from '../services/wakeword'; } from '../services/wakeword';
import ModeSelector from '../components/ModeSelector'; import ModeSelector from '../components/ModeSelector';
import QRScanner from '../components/QRScanner'; import QRScanner from '../components/QRScanner';
@@ -204,7 +197,6 @@ const SettingsScreen: React.FC = () => {
// Aktive Streaming-Pausen-Toleranz (STT_ENDPOINT) — der "Stille-Toleranz"-Regler // Aktive Streaming-Pausen-Toleranz (STT_ENDPOINT) — der "Stille-Toleranz"-Regler
// steuert jetzt DIESEN Wert (der alte vadSilenceSec war der tote Legacy-dB-Pfad). // steuert jetzt DIESEN Wert (der alte vadSilenceSec war der tote Legacy-dB-Pfad).
const [sttEndpointSec, setSttEndpointSec] = useState<number>(STT_ENDPOINT_DEFAULT_MS / 1000); const [sttEndpointSec, setSttEndpointSec] = useState<number>(STT_ENDPOINT_DEFAULT_MS / 1000);
const [convWindowSec, setConvWindowSec] = useState<number>(CONV_WINDOW_DEFAULT_SEC);
const [maxRecordingSec, setMaxRecordingSec] = useState<number>(MAX_RECORDING_DEFAULT_SEC); const [maxRecordingSec, setMaxRecordingSec] = useState<number>(MAX_RECORDING_DEFAULT_SEC);
// Barge-in: ARIA waehrend ihrer Antwort unterbrechen duerfen. Default aus (Halb-Duplex). // Barge-in: ARIA waehrend ihrer Antwort unterbrechen duerfen. Default aus (Halb-Duplex).
const [bargeIn, setBargeIn] = useState<boolean>(false); const [bargeIn, setBargeIn] = useState<boolean>(false);
@@ -220,7 +212,6 @@ const SettingsScreen: React.FC = () => {
const [wakeStatus, setWakeStatus] = useState<string>(''); const [wakeStatus, setWakeStatus] = useState<string>('');
const [wakeReadySound, setWakeReadySound] = useState<boolean>(true); const [wakeReadySound, setWakeReadySound] = useState<boolean>(true);
const [wakeThreshold, setWakeThreshold] = useState<number>(WAKE_THRESHOLD_DEFAULT); const [wakeThreshold, setWakeThreshold] = useState<number>(WAKE_THRESHOLD_DEFAULT);
const [passiveSec, setPassiveSec] = useState<number>(Math.round(PASSIVE_LISTEN_DEFAULT_MS / 1000));
const [editingPath, setEditingPath] = useState(false); const [editingPath, setEditingPath] = useState(false);
const [xttsVoice, setXttsVoice] = useState(''); const [xttsVoice, setXttsVoice] = useState('');
const [loadingVoice, setLoadingVoice] = useState<string | null>(null); const [loadingVoice, setLoadingVoice] = useState<string | null>(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 => { AsyncStorage.getItem(STT_ENDPOINT_STORAGE_KEY).then(saved => {
if (saved != null) { if (saved != null) {
const n = parseInt(saved, 10); const n = parseInt(saved, 10);
@@ -353,7 +336,6 @@ const SettingsScreen: React.FC = () => {
}); });
isWakeReadySoundEnabled().then(setWakeReadySound); isWakeReadySoundEnabled().then(setWakeReadySound);
loadWakeThreshold().then(setWakeThreshold).catch(() => {}); loadWakeThreshold().then(setWakeThreshold).catch(() => {});
loadPassiveListenMs().then(ms => setPassiveSec(Math.round(ms / 1000))).catch(() => {});
updateService.getApkCacheSize().then(setApkCacheInfo).catch(() => {}); updateService.getApkCacheSize().then(setApkCacheInfo).catch(() => {});
audioService.getTtsCacheSize().then(setTtsCacheInfo).catch(() => {}); audioService.getTtsCacheSize().then(setTtsCacheInfo).catch(() => {});
AsyncStorage.getItem('aria_xtts_voice').then(saved => { AsyncStorage.getItem('aria_xtts_voice').then(saved => {
@@ -1721,39 +1703,6 @@ const SettingsScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={[styles.toggleLabel, {marginTop: 24}]}>Konversations-Fenster</Text>
<Text style={styles.toggleHint}>
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.
</Text>
<View style={styles.prerollRow}>
<TouchableOpacity
style={styles.prerollButton}
onPress={() => {
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}
>
<Text style={styles.prerollButtonText}>1</Text>
</TouchableOpacity>
<Text style={styles.prerollValue}>{convWindowSec.toFixed(0)} s</Text>
<TouchableOpacity
style={styles.prerollButton}
onPress={() => {
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}
>
<Text style={styles.prerollButtonText}>+1</Text>
</TouchableOpacity>
</View>
<Text style={[styles.toggleLabel, {marginTop: 24}]}>Maximale Aufnahmedauer</Text> <Text style={[styles.toggleLabel, {marginTop: 24}]}>Maximale Aufnahmedauer</Text>
<Text style={styles.toggleHint}> <Text style={styles.toggleHint}>
Notbremse: nach so vielen Minuten wird die Aufnahme automatisch beendet, Notbremse: nach so vielen Minuten wird die Aufnahme automatisch beendet,
@@ -1945,38 +1894,14 @@ const SettingsScreen: React.FC = () => {
/> />
</View> </View>
<Text style={[styles.toggleLabel, {marginTop: 20}]}>Weiterreden-Fenster (Gespraech)</Text> <Text style={[styles.toggleLabel, {marginTop: 20}]}>Weiterreden nach der Antwort</Text>
<Text style={styles.toggleHint}> <Text style={styles.toggleHint}>
Nach einer gesprochenen ARIA-Antwort kannst du so lange einfach Nach einer gesprochenen ARIA-Antwort geht das Mikro auf du kannst ohne
weiterreden — ohne Wake-Word — bevor zurueck aufs Wake-Word geschaltet Wake-Word weiterreden. Fängst du nicht innerhalb der Stille-Toleranz"
wird. Reine Steuerbefehle (z.B. „nächster Titel") beenden sofort. (Sektion Spracheingabe) an, geht's zurück aufs Wake-Word. Reine
Default: {Math.round(PASSIVE_LISTEN_DEFAULT_MS / 1000)}s. Steuerbefehle beenden sofort. Ein separates Zeitfenster gibt es nicht
mehr — es zählt überall derselbe Stille-Wert.
</Text> </Text>
<View style={styles.prerollRow}>
<TouchableOpacity
style={styles.prerollButton}
onPress={() => {
const next = Math.max(10, passiveSec - 5);
setPassiveSec(next);
savePassiveListenMs(next * 1000);
}}
disabled={passiveSec <= 10}
>
<Text style={styles.prerollButtonText}>5</Text>
</TouchableOpacity>
<Text style={styles.prerollValue}>{passiveSec} s</Text>
<TouchableOpacity
style={styles.prerollButton}
onPress={() => {
const next = Math.min(60, passiveSec + 5);
setPassiveSec(next);
savePassiveListenMs(next * 1000);
}}
disabled={passiveSec >= 60}
>
<Text style={styles.prerollButtonText}>+5</Text>
</TouchableOpacity>
</View>
</View> </View>
</>)} </>)}
-20
View File
@@ -143,14 +143,6 @@ export const VAD_SILENCE_MIN_SEC = 1.0;
export const VAD_SILENCE_MAX_SEC = 8.0; export const VAD_SILENCE_MAX_SEC = 8.0;
export const VAD_SILENCE_STORAGE_KEY = 'aria_vad_silence_sec'; 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 // 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 // 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 // zu aggressiv; 2400 default, bis 8s hoch stellbar (Denkpausen). In den Settings
@@ -208,18 +200,6 @@ export async function loadTtsSpeed(): Promise<number> {
return TTS_SPEED_DEFAULT; return TTS_SPEED_DEFAULT;
} }
export async function loadConvWindowMs(): Promise<number> {
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<number> { async function loadVadSilenceMs(): Promise<number> {
try { try {
+18 -32
View File
@@ -30,26 +30,12 @@ type PassiveListenCallback = () => void;
export type WakeWordState = 'off' | 'armed' | 'conversing' | 'listening'; export type WakeWordState = 'off' | 'armed' | 'conversing' | 'listening';
/** Default-Dauer fuer den Passive-Listen-Modus nach einer Konversation — /** Backstop-Dauer fuer den Passive-Listen-Modus nach einer Antwort. Das echte
* in dem Fenster braucht's kein Wake-Word, Speaker-ID-Filter haelt * Ende regelt die Stille-Toleranz (die passive Aufnahme endet nach dem einen
* fremde Stimmen raus (TV, Familie). 30s default; konfigurierbar. */ * No-Speech-Fenster → ChatScreen ruft exitPassiveListening). Dieser Timer ist
export const PASSIVE_LISTEN_DEFAULT_MS = 30_000; * nur die Notbremse, falls die Aufnahme mal nicht sauber endpointet — daher
export const PASSIVE_LISTEN_STORAGE_KEY = 'aria_passive_listen_ms'; * fix und kurz, kein separater Nutzer-Wert mehr (das alte 30s-Fenster ist raus). */
const PASSIVE_BACKSTOP_MS = 15_000;
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';
@@ -152,8 +138,9 @@ 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, /** Passive-Listen-Backstop-Timer: Notbremse (PASSIVE_BACKSTOP_MS). Normal endet
* beendet den listening-State und geht zurueck zu armed. */ * das Fenster ueber die Stille-Toleranz der Aufnahme; feuert dieser Timer
* trotzdem, zurueck zu armed. */
private passiveListenTimer: ReturnType<typeof setTimeout> | null = null; private passiveListenTimer: ReturnType<typeof setTimeout> | null = null;
/** Callbacks fuer den Eintritt in Passive-Listen — ChatScreen startet /** Callbacks fuer den Eintritt in Passive-Listen — ChatScreen startet
* hier eine streaming-Aufnahme OHNE User-Bubble (passiv lauschen). */ * hier eine streaming-Aufnahme OHNE User-Bubble (passiv lauschen). */
@@ -489,13 +476,12 @@ class WakeWordService {
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 // Kein skipPassive? Dann EIN Stille-Fenster zum Weiterreden (kein Wake-Word
// fuer N Sekunden, dann erst Wake-Word wieder aktivieren. Speaker-ID // noetig). Das echte Ende regelt die Stille-Toleranz der passiven Aufnahme;
// (Phase 3) filtert fremde Stimmen weg, der User kann ohne erneute // der Backstop-Timer ist nur die Notbremse. Der User kann ohne erneute
// Anrede weitersprechen. // Anrede weitersprechen; sagt er nichts → zurueck aufs Wake-Word.
const passiveMs = await loadPassiveListenMs(); if (!skipPassive && this.nativeReady) {
if (!skipPassive && passiveMs > 0 && this.nativeReady) { this.enterPassiveListening(PASSIVE_BACKSTOP_MS);
this.enterPassiveListening(passiveMs);
return; return;
} }
@@ -537,10 +523,10 @@ class WakeWordService {
this.cancelPassiveListenTimer(); this.cancelPassiveListenTimer();
this.setState('listening'); this.setState('listening');
const seconds = Math.round(durationMs / 1000); 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', import('./logger').then(m => m.reportAppDebug('wake.passive',
`entered listening for ${seconds}s, cb-count=${this.passiveListenCallbacks.length}`)).catch(()=>{}); `entered listening (backstop ${seconds}s), cb-count=${this.passiveListenCallbacks.length}`)).catch(()=>{});
ToastAndroid.show(`🎧 ${seconds}s lauscht — sprich einfach weiter`, ToastAndroid.SHORT); ToastAndroid.show('🎧 sprich einfach weiter', ToastAndroid.SHORT);
this.passiveListenTimer = setTimeout(() => { this.passiveListenTimer = setTimeout(() => {
this.passiveListenTimer = null; this.passiveListenTimer = null;
this.exitPassiveListening('timeout').catch(() => {}); this.exitPassiveListening('timeout').catch(() => {});