Compare commits
2 Commits
745b4a07c0
...
be373466a3
| Author | SHA1 | Date |
|---|---|---|
|
|
be373466a3 | |
|
|
bbf9aed3ba |
|
|
@ -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 => {
|
||||||
m.sender === 'user' && m.text.includes('Spracheingabe wird verarbeitet')
|
const idx = prev.findIndex(m =>
|
||||||
? { ...m, text: `\uD83C\uDFA4 ${sttText}` }
|
m.sender === 'user' && m.text.includes('Spracheingabe wird verarbeitet')
|
||||||
: 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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue