Ohr-Button Absturz gefixt (LiveAudioStream entfernt, Phase 1 , Play-Button in ARIA-Nachrichten fuer Sprachwiedergabe
- [x] Chat-Suche in der App (Lupe in Statusleiste) - [x] Watchdog mit Container-Restart (2min Warnung → 5min doctor --fix → 8min Restart),Abbrechen-Button im Diagnostic Chat - [x] Nachrichten Backup on-the-fly (/shared/config/chat_backup.jsonl) - [x] Grosse Nachrichten satzweise aufteilen fuer TTS - [x] RVS Nachrichten vom Smartphone gehen durch
This commit is contained in:
@@ -91,6 +91,8 @@ const ChatScreen: React.FC = () => {
|
||||
const [gpsEnabled, setGpsEnabled] = useState(false);
|
||||
const [wakeWordActive, setWakeWordActive] = useState(false);
|
||||
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const messageIdCounter = useRef(0);
|
||||
@@ -581,6 +583,18 @@ const ChatScreen: React.FC = () => {
|
||||
{item.text}
|
||||
</Text>
|
||||
)}
|
||||
{/* Play-Button fuer ARIA-Nachrichten */}
|
||||
{!isUser && item.text.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.playButton}
|
||||
onPress={() => {
|
||||
// TTS-Request an Bridge senden
|
||||
rvs.send('tts_request' as any, { text: item.text, voice: '' });
|
||||
}}
|
||||
>
|
||||
<Text style={styles.playButtonText}>{'\uD83D\uDD0A'}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text style={styles.timestamp}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -603,12 +617,32 @@ const ChatScreen: React.FC = () => {
|
||||
{connectionState === 'connected' ? 'Verbunden' :
|
||||
connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => setSearchVisible(!searchVisible)} style={{marginLeft: 'auto', paddingHorizontal: 8}}>
|
||||
<Text style={{fontSize: 16}}>{'\uD83D\uDD0D'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Suchleiste */}
|
||||
{searchVisible && (
|
||||
<View style={styles.searchBar}>
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
placeholder="Chat durchsuchen..."
|
||||
placeholderTextColor="#555570"
|
||||
autoFocus
|
||||
/>
|
||||
<TouchableOpacity onPress={() => { setSearchVisible(false); setSearchQuery(''); }}>
|
||||
<Text style={{color: '#FF3B30', fontSize: 14, paddingHorizontal: 8}}>X</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Nachrichtenliste */}
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={messages}
|
||||
data={searchQuery ? messages.filter(m => m.text.toLowerCase().includes(searchQuery.toLowerCase())) : messages}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderMessage}
|
||||
contentContainerStyle={styles.messageList}
|
||||
@@ -887,6 +921,30 @@ const styles = StyleSheet.create({
|
||||
wakeWordIcon: {
|
||||
fontSize: 16,
|
||||
},
|
||||
searchBar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#12122A',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#1E1E2E',
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
playButton: {
|
||||
alignSelf: 'flex-end',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
marginTop: 4,
|
||||
},
|
||||
playButtonText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
fullscreenOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.95)',
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
/**
|
||||
* Wake Word Service — "ARIA" Erkennung
|
||||
*
|
||||
* Nutzt react-native-live-audio-stream fuer kontinuierliches Mikrofon-Monitoring.
|
||||
* Erkennt Sprache per Energie-Schwellwert und sendet kurze Audio-Clips
|
||||
* zur serverseitigen Wake-Word-Pruefung (openwakeword in der Bridge).
|
||||
* Phase 1: Deaktiviert — react-native-live-audio-stream hat native Bridge-Probleme.
|
||||
* Nutzt stattdessen Tap-to-Talk (VoiceButton) als primaeren Eingabemodus.
|
||||
*
|
||||
* Architektur:
|
||||
* App (Mikrofon) → Energie-Erkennung → Audio-Buffer
|
||||
* → RVS "wake_check" → Bridge → openwakeword → Bestaetigung
|
||||
* → App startet Aufnahme
|
||||
*
|
||||
* Aktuell (Phase 1): Einfacher Tap-to-Talk + Auto-Stop.
|
||||
* Spaeter (Phase 2): Porcupine on-device "ARIA" Keyword.
|
||||
* Phase 2: Porcupine on-device "ARIA" Keyword (geplant).
|
||||
*/
|
||||
|
||||
import LiveAudioStream from 'react-native-live-audio-stream';
|
||||
|
||||
type WakeWordCallback = () => void;
|
||||
type StateCallback = (state: WakeWordState) => void;
|
||||
|
||||
@@ -25,47 +16,16 @@ class WakeWordService {
|
||||
private state: WakeWordState = 'off';
|
||||
private wakeCallbacks: WakeWordCallback[] = [];
|
||||
private stateCallbacks: StateCallback[] = [];
|
||||
private isInitialized = false;
|
||||
|
||||
/** Wake Word Erkennung starten */
|
||||
async start(): Promise<boolean> {
|
||||
if (this.state === 'listening') return true;
|
||||
|
||||
try {
|
||||
if (!this.isInitialized) {
|
||||
LiveAudioStream.init({
|
||||
sampleRate: 16000,
|
||||
channels: 1,
|
||||
bitsPerSample: 16,
|
||||
audioSource: 6, // VOICE_RECOGNITION
|
||||
bufferSize: 4096,
|
||||
});
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
// Audio-Stream starten und auf Energie pruefen
|
||||
LiveAudioStream.start();
|
||||
|
||||
LiveAudioStream.on('data', (base64Chunk: string) => {
|
||||
if (this.state !== 'listening') return;
|
||||
|
||||
// Base64 → Int16 Array → RMS berechnen
|
||||
const raw = this._base64ToInt16(base64Chunk);
|
||||
const rms = this._calculateRMS(raw);
|
||||
|
||||
// Schwellwert: wenn laut genug → Wake Word erkannt
|
||||
// Phase 1: Einfache Energie-Erkennung (jemand spricht)
|
||||
// Phase 2: Porcupine "ARIA" Keyword
|
||||
if (rms > 2000) {
|
||||
this.setState('detected');
|
||||
this.wakeCallbacks.forEach(cb => cb());
|
||||
// Nach Detection kurz pausieren, Aufnahme uebernimmt das Mikrofon
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 1: LiveAudioStream deaktiviert (native Bridge instabil)
|
||||
// Stattdessen: Tap-to-Talk als primaerer Modus
|
||||
console.log('[WakeWord] Wake Word ist in Phase 1 noch nicht verfuegbar — nutze Tap-to-Talk');
|
||||
this.setState('listening');
|
||||
console.log('[WakeWord] Listening gestartet');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[WakeWord] Start fehlgeschlagen:', err);
|
||||
@@ -75,22 +35,12 @@ class WakeWordService {
|
||||
|
||||
/** Wake Word Erkennung stoppen */
|
||||
stop(): void {
|
||||
if (this.state === 'off') return;
|
||||
try {
|
||||
LiveAudioStream.stop();
|
||||
} catch {}
|
||||
this.setState('off');
|
||||
console.log('[WakeWord] Gestoppt');
|
||||
}
|
||||
|
||||
/** Nach Aufnahme erneut starten */
|
||||
async resume(): Promise<void> {
|
||||
// Kurze Pause damit Aufnahme das Mikrofon freigeben kann
|
||||
setTimeout(() => {
|
||||
if (this.state === 'off') {
|
||||
this.start();
|
||||
}
|
||||
}, 500);
|
||||
// Nichts zu tun in Phase 1
|
||||
}
|
||||
|
||||
// --- Callbacks ---
|
||||
@@ -113,32 +63,12 @@ class WakeWordService {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
// --- Hilfsfunktionen ---
|
||||
|
||||
private setState(state: WakeWordState): void {
|
||||
if (this.state !== state) {
|
||||
this.state = state;
|
||||
this.stateCallbacks.forEach(cb => cb(state));
|
||||
}
|
||||
}
|
||||
|
||||
private _base64ToInt16(base64: string): Int16Array {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return new Int16Array(bytes.buffer);
|
||||
}
|
||||
|
||||
private _calculateRMS(samples: Int16Array): number {
|
||||
if (samples.length === 0) return 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
sum += samples[i] * samples[i];
|
||||
}
|
||||
return Math.sqrt(sum / samples.length);
|
||||
}
|
||||
}
|
||||
|
||||
const wakeWordService = new WakeWordService();
|
||||
|
||||
Reference in New Issue
Block a user