fix: 5er-Bundle — Wake-Word, Spotify-Latenz, File-Limit, Connection-Refused
- WakeWord Doppel-Trigger: detectionInProgress-Guard gegen Native-Event- Race + setBackground/setForeground statt setResumeCooldown im AppState. - Media-Pause beim App-Oeffnen: 1.5s Startup-Suppression im Kotlin emitDetected() — Mikro-Spin-up-Spike triggert kein false-positive mehr. - Spotify Fast-Path im Brain: einfache Media-Commands (naechster Track, pause, play, lauter, ...) matchen via Regex und gehen direkt aufs spotify-Skill statt durch Claude. ~1.5s statt 5-10s pro Befehl. - File-Limit auf 1 GB hochgezogen (war 70 MB). RVS maxPayload + Bridge max_size auf 1500 MB; Node-Heap im RVS-Container auf 4 GB. - TriggerBrowser / Datei-Manager Connection-Refused: brainApi._send fast-failt bei disconnected RVS statt 30s zu timeouten, und beide UIs reloaden automatisch beim Reconnect-Event. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,12 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
private const val EMBEDDING_DIM = 96
|
||||
private const val MEL_BINS = 32
|
||||
private const val DEFAULT_WW_INPUT_FRAMES = 16 // Fallback wenn Modell-Metadata fehlt
|
||||
// Nach record.startRecording() erzeugt das Mikro fuer ~1s einen Spin-up-Spike
|
||||
// (DC-Offset, AGC-Settling) der vom Wake-Word-Klassifikator faelschlich als
|
||||
// Trigger eingestuft werden kann. Folge: App pausiert beim Oeffnen die Musik,
|
||||
// weil der False-Positive die AudioFocus-Switch-Logik anwirft (Stefan-Bug 06/2026).
|
||||
// Loesung: in dieser Phase keine Detections an JS weiterleiten.
|
||||
private const val STARTUP_SUPPRESSION_MS = 1500L
|
||||
}
|
||||
|
||||
private val env: OrtEnvironment = OrtEnvironment.getEnvironment()
|
||||
@@ -95,6 +101,8 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
private val embBuffer: ArrayDeque<FloatArray> = ArrayDeque(32) // Ringpuffer letzter Embeddings
|
||||
private var consecutiveAboveThreshold: Int = 0
|
||||
private var lastDetectionMs: Long = 0L
|
||||
// Zeitpunkt des letzten startRecording — fuer STARTUP_SUPPRESSION_MS-Fenster
|
||||
private var recordingStartedMs: Long = 0L
|
||||
|
||||
/**
|
||||
* Initialisiert die ONNX-Sessions fuer ein bestimmtes Wake-Word.
|
||||
@@ -206,6 +214,7 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
resetInferenceState()
|
||||
running.set(true)
|
||||
record.startRecording()
|
||||
recordingStartedMs = System.currentTimeMillis()
|
||||
|
||||
// PARTIAL_WAKE_LOCK greifen damit die CPU nicht in Doze geht und
|
||||
// die JS-Bridge die emit("WakeWordDetected")-Events live verarbeitet.
|
||||
@@ -313,6 +322,11 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
}
|
||||
|
||||
private fun emitDetected() {
|
||||
val sinceStart = System.currentTimeMillis() - recordingStartedMs
|
||||
if (sinceStart in 0 until STARTUP_SUPPRESSION_MS) {
|
||||
Log.i(TAG, "Wake-Word emit unterdrueckt (sinceStart=${sinceStart}ms < ${STARTUP_SUPPRESSION_MS}ms — Mikro-Spin-up-Spike)")
|
||||
return
|
||||
}
|
||||
val params = com.facebook.react.bridge.Arguments.createMap().apply {
|
||||
putString("model", modelName)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import brainApi, { Trigger } from '../services/brainApi';
|
||||
import rvs from '../services/rvs';
|
||||
|
||||
const COL_ACTIVE = '#34C759';
|
||||
const COL_INACTIVE = '#555570';
|
||||
@@ -65,6 +66,17 @@ export const TriggerBrowser: React.FC = () => {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
// Auto-Reload bei RVS-Reconnect — sonst zeigt die Liste den Fast-Fail-
|
||||
// Fehler aus brainApi ewig an obwohl die Verbindung schon wieder da ist.
|
||||
useEffect(() => {
|
||||
const unsub = rvs.onStateChange((state) => {
|
||||
if (state === 'connected') {
|
||||
load();
|
||||
}
|
||||
});
|
||||
return () => unsub();
|
||||
}, [load]);
|
||||
|
||||
const visible = items.filter(t => {
|
||||
if (filter === 'active') return t.active;
|
||||
if (filter === 'inactive') return !t.active;
|
||||
|
||||
@@ -522,8 +522,9 @@ const ChatScreen: React.FC = () => {
|
||||
const sub = AppState.addEventListener('change', (next) => {
|
||||
if (next === 'background' || next === 'inactive') {
|
||||
lastBackgroundAt = Date.now();
|
||||
wakeWordService.setBackground();
|
||||
} else if (lastState !== 'active' && next === 'active') {
|
||||
wakeWordService.setResumeCooldown(3000);
|
||||
wakeWordService.setForeground();
|
||||
const bgDur = lastBackgroundAt > 0 ? Date.now() - lastBackgroundAt : 0;
|
||||
// Bei laengerer Hintergrund-Zeit (>30s): pruefen ob ein frisches
|
||||
// Wake-Word getriggert wurde wahrend die App weg war — wenn ja,
|
||||
|
||||
@@ -708,6 +708,20 @@ const SettingsScreen: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Datei-Manager: Auto-Reload bei RVS-Reconnect — sonst zeigt das offene
|
||||
// Modal den Fehler "Connection refused" ewig an, obwohl die Verbindung
|
||||
// schon wieder da ist. Triggered nur wenn das Modal gerade offen ist.
|
||||
useEffect(() => {
|
||||
const unsub = rvs.onStateChange((state) => {
|
||||
if (state === 'connected' && fileManagerOpen) {
|
||||
setFileManagerError('');
|
||||
setFileManagerLoading(true);
|
||||
rvs.send('file_list_request' as any, {});
|
||||
}
|
||||
});
|
||||
return () => unsub();
|
||||
}, [fileManagerOpen]);
|
||||
|
||||
// --- QR-Code scannen ---
|
||||
|
||||
const openQRScanner = useCallback(() => {
|
||||
|
||||
@@ -77,6 +77,15 @@ interface SendOpts {
|
||||
|
||||
function _send(path: string, opts: SendOpts = {}): Promise<AnyJson> {
|
||||
_ensureListener();
|
||||
// Fast-Fail wenn RVS nicht verbunden — sonst tickt der Timeout 30s und
|
||||
// der TriggerBrowser / Dateimanager zeigt ne ewig drehende Spinner.
|
||||
// Stefan-Bug 06/2026: "Connection refused, App haengt 30 Sekunden".
|
||||
const rvsState = rvs.getState();
|
||||
if (rvsState !== 'connected') {
|
||||
return Promise.reject(new Error(
|
||||
`Keine Verbindung zum Brain (RVS: ${rvsState}). Warte auf Reconnect...`,
|
||||
));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = _newRequestId();
|
||||
const timer = setTimeout(() => {
|
||||
|
||||
@@ -91,6 +91,18 @@ class WakeWordService {
|
||||
* ein false-positive war (Wake-Word im Hintergrund getriggert waehrend
|
||||
* Stefan gar nicht in der App war). */
|
||||
private lastTriggerAt: number = 0;
|
||||
/** App liegt im Hintergrund — alle Detections sperren. Wird vom
|
||||
* AppState-Listener im ChatScreen via setBackground/setForeground gesetzt.
|
||||
* Hintergrund-Detections sind quasi immer false-positives (TV, Husten,
|
||||
* AudioFocus-Switch beim Wechsel zu Musik etc.). */
|
||||
private inBackground: boolean = false;
|
||||
/** Re-Entry-Guard fuer onWakeDetected: native kann mehrere
|
||||
* WakeWordDetected-Events emitten BEVOR OpenWakeWord.stop() in JS
|
||||
* resolved (Bridge-Queue + Doze-Backlog). Mit dem Flag wird das zweite
|
||||
* Event sofort verworfen. Reset beim Verlassen von 'conversing'.
|
||||
* Ausnahme: bargeListening → Barge-In ist ein legitimer neuer Trigger
|
||||
* waehrend ARIA noch redet, NICHT vom Guard blockieren. */
|
||||
private detectionInProgress: boolean = false;
|
||||
|
||||
private keyword: WakeKeyword = DEFAULT_KEYWORD;
|
||||
private nativeReady: boolean = false;
|
||||
@@ -228,14 +240,44 @@ class WakeWordService {
|
||||
console.log('[WakeWord] Cooldown aktiv fuer %dms', ms);
|
||||
}
|
||||
|
||||
/** App in den Hintergrund: alle Wake-Word-Detections sperren.
|
||||
* Im Hintergrund will Stefan praktisch nie einen neuen Dialog starten —
|
||||
* was als „Wake-Word" reinkommt ist Husten/TV/AudioFocus-Switch. */
|
||||
setBackground(): void {
|
||||
this.inBackground = true;
|
||||
console.log('[WakeWord] App im Hintergrund — Detections gesperrt');
|
||||
}
|
||||
|
||||
/** App im Vordergrund: Detections wieder freigeben, plus 3s Cooldown
|
||||
* als Schutz gegen den AudioFocus-/AudioTrack-Spike der direkt nach
|
||||
* dem Resume kommt. Ersetzt das alte setResumeCooldown(3000)-Pattern. */
|
||||
setForeground(): void {
|
||||
this.inBackground = false;
|
||||
this.cooldownUntilMs = Date.now() + 3000;
|
||||
console.log('[WakeWord] App im Vordergrund — Cooldown 3s aktiv');
|
||||
}
|
||||
|
||||
/** Wake-Word getriggert: Native-Modul pausieren, Konversation starten. */
|
||||
private async onWakeDetected(): Promise<void> {
|
||||
if (this.inBackground) {
|
||||
console.log('[WakeWord] Trigger ignoriert (App im Hintergrund)');
|
||||
import('./logger').then(m => m.reportAppDebug('wake.detect', 'ignored: app in background')).catch(()=>{});
|
||||
return;
|
||||
}
|
||||
// Re-Entry-Guard: blocken wenn ein Detection-Zyklus schon laeuft.
|
||||
// Ausnahme: Barge-In waehrend ARIA-TTS ist ein legitimer neuer Trigger.
|
||||
if (this.detectionInProgress && !this.bargeListening) {
|
||||
console.log('[WakeWord] Trigger ignoriert (Detection-Zyklus laeuft schon — Native-Doppel-Event-Race)');
|
||||
import('./logger').then(m => m.reportAppDebug('wake.detect', 'ignored: detectionInProgress')).catch(()=>{});
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now < this.cooldownUntilMs) {
|
||||
const left = this.cooldownUntilMs - now;
|
||||
console.log('[WakeWord] Trigger ignoriert (Cooldown noch %dms aktiv — wahrscheinlich App-Resume-Spike)', left);
|
||||
return;
|
||||
}
|
||||
this.detectionInProgress = true;
|
||||
console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)',
|
||||
this.keyword, this.state, this.bargeListening);
|
||||
import('./logger').then(m => m.reportAppDebug('wake.detect',
|
||||
@@ -503,7 +545,12 @@ class WakeWordService {
|
||||
|
||||
private setState(state: WakeWordState): void {
|
||||
if (this.state !== state) {
|
||||
const wasConversing = this.state === 'conversing';
|
||||
this.state = state;
|
||||
// Re-Entry-Guard freigeben sobald wir 'conversing' verlassen — Zyklus ist durch
|
||||
if (wasConversing && state !== 'conversing') {
|
||||
this.detectionInProgress = false;
|
||||
}
|
||||
this.stateCallbacks.forEach(cb => cb(state));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user