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:
@@ -171,6 +171,39 @@ export async function saveBargeInEnabled(enabled: boolean): Promise<void> {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** One-Shot-Transkription eines PCM-Schnipsels (base64, s16le 16kHz mono) via
|
||||
* Voxtral — fuer die Wake-Wort-Bestaetigung. Schickt stt_transcribe_blob und
|
||||
* wartet auf stt_transcribe_result (matching requestId) mit Timeout.
|
||||
* Rueckgabe: Text (evtl. '') bei Antwort, oder null bei Timeout/Fehler →
|
||||
* Aufrufer macht dann fail-open (Wake normal durchlassen). */
|
||||
export async function transcribeBlob(pcmBase64: string, timeoutMs = 2500): Promise<string | null> {
|
||||
if (!pcmBase64) return null;
|
||||
const requestId = `wakeverify_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
return new Promise<string | null>((resolve) => {
|
||||
let done = false;
|
||||
let unsub: (() => void) | null = null;
|
||||
const timer = setTimeout(() => finish(null), timeoutMs);
|
||||
function finish(val: string | null) {
|
||||
if (done) return;
|
||||
done = true;
|
||||
try { unsub && unsub(); } catch {}
|
||||
clearTimeout(timer);
|
||||
resolve(val);
|
||||
}
|
||||
try {
|
||||
unsub = rvs.onMessage((msg: any) => {
|
||||
if (msg?.type !== 'stt_transcribe_result') return;
|
||||
const p = (msg as any).payload || {};
|
||||
if (String(p.requestId || '') !== requestId) return;
|
||||
finish(typeof p.text === 'string' ? p.text : '');
|
||||
});
|
||||
rvs.send('stt_transcribe_blob' as any, { requestId, pcm: pcmBase64, language: 'de' });
|
||||
} catch {
|
||||
finish(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadSttEndpointMs(): Promise<number> {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(STT_ENDPOINT_STORAGE_KEY);
|
||||
|
||||
Reference in New Issue
Block a user