Compare commits

...

2 Commits

Author SHA1 Message Date
duffyduck be373466a3 fix: klares UI-Feedback fuer Wake-Word-State
Stefan's Verwirrung: Ohr-Button + KEIN Porcupine = Direkt-Aufnahme,
nicht passives Lauschen. Wenn er lange wartet, schnappt das Mikro
Hintergrundgeraeusche/Sprache auf, sendet ab, Ohr aus. Sah aus wie
"Wake-Word triggerte" — war aber stinknormales Recording.

Fixes fuer klares Feedback:
- Toast bei jedem State-Wechsel:
  * Direkt-Aufnahme (kein Porcupine): "Wake-Word nicht aktiv —
    direkte Aufnahme startet (Mikro hoert mit)"
  * armed: "Lausche auf X..."
  * Wake erkannt: "Wake-Word X erkannt — sprich jetzt"
  * endConversation: "Lausche wieder auf X" oder "Mikro aus"
- Ohr-Button-Icon zeigt drei States:
  🔇 off
  👂 armed (Porcupine lauscht passiv)
  🎙️ conversing (aktive Aufnahme laeuft)
- ChatScreen subscribed wakeWordService.onStateChange fuer Live-
  Updates des Icons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:34:07 +02:00
duffyduck bbf9aed3ba fix: 4 Bugs — STT-Mapping, Speed-Logging, VAD-Logs, Wake-Word-Toast
Bug 2: STT-Result ueberschrieb beide noch unaufgeloeste Audio-Bubbles
mit gleichem Text. Fix: nur die ERSTE matchende Bubble aktualisieren
(findIndex + index-Update statt map). Reihenfolge ist FIFO weil Whisper
sequenziell verarbeitet.

Bug 3: Speed-Param wird nun in jedem Hop geloggt:
  - ChatScreen: "[Chat] sende mit voice=X speed=Y"
  - aria-bridge: "XTTS-Request gesendet (voice=X, speed=Y.YYx)"
  - f5tts-bridge: "F5-TTS: N Satz(e), voice=X, speed=Y.YYx"
Damit kann man im logcat/docker-logs eindeutig sehen wo speed evtl.
verloren geht oder ob die Stimme einfach von Natur aus schnell ist.

Bug 4: VAD-Trigger-Reason mit Schwelle: "VAD NNN ms Stille (Schwelle=NNN ms)".
Plus startRecording loggt jetzt VAD-Stille + MAX-Recording.

Bug 1 (Porcupine): mehr Debug + Toast-Meldungen.
  - init failure: err.name/code/stack ins Log
  - start() ohne Porcupine: Toast "Access Key in Settings setzen"
  - start() Fehler: Toast mit Fehlermeldung
  - configure(): Toast wenn init scheitert
  - Erfolgreiches arming: Toast "Lausche auf X"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:28:46 +02:00
5 changed files with 73 additions and 20 deletions

View File

@ -104,6 +104,8 @@ const ChatScreen: React.FC = () => {
const [showCameraUpload, setShowCameraUpload] = useState(false); const [showCameraUpload, setShowCameraUpload] = useState(false);
const [gpsEnabled, setGpsEnabled] = useState(false); const [gpsEnabled, setGpsEnabled] = useState(false);
const [wakeWordActive, setWakeWordActive] = useState(false); const [wakeWordActive, setWakeWordActive] = useState(false);
// Genauer State (off/armed/conversing) fuer UI-Feedback am Button
const [wakeWordState, setWakeWordState] = useState<'off' | 'armed' | 'conversing'>('off');
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null); const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [searchVisible, setSearchVisible] = useState(false); const [searchVisible, setSearchVisible] = useState(false);
@ -154,6 +156,11 @@ const ChatScreen: React.FC = () => {
// Wake Word: einmalig laden + Porcupine vorbereiten (wenn Access Key gesetzt) // Wake Word: einmalig laden + Porcupine vorbereiten (wenn Access Key gesetzt)
useEffect(() => { useEffect(() => {
wakeWordService.loadFromStorage().catch(() => {}); wakeWordService.loadFromStorage().catch(() => {});
const unsub = wakeWordService.onStateChange((s) => {
setWakeWordState(s);
setWakeWordActive(s !== 'off');
});
return () => unsub();
}, []); }, []);
// ttsCanPlayRef live aktuell halten — Closure in onMessage unten liest // ttsCanPlayRef live aktuell halten — Closure in onMessage unten liest
@ -263,15 +270,22 @@ const ChatScreen: React.FC = () => {
if (message.type === 'chat') { if (message.type === 'chat') {
const sender = (message.payload.sender as string) || ''; const sender = (message.payload.sender as string) || '';
// STT-Ergebnis: Transkribierten Text in die Sprach-Bubble schreiben // STT-Ergebnis: Transkribierten Text in die Sprach-Bubble schreiben.
// WICHTIG: Nur die ERSTE noch unaufgeloeste Aufnahme matchen — sonst
// wuerde bei zwei kurz hintereinander gesendeten Audios beide Bubbles
// den gleichen Text bekommen (Bug: zweite Antwort ueberschreibt erste).
if (sender === 'stt') { if (sender === 'stt') {
const sttText = (message.payload.text as string) || ''; const sttText = (message.payload.text as string) || '';
if (sttText) { if (sttText) {
setMessages(prev => prev.map(m => setMessages(prev => {
const idx = prev.findIndex(m =>
m.sender === 'user' && m.text.includes('Spracheingabe wird verarbeitet') m.sender === 'user' && m.text.includes('Spracheingabe wird verarbeitet')
? { ...m, text: `\uD83C\uDFA4 ${sttText}` } );
: m if (idx < 0) return prev;
)); const next = prev.slice();
next[idx] = { ...next[idx], text: `\uD83C\uDFA4 ${sttText}` };
return next;
});
} }
return; return;
} }
@ -572,6 +586,8 @@ const ChatScreen: React.FC = () => {
}; };
setMessages(prev => capMessages([...prev, userMsg])); setMessages(prev => capMessages([...prev, userMsg]));
console.log('[Chat] sende mit voice=%s speed=%s',
localXttsVoiceRef.current || '(default)', ttsSpeedRef.current);
// An RVS senden — mit geraetelokaler Voice (Bridge nutzt sie fuer die Antwort) // An RVS senden — mit geraetelokaler Voice (Bridge nutzt sie fuer die Antwort)
rvs.send('chat', { rvs.send('chat', {
text, text,
@ -1000,7 +1016,10 @@ const ChatScreen: React.FC = () => {
style={[styles.wakeWordBtn, wakeWordActive && styles.wakeWordBtnActive]} style={[styles.wakeWordBtn, wakeWordActive && styles.wakeWordBtnActive]}
onPress={toggleWakeWord} onPress={toggleWakeWord}
> >
<Text style={styles.wakeWordIcon}>{wakeWordActive ? '👂' : '🔇'}</Text> <Text style={styles.wakeWordIcon}>
{wakeWordState === 'conversing' ? '🎙️' :
wakeWordState === 'armed' ? '👂' : '🔇'}
</Text>
</TouchableOpacity> </TouchableOpacity>
</> </>
)} )}

View File

@ -328,11 +328,12 @@ class AudioService {
}; };
if (autoStop) { if (autoStop) {
const vadSilenceMs = await loadVadSilenceMs(); const vadSilenceMs = await loadVadSilenceMs();
console.log('[Audio] VAD-Stille:', vadSilenceMs, 'ms'); console.log('[Audio] startRecording: autoStop=true, VAD-Stille=%dms, MAX=%dms',
vadSilenceMs, MAX_RECORDING_MS);
this.vadTimer = setInterval(() => { this.vadTimer = setInterval(() => {
const silenceDuration = Date.now() - this.lastSpeechTime; const silenceDuration = Date.now() - this.lastSpeechTime;
if (silenceDuration >= vadSilenceMs) { if (silenceDuration >= vadSilenceMs) {
fireSilenceOnce(`VAD ${silenceDuration}ms Stille`); fireSilenceOnce(`VAD ${silenceDuration}ms Stille (Schwelle=${vadSilenceMs}ms)`);
} }
}, 200); }, 200);
// Notbremse: Nach MAX_RECORDING_MS zwangsweise stoppen // Notbremse: Nach MAX_RECORDING_MS zwangsweise stoppen

View File

@ -17,6 +17,7 @@
*/ */
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { ToastAndroid } from 'react-native';
type WakeWordCallback = () => void; type WakeWordCallback = () => void;
type StateCallback = (state: WakeWordState) => void; type StateCallback = (state: WakeWordState) => void;
@ -80,10 +81,20 @@ class WakeWordService {
// Laufende Instanz stoppen // Laufende Instanz stoppen
await this.disposePorcupine(); await this.disposePorcupine();
if (!this.accessKey) return false; if (!this.accessKey) {
console.warn('[WakeWord] configure: kein Access Key gesetzt');
return false;
}
// Neu initialisieren // Neu initialisieren
return this.initPorcupine(); const ok = await this.initPorcupine();
if (!ok) {
ToastAndroid.show(
`Wake-Word "${this.keyword}" konnte nicht initialisiert werden — Logs pruefen`,
ToastAndroid.LONG,
);
}
return ok;
} }
private async initPorcupine(): Promise<boolean> { private async initPorcupine(): Promise<boolean> {
@ -117,10 +128,14 @@ class WakeWordService {
this.disposePorcupine().catch(() => {}); this.disposePorcupine().catch(() => {});
}, },
); );
console.log('[WakeWord] Porcupine init OK (keyword=%s)', this.keyword); console.log('[WakeWord] Porcupine init OK (keyword=%s, manager=%s)',
this.keyword, this.porcupine ? 'created' : 'NULL');
return true; return true;
} catch (err) { } catch (err: any) {
console.warn('[WakeWord] Porcupine init fehlgeschlagen:', err); console.warn('[WakeWord] Porcupine init fehlgeschlagen:', err?.message || err);
console.warn('[WakeWord] err details:', JSON.stringify({
name: err?.name, code: err?.code, stack: err?.stack?.slice(0, 200),
}));
this.porcupine = null; this.porcupine = null;
return false; return false;
} finally { } finally {
@ -146,14 +161,27 @@ class WakeWordService {
try { try {
await this.porcupine.start(); await this.porcupine.start();
console.log('[WakeWord] armed — warte auf Wake Word "%s"', this.keyword); console.log('[WakeWord] armed — warte auf Wake Word "%s"', this.keyword);
ToastAndroid.show(`Lausche auf "${this.keyword}"`, ToastAndroid.SHORT);
this.setState('armed'); this.setState('armed');
return true; return true;
} catch (err) { } catch (err: any) {
console.warn('[WakeWord] Porcupine start fehlgeschlagen — Fallback Direkt-Konversation:', err); console.warn('[WakeWord] Porcupine start fehlgeschlagen — Fallback Direkt-Konversation:',
err?.message || err);
ToastAndroid.show(
`Wake-Word-Start failed: ${err?.message || err}`,
ToastAndroid.LONG,
);
} }
} else {
// Kein Porcupine init → User explicit informieren
console.warn('[WakeWord] Porcupine nicht initialisiert — Access Key fehlt? Fallback Direkt-Aufnahme');
ToastAndroid.show(
'Wake-Word nicht aktiv — direkte Aufnahme startet (Mikro hoert mit)',
ToastAndroid.LONG,
);
} }
// Fallback: direkt in die Konversation // Fallback: direkt in die Konversation (Mikro AKTIV, nicht passive)
console.log('[WakeWord] Konversation startet sofort (kein Wake-Word)'); console.log('[WakeWord] Direkt-Aufnahme startet (kein Wake-Word)');
this.setState('conversing'); this.setState('conversing');
setTimeout(() => { setTimeout(() => {
if (this.state === 'conversing') { if (this.state === 'conversing') {
@ -175,6 +203,7 @@ class WakeWordService {
/** Wake-Word getriggert: Porcupine pausieren, Konversation starten. */ /** Wake-Word getriggert: Porcupine pausieren, Konversation starten. */
private async onWakeDetected(): Promise<void> { private async onWakeDetected(): Promise<void> {
console.log('[WakeWord] Wake-Word "%s" erkannt!', this.keyword); console.log('[WakeWord] Wake-Word "%s" erkannt!', this.keyword);
ToastAndroid.show(`Wake-Word "${this.keyword}" erkannt — sprich jetzt`, ToastAndroid.SHORT);
if (this.porcupine) { if (this.porcupine) {
try { await this.porcupine.stop(); } catch {} try { await this.porcupine.stop(); } catch {}
} }
@ -197,6 +226,7 @@ class WakeWordService {
try { try {
await this.porcupine.start(); await this.porcupine.start();
console.log('[WakeWord] Konversation zu Ende — zurueck zu armed'); console.log('[WakeWord] Konversation zu Ende — zurueck zu armed');
ToastAndroid.show(`Lausche wieder auf "${this.keyword}"`, ToastAndroid.SHORT);
this.setState('armed'); this.setState('armed');
return; return;
} catch (err) { } catch (err) {
@ -204,6 +234,7 @@ class WakeWordService {
} }
} }
console.log('[WakeWord] Konversation zu Ende — Ohr aus'); console.log('[WakeWord] Konversation zu Ende — Ohr aus');
ToastAndroid.show('Mikro aus', ToastAndroid.SHORT);
this.setState('off'); this.setState('off');
} }

View File

@ -942,7 +942,8 @@ class ARIABridge:
}, },
"timestamp": int(asyncio.get_event_loop().time() * 1000), "timestamp": int(asyncio.get_event_loop().time() * 1000),
}) })
logger.info("[core] XTTS-Request gesendet (%s): '%s'", xtts_voice or "default", tts_text[:60]) logger.info("[core] XTTS-Request gesendet (voice=%s, speed=%.2fx): '%s'",
xtts_voice or "default", xtts_speed, tts_text[:60])
except Exception as e: except Exception as e:
logger.error("[core] XTTS-Request fehlgeschlagen: %s — kein Audio", e) logger.error("[core] XTTS-Request fehlgeschlagen: %s — kein Audio", e)

View File

@ -507,7 +507,8 @@ async def _do_tts(ws, runner: F5Runner, text: str, voice: str,
ref_wav_str, ref_text = str(pair[0]), pair[1].read_text(encoding="utf-8").strip() ref_wav_str, ref_text = str(pair[0]), pair[1].read_text(encoding="utf-8").strip()
sentences = split_sentences(text) sentences = split_sentences(text)
logger.info("F5-TTS: %d Satz(e), voice=%s (%s)", len(sentences), voice or "default", ref_wav_str) logger.info("F5-TTS: %d Satz(e), voice=%s, speed=%.2fx (%s)",
len(sentences), voice or "default", speed, ref_wav_str)
chunk_index = 0 chunk_index = 0
pcm_sr = TARGET_SR pcm_sr = TARGET_SR