feat(wake): Voxtral-Bestätigungsstufe gegen Musik-Fehltrigger

openWakeWord triggert oft auf Musik (Pet Shop Boys "West End Girls" & Co.). Neu:
nach dem Trigger prüft Voxtral den Vor-Trigger-Audio ("war das wirklich das
Wake-Wort oder nur Musik?") — erst bei Bestätigung Gong + Mikro, sonst still
verworfen + re-arm. Kostet ~0,5-1s vor dem Gong.

- Native (OpenWakeWordModule.kt): 2s Roh-PCM-Ringpuffer; bei Erkennung wird der
  1,5s-Vor-Trigger-Schnipsel base64 (s16le 16kHz) ans WakeWordDetected-Event
  gehängt (preTriggerPcm).
- Bridge (voxtral): neuer One-Shot-Handler stt_transcribe_blob → Silero + Voxtral
  → stt_transcribe_result. Musik/Rauschen → leerer Text.
- App: audio.transcribeBlob() schickt den Schnipsel, wakeword.confirmWake() prüft
  ob das distinktive Keyword (>=4 Zeichen) im Transkript steht. Gate sitzt in
  onWakeDetected VOR Gong/Dialog. Setting "Wake-Wort per Voxtral bestätigen"
  (default aus, opt-in).

FAIL-OPEN durchgängig: kein preTriggerPcm (altes APK) / Timeout / Fehler / VAD
weg → Wake läuft normal durch. Nie schlechter als heute. NATIVER TEIL UNGETESTET
(kein Gerät hier) — braucht Build + Test auf dem Handy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 21:37:15 +02:00
co-authored by Claude Opus 4.8
parent a3650bb0e4
commit 49ea172617
5 changed files with 222 additions and 6 deletions
+27
View File
@@ -114,6 +114,8 @@ import wakeWordService, {
saveWakeThreshold,
loadBgWakeEnabled,
saveBgWakeEnabled,
loadWakeConfirmEnabled,
saveWakeConfirmEnabled,
} from '../services/wakeword';
import ModeSelector from '../components/ModeSelector';
import QRScanner from '../components/QRScanner';
@@ -216,6 +218,8 @@ const SettingsScreen: React.FC = () => {
const [wakeThreshold, setWakeThreshold] = useState<number>(WAKE_THRESHOLD_DEFAULT);
// Hintergrund-Wake: auch bei gesperrtem Bildschirm auf das Wake-Wort hoeren. Default aus.
const [bgWake, setBgWake] = useState<boolean>(false);
// Wake-Wort per Voxtral bestaetigen (gegen Musik-Fehltrigger). Default aus.
const [wakeConfirm, setWakeConfirm] = useState<boolean>(false);
const [editingPath, setEditingPath] = useState(false);
const [xttsVoice, setXttsVoice] = useState('');
const [loadingVoice, setLoadingVoice] = useState<string | null>(null);
@@ -341,6 +345,7 @@ const SettingsScreen: React.FC = () => {
isWakeReadySoundEnabled().then(setWakeReadySound);
loadWakeThreshold().then(setWakeThreshold).catch(() => {});
loadBgWakeEnabled().then(setBgWake).catch(() => {});
loadWakeConfirmEnabled().then(setWakeConfirm).catch(() => {});
updateService.getApkCacheSize().then(setApkCacheInfo).catch(() => {});
audioService.getTtsCacheSize().then(setTtsCacheInfo).catch(() => {});
AsyncStorage.getItem('aria_xtts_voice').then(saved => {
@@ -1921,6 +1926,28 @@ const SettingsScreen: React.FC = () => {
/>
</View>
<View style={[styles.toggleRow, {marginTop: 20, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 16}]}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>Wake-Wort per Voxtral bestätigen</Text>
<Text style={styles.toggleHint}>
Gegen Musik-Fehltrigger: nach {KEYWORD_LABELS[wakeKeyword as keyof typeof KEYWORD_LABELS] || wakeKeyword}"
prüft Voxtral kurz nach, ob's wirklich das Wake-Wort war (oder nur
Musik/TV) — erst dann Gong + Mikro. Kostet ~0,51 s Extra vor dem
Gong. Empfohlen zusammen mit Hintergrund-Zuhören.
</Text>
</View>
<Switch
value={wakeConfirm}
onValueChange={(val) => {
setWakeConfirm(val);
saveWakeConfirmEnabled(val).catch(() => {});
wakeWordService.setWakeConfirmEnabled(val);
}}
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
thumbColor={wakeConfirm ? '#FFFFFF' : '#666680'}
/>
</View>
<Text style={[styles.toggleLabel, {marginTop: 20}]}>Weiterreden nach der Antwort</Text>
<Text style={styles.toggleHint}>
Nach einer gesprochenen ARIA-Antwort geht das Mikro auf — du kannst ohne