Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79c50aedcc | |||
| eb72b35e23 | |||
| bbd02d46a6 | |||
| 3d3c8ce973 | |||
| 562f929056 | |||
| ff03d8ce62 | |||
| 8281131432 | |||
| 8a6bd4e0e7 | |||
| 1b4df0565a | |||
| eb3692ef81 |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 108
|
versionCode 200
|
||||||
versionName "0.0.1.8"
|
versionName "0.0.2.0"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.0.1.8",
|
"version": "0.0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ 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);
|
||||||
|
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||||
|
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
const messageIdCounter = useRef(0);
|
const messageIdCounter = useRef(0);
|
||||||
@@ -525,12 +526,12 @@ const ChatScreen: React.FC = () => {
|
|||||||
{item.attachments?.map((att, idx) => (
|
{item.attachments?.map((att, idx) => (
|
||||||
<View key={idx}>
|
<View key={idx}>
|
||||||
{att.type === 'image' && att.uri ? (
|
{att.type === 'image' && att.uri ? (
|
||||||
|
<TouchableOpacity onPress={() => setFullscreenImage(att.uri || null)} activeOpacity={0.8}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: att.uri }}
|
source={{ uri: att.uri }}
|
||||||
style={styles.attachmentImage}
|
style={styles.attachmentImage}
|
||||||
resizeMode="contain"
|
resizeMode="cover"
|
||||||
onError={() => {
|
onError={() => {
|
||||||
// Bild nicht mehr verfuegbar — Placeholder setzen
|
|
||||||
setMessages(prev => prev.map(m =>
|
setMessages(prev => prev.map(m =>
|
||||||
m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) =>
|
m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) =>
|
||||||
i === idx ? { ...a, uri: undefined } : a
|
i === idx ? { ...a, uri: undefined } : a
|
||||||
@@ -538,6 +539,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
));
|
));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
) : att.type === 'image' && !att.uri ? (
|
) : att.type === 'image' && !att.uri ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.attachmentFile}
|
style={styles.attachmentFile}
|
||||||
@@ -675,6 +677,23 @@ const ChatScreen: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Bild-Vollbild Modal */}
|
||||||
|
<Modal visible={!!fullscreenImage} transparent animationType="fade" onRequestClose={() => setFullscreenImage(null)}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.fullscreenOverlay}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setFullscreenImage(null)}
|
||||||
|
>
|
||||||
|
{fullscreenImage && (
|
||||||
|
<Image
|
||||||
|
source={{ uri: fullscreenImage }}
|
||||||
|
style={styles.fullscreenImage}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Datei-Upload Modal */}
|
{/* Datei-Upload Modal */}
|
||||||
<Modal visible={showFileUpload} transparent animationType="slide">
|
<Modal visible={showFileUpload} transparent animationType="slide">
|
||||||
<View style={styles.modalOverlay}>
|
<View style={styles.modalOverlay}>
|
||||||
@@ -757,7 +776,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
attachmentImage: {
|
attachmentImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 200,
|
minHeight: 200,
|
||||||
|
maxHeight: 400,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
backgroundColor: '#0D0D1A',
|
backgroundColor: '#0D0D1A',
|
||||||
@@ -867,6 +887,16 @@ const styles = StyleSheet.create({
|
|||||||
wakeWordIcon: {
|
wakeWordIcon: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
|
fullscreenOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.95)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
fullscreenImage: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
modalOverlay: {
|
modalOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH);
|
const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH);
|
||||||
const [autoDownload, setAutoDownload] = useState(true);
|
const [autoDownload, setAutoDownload] = useState(true);
|
||||||
const [storageSize, setStorageSize] = useState('...');
|
const [storageSize, setStorageSize] = useState('...');
|
||||||
|
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||||
|
const [defaultVoice, setDefaultVoice] = useState('ramona');
|
||||||
|
const [highlightVoice, setHighlightVoice] = useState('thorsten');
|
||||||
const [editingPath, setEditingPath] = useState(false);
|
const [editingPath, setEditingPath] = useState(false);
|
||||||
const [tempPath, setTempPath] = useState('');
|
const [tempPath, setTempPath] = useState('');
|
||||||
|
|
||||||
@@ -91,6 +94,15 @@ const SettingsScreen: React.FC = () => {
|
|||||||
AsyncStorage.getItem('aria_auto_download').then(saved => {
|
AsyncStorage.getItem('aria_auto_download').then(saved => {
|
||||||
if (saved !== null) setAutoDownload(saved === 'true');
|
if (saved !== null) setAutoDownload(saved === 'true');
|
||||||
});
|
});
|
||||||
|
AsyncStorage.getItem('aria_tts_enabled').then(saved => {
|
||||||
|
if (saved !== null) setTtsEnabled(saved === 'true');
|
||||||
|
});
|
||||||
|
AsyncStorage.getItem('aria_default_voice').then(saved => {
|
||||||
|
if (saved) setDefaultVoice(saved);
|
||||||
|
});
|
||||||
|
AsyncStorage.getItem('aria_highlight_voice').then(saved => {
|
||||||
|
if (saved) setHighlightVoice(saved);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Speichergroesse berechnen
|
// Speichergroesse berechnen
|
||||||
@@ -442,6 +454,83 @@ const SettingsScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* === Sprachausgabe === */}
|
||||||
|
<Text style={styles.sectionTitle}>Sprachausgabe</Text>
|
||||||
|
<View style={styles.card}>
|
||||||
|
{/* TTS An/Aus */}
|
||||||
|
<View style={styles.toggleRow}>
|
||||||
|
<View style={styles.toggleInfo}>
|
||||||
|
<Text style={styles.toggleLabel}>Sprachausgabe</Text>
|
||||||
|
<Text style={styles.toggleHint}>ARIA antwortet per Sprache (TTS)</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={ttsEnabled}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setTtsEnabled(val);
|
||||||
|
AsyncStorage.setItem('aria_tts_enabled', String(val));
|
||||||
|
rvs.send('config' as any, { ttsEnabled: val });
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
|
||||||
|
thumbColor={ttsEnabled ? '#FFFFFF' : '#666680'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Standard-Stimme */}
|
||||||
|
<View style={{marginTop: 16}}>
|
||||||
|
<Text style={styles.toggleLabel}>Standard-Stimme</Text>
|
||||||
|
<Text style={styles.toggleHint}>Fuer normale Antworten und Gespraeche</Text>
|
||||||
|
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, defaultVoice === 'ramona' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setDefaultVoice('ramona'); AsyncStorage.setItem('aria_default_voice', 'ramona'); rvs.send('config' as any, { defaultVoice: 'ramona' }); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, defaultVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
|
||||||
|
<Text style={styles.voiceBtnHint}>Weiblich, warm</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, defaultVoice === 'thorsten' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setDefaultVoice('thorsten'); AsyncStorage.setItem('aria_default_voice', 'thorsten'); rvs.send('config' as any, { defaultVoice: 'thorsten' }); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, defaultVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
|
||||||
|
<Text style={styles.voiceBtnHint}>Maennlich, tief</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Highlight-Stimme */}
|
||||||
|
<View style={{marginTop: 16}}>
|
||||||
|
<Text style={styles.toggleLabel}>Highlight-Stimme</Text>
|
||||||
|
<Text style={styles.toggleHint}>Fuer besondere Ereignisse (Deploy, Alarm, Erfolg)</Text>
|
||||||
|
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, highlightVoice === 'thorsten' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setHighlightVoice('thorsten'); AsyncStorage.setItem('aria_highlight_voice', 'thorsten'); rvs.send('config' as any, { highlightVoice: 'thorsten' }); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, highlightVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, highlightVoice === 'ramona' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setHighlightVoice('ramona'); AsyncStorage.setItem('aria_highlight_voice', 'ramona'); rvs.send('config' as any, { highlightVoice: 'ramona' }); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, highlightVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Highlight-Trigger Info */}
|
||||||
|
<View style={{marginTop: 16, padding: 10, backgroundColor: '#1E1E2E', borderRadius: 8}}>
|
||||||
|
<Text style={styles.toggleLabel}>{'\u26A1'} Highlight-Trigger</Text>
|
||||||
|
<Text style={[styles.toggleHint, {marginTop: 4}]}>
|
||||||
|
Die Highlight-Stimme wird automatisch bei diesen Woertern verwendet:{'\n'}
|
||||||
|
deploy, erfolgreich, alarm, so soll es sein, kritisch, server down, sicherheitswarnung, ticket geloest, aufgabe abgeschlossen
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* === Speicher === */}
|
{/* === Speicher === */}
|
||||||
<Text style={styles.sectionTitle}>Anhang-Speicher</Text>
|
<Text style={styles.sectionTitle}>Anhang-Speicher</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
@@ -601,7 +690,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
|
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
||||||
<Text style={styles.aboutVersion}>Version 0.0.1.8 </Text>
|
<Text style={styles.aboutVersion}>Version 0.0.2.0 </Text>
|
||||||
<Text style={styles.aboutInfo}>
|
<Text style={styles.aboutInfo}>
|
||||||
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
|
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
|
||||||
Gebaut mit React Native + TypeScript.
|
Gebaut mit React Native + TypeScript.
|
||||||
@@ -744,6 +833,38 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Stimmen
|
||||||
|
voiceBtn: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: '#1E1E2E',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
voiceBtnActive: {
|
||||||
|
borderColor: '#0096FF',
|
||||||
|
backgroundColor: '#0D1A2E',
|
||||||
|
},
|
||||||
|
voiceBtnIcon: {
|
||||||
|
fontSize: 28,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
voiceBtnText: {
|
||||||
|
color: '#8888AA',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
voiceBtnTextActive: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
voiceBtnHint: {
|
||||||
|
color: '#555570',
|
||||||
|
fontSize: 11,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
|
||||||
// Speicher
|
// Speicher
|
||||||
storagePathText: {
|
storagePathText: {
|
||||||
color: '#0096FF',
|
color: '#0096FF',
|
||||||
|
|||||||
+48
-9
@@ -72,7 +72,7 @@ BLOCK_SIZE = 1280 # 80ms bei 16kHz — gut fuer Wake-Word-Erkennung
|
|||||||
RECORD_SECONDS = 8 # Max. Aufnahmedauer nach Wake-Word
|
RECORD_SECONDS = 8 # Max. Aufnahmedauer nach Wake-Word
|
||||||
|
|
||||||
# Epische Trigger — bei diesen Woertern spricht Thorsten
|
# Epische Trigger — bei diesen Woertern spricht Thorsten
|
||||||
EPIC_TRIGGERS = [
|
EPIC_TRIGGERS_DEFAULT = [
|
||||||
"deploy",
|
"deploy",
|
||||||
"erfolgreich",
|
"erfolgreich",
|
||||||
"alarm",
|
"alarm",
|
||||||
@@ -84,6 +84,24 @@ EPIC_TRIGGERS = [
|
|||||||
"aufgabe abgeschlossen",
|
"aufgabe abgeschlossen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Trigger aus Shared-Config laden (von Diagnostic gespeichert)
|
||||||
|
TRIGGERS_FILE = "/shared/config/highlight_triggers.json"
|
||||||
|
|
||||||
|
def load_epic_triggers():
|
||||||
|
"""Laedt Highlight-Trigger aus Shared-Config oder nutzt Defaults."""
|
||||||
|
try:
|
||||||
|
if os.path.exists(TRIGGERS_FILE):
|
||||||
|
with open(TRIGGERS_FILE) as f:
|
||||||
|
triggers = json.load(f)
|
||||||
|
if isinstance(triggers, list) and len(triggers) > 0:
|
||||||
|
logger.info("Highlight-Trigger geladen: %d aus %s", len(triggers), TRIGGERS_FILE)
|
||||||
|
return triggers
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Highlight-Trigger laden fehlgeschlagen: %s — nutze Defaults", e)
|
||||||
|
return EPIC_TRIGGERS_DEFAULT
|
||||||
|
|
||||||
|
EPIC_TRIGGERS = load_epic_triggers()
|
||||||
|
|
||||||
|
|
||||||
def load_config() -> dict[str, str]:
|
def load_config() -> dict[str, str]:
|
||||||
"""Laedt Konfiguration aus /config/aria.env."""
|
"""Laedt Konfiguration aus /config/aria.env."""
|
||||||
@@ -111,6 +129,8 @@ class VoiceEngine:
|
|||||||
def __init__(self, voices_dir: Path) -> None:
|
def __init__(self, voices_dir: Path) -> None:
|
||||||
self.voices_dir = voices_dir
|
self.voices_dir = voices_dir
|
||||||
self.voices: dict[str, PiperVoice] = {}
|
self.voices: dict[str, PiperVoice] = {}
|
||||||
|
self.default_voice = "ramona"
|
||||||
|
self.highlight_voice = "thorsten"
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
"""Laedt die Piper-Stimmen aus dem Voices-Verzeichnis."""
|
"""Laedt die Piper-Stimmen aus dem Voices-Verzeichnis."""
|
||||||
@@ -154,14 +174,14 @@ class VoiceEngine:
|
|||||||
if requested_voice and requested_voice in self.voices:
|
if requested_voice and requested_voice in self.voices:
|
||||||
return requested_voice
|
return requested_voice
|
||||||
|
|
||||||
# Epische Trigger pruefen
|
# Highlight-Trigger pruefen
|
||||||
text_lower = text.lower()
|
text_lower = text.lower()
|
||||||
for trigger in EPIC_TRIGGERS:
|
for trigger in EPIC_TRIGGERS:
|
||||||
if trigger in text_lower:
|
if trigger in text_lower:
|
||||||
logger.info("Epischer Trigger erkannt: '%s' — Thorsten spricht", trigger)
|
logger.info("Highlight-Trigger erkannt: '%s' — %s spricht", trigger, self.highlight_voice)
|
||||||
return "thorsten"
|
return self.highlight_voice
|
||||||
|
|
||||||
return "ramona"
|
return self.default_voice
|
||||||
|
|
||||||
def synthesize(self, text: str, voice_name: str = "ramona") -> Optional[bytes]:
|
def synthesize(self, text: str, voice_name: str = "ramona") -> Optional[bytes]:
|
||||||
"""Erzeugt Audio-Daten aus Text mit der gewaehlten Stimme.
|
"""Erzeugt Audio-Daten aus Text mit der gewaehlten Stimme.
|
||||||
@@ -184,7 +204,7 @@ class VoiceEngine:
|
|||||||
tmp_path = tmp.name
|
tmp_path = tmp.name
|
||||||
|
|
||||||
with wave.open(tmp_path, "wb") as wav_file:
|
with wave.open(tmp_path, "wb") as wav_file:
|
||||||
voice.synthesize(text, wav_file)
|
voice.synthesize_wav(text, wav_file)
|
||||||
|
|
||||||
audio_data = Path(tmp_path).read_bytes()
|
audio_data = Path(tmp_path).read_bytes()
|
||||||
Path(tmp_path).unlink(missing_ok=True)
|
Path(tmp_path).unlink(missing_ok=True)
|
||||||
@@ -773,7 +793,7 @@ class ARIABridge:
|
|||||||
})
|
})
|
||||||
|
|
||||||
# TTS-Audio rendern und an die App senden (wenn Modus es erlaubt)
|
# TTS-Audio rendern und an die App senden (wenn Modus es erlaubt)
|
||||||
if should_speak(self.current_mode, is_critical):
|
if getattr(self, 'tts_enabled', True) and should_speak(self.current_mode, is_critical):
|
||||||
audio_data = self.voice_engine.synthesize(text, voice_name)
|
audio_data = self.voice_engine.synthesize(text, voice_name)
|
||||||
if audio_data:
|
if audio_data:
|
||||||
audio_b64 = base64.b64encode(audio_data).decode("ascii")
|
audio_b64 = base64.b64encode(audio_data).decode("ascii")
|
||||||
@@ -929,6 +949,23 @@ class ARIABridge:
|
|||||||
sender = payload.get("sender", "")
|
sender = payload.get("sender", "")
|
||||||
if sender in ("aria", "stt"):
|
if sender in ("aria", "stt"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
elif msg_type == "config":
|
||||||
|
# Konfiguration von App/Diagnostic empfangen
|
||||||
|
if "defaultVoice" in payload:
|
||||||
|
new_voice = payload["defaultVoice"]
|
||||||
|
if new_voice in self.voice_engine.voices:
|
||||||
|
self.voice_engine.default_voice = new_voice
|
||||||
|
logger.info("[rvs] Standard-Stimme gewechselt: %s", new_voice)
|
||||||
|
if "highlightVoice" in payload:
|
||||||
|
new_voice = payload["highlightVoice"]
|
||||||
|
if new_voice in self.voice_engine.voices:
|
||||||
|
self.voice_engine.highlight_voice = new_voice
|
||||||
|
logger.info("[rvs] Highlight-Stimme gewechselt: %s", new_voice)
|
||||||
|
if "ttsEnabled" in payload:
|
||||||
|
self.tts_enabled = bool(payload["ttsEnabled"])
|
||||||
|
logger.info("[rvs] TTS %s", "aktiviert" if self.tts_enabled else "deaktiviert")
|
||||||
|
return
|
||||||
text = payload.get("text", "")
|
text = payload.get("text", "")
|
||||||
if text:
|
if text:
|
||||||
logger.info("[rvs] App-Chat: '%s'", text[:80])
|
logger.info("[rvs] App-Chat: '%s'", text[:80])
|
||||||
@@ -984,7 +1021,8 @@ class ARIABridge:
|
|||||||
text = (f"Stefan hat dir ein Bild geschickt: {file_name}"
|
text = (f"Stefan hat dir ein Bild geschickt: {file_name}"
|
||||||
f"{f' ({width}x{height}px)' if width else ''}"
|
f"{f' ({width}x{height}px)' if width else ''}"
|
||||||
f", {size_kb}KB."
|
f", {size_kb}KB."
|
||||||
f" Das Bild liegt unter: {file_path}")
|
f" Das Bild liegt unter: {file_path}"
|
||||||
|
f" Warte auf Stefans Anweisung was du damit tun sollst.")
|
||||||
await self.send_to_core(text, source="app-file")
|
await self.send_to_core(text, source="app-file")
|
||||||
# Dann App informieren (optional, darf nicht crashen)
|
# Dann App informieren (optional, darf nicht crashen)
|
||||||
try:
|
try:
|
||||||
@@ -1006,7 +1044,8 @@ class ARIABridge:
|
|||||||
# ERST an aria-core senden
|
# ERST an aria-core senden
|
||||||
text = (f"Stefan hat dir eine Datei geschickt: {file_name}"
|
text = (f"Stefan hat dir eine Datei geschickt: {file_name}"
|
||||||
f" ({file_type}, {size_kb}KB)."
|
f" ({file_type}, {size_kb}KB)."
|
||||||
f" Die Datei liegt unter: {file_path}")
|
f" Die Datei liegt unter: {file_path}"
|
||||||
|
f" Warte auf Stefans Anweisung was du damit tun sollst.")
|
||||||
await self.send_to_core(text, source="app-file")
|
await self.send_to_core(text, source="app-file")
|
||||||
try:
|
try:
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
<button class="tab-btn" data-tab="bridge" onclick="switchTab('bridge')">Bridge <span class="tab-count" id="count-bridge">0</span></button>
|
<button class="tab-btn" data-tab="bridge" onclick="switchTab('bridge')">Bridge <span class="tab-count" id="count-bridge">0</span></button>
|
||||||
<button class="tab-btn" data-tab="server" onclick="switchTab('server')">Server <span class="tab-count" id="count-server">0</span></button>
|
<button class="tab-btn" data-tab="server" onclick="switchTab('server')">Server <span class="tab-count" id="count-server">0</span></button>
|
||||||
<button class="tab-btn" data-tab="pipeline" onclick="switchTab('pipeline')" style="margin-left:auto;border-color:#0096FF44;color:#0096FF">Pipeline <span class="tab-count" id="count-pipeline">0</span></button>
|
<button class="tab-btn" data-tab="pipeline" onclick="switchTab('pipeline')" style="margin-left:auto;border-color:#0096FF44;color:#0096FF">Pipeline <span class="tab-count" id="count-pipeline">0</span></button>
|
||||||
|
<button class="tab-btn" data-tab="tts" onclick="switchTab('tts')" style="border-color:#34C75944;color:#34C759">TTS</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-panel">
|
<div class="log-panel">
|
||||||
@@ -302,6 +303,36 @@
|
|||||||
<div class="log-box hidden" id="log-bridge"></div>
|
<div class="log-box hidden" id="log-bridge"></div>
|
||||||
<div class="log-box hidden" id="log-server"></div>
|
<div class="log-box hidden" id="log-server"></div>
|
||||||
<div class="log-box hidden" id="log-pipeline"></div>
|
<div class="log-box hidden" id="log-pipeline"></div>
|
||||||
|
<div class="log-box hidden" id="log-tts" style="padding:12px;">
|
||||||
|
<h3 style="color:#34C759;margin:0 0 12px;">TTS Diagnose</h3>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
|
||||||
|
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
|
||||||
|
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Standard-Stimme</div>
|
||||||
|
<div style="color:#fff;font-size:14px;margin-top:4px;" id="tts-default-voice">Ramona</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
|
||||||
|
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Highlight-Stimme</div>
|
||||||
|
<div style="color:#fff;font-size:14px;margin-top:4px;" id="tts-highlight-voice">Thorsten</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
|
||||||
|
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Status</div>
|
||||||
|
<div style="font-size:14px;margin-top:4px;" id="tts-status">Unbekannt</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
|
||||||
|
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Letzter Fehler</div>
|
||||||
|
<div style="color:#FF6B6B;font-size:12px;margin-top:4px;word-break:break-all;" id="tts-last-error">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:8px;">
|
||||||
|
<input type="text" id="tts-test-text" value="Hallo Stefan, ich bin ARIA." placeholder="Test-Text..." style="background:#1E1E2E;border:1px solid #2A2A3E;border-radius:6px;padding:8px;color:#fff;font-size:13px;width:100%;box-sizing:border-box;">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<button class="btn" onclick="testTTS('ramona')" style="flex:1;">Ramona testen</button>
|
||||||
|
<button class="btn" onclick="testTTS('thorsten')" style="flex:1;">Thorsten testen</button>
|
||||||
|
<button class="btn secondary" onclick="checkTTSStatus()" style="flex:1;">Status pruefen</button>
|
||||||
|
</div>
|
||||||
|
<div id="tts-log" style="margin-top:12px;max-height:200px;overflow-y:auto;font-size:11px;font-family:monospace;color:#8888AA;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -340,6 +371,74 @@
|
|||||||
<!-- ══════ TAB: Einstellungen ══════ -->
|
<!-- ══════ TAB: Einstellungen ══════ -->
|
||||||
<div id="tab-settings" class="main-tab">
|
<div id="tab-settings" class="main-tab">
|
||||||
|
|
||||||
|
<!-- Betriebsmodus -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>Betriebsmodus</h2>
|
||||||
|
<div class="card" style="max-width:500px;">
|
||||||
|
<div id="mode-selector" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
||||||
|
<button class="btn mode-btn" data-mode="normal" onclick="setMode('normal')" style="background:#1E1E2E;border:2px solid transparent;">
|
||||||
|
<span style="font-size:18px;">🟢</span> Normal<br><span style="font-size:10px;color:#8888AA;">Hoert zu, antwortet, spricht</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn mode-btn" data-mode="dnd" onclick="setMode('dnd')" style="background:#1E1E2E;border:2px solid transparent;">
|
||||||
|
<span style="font-size:18px;">🔴</span> Nicht stoeren<br><span style="font-size:10px;color:#8888AA;">Nur Kritikalarme</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn mode-btn" data-mode="whisper" onclick="setMode('whisper')" style="background:#1E1E2E;border:2px solid transparent;">
|
||||||
|
<span style="font-size:18px;">🟡</span> Fluestern<br><span style="font-size:10px;color:#8888AA;">Nur Text, keine Sprache</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn mode-btn" data-mode="hangar" onclick="setMode('hangar')" style="background:#1E1E2E;border:2px solid transparent;">
|
||||||
|
<span style="font-size:18px;">✈️</span> Hangar<br><span style="font-size:10px;color:#8888AA;">Nur wichtige Meldungen</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn mode-btn" data-mode="gaming" onclick="setMode('gaming')" style="background:#1E1E2E;border:2px solid transparent;grid-column:1/-1;">
|
||||||
|
<span style="font-size:18px;">🎮</span> Gaming<br><span style="font-size:10px;color:#8888AA;">Nur direkte Fragen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;font-size:11px;color:#555570;" id="mode-status">Aktueller Modus: Normal</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stimmen -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>Sprachausgabe</h2>
|
||||||
|
<div class="card" style="max-width:500px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||||
|
<label style="color:#8888AA;font-size:12px;">Standard-Stimme:</label>
|
||||||
|
<select id="diag-default-voice" onchange="sendVoiceConfig()" style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||||||
|
<option value="ramona">Ramona (weiblich)</option>
|
||||||
|
<option value="thorsten">Thorsten (maennlich)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||||
|
<label style="color:#8888AA;font-size:12px;">Highlight-Stimme:</label>
|
||||||
|
<select id="diag-highlight-voice" onchange="sendVoiceConfig()" style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||||||
|
<option value="thorsten">Thorsten (maennlich)</option>
|
||||||
|
<option value="ramona">Ramona (weiblich)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;">
|
||||||
|
<label style="color:#8888AA;font-size:12px;">TTS aktiv:</label>
|
||||||
|
<label class="toggle"><input type="checkbox" id="diag-tts-enabled" checked onchange="sendVoiceConfig()"><span class="slider"></span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Highlight-Trigger -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>Highlight-Trigger</h2>
|
||||||
|
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
|
||||||
|
Woerter die automatisch die Highlight-Stimme (Thorsten) ausloesen.
|
||||||
|
Eines pro Zeile. Aenderungen werden in der Bridge gespeichert.
|
||||||
|
</div>
|
||||||
|
<div class="card" style="max-width:500px;">
|
||||||
|
<textarea id="highlight-triggers" rows="8" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:6px;padding:8px;color:#fff;font-size:13px;font-family:monospace;resize:vertical;"
|
||||||
|
placeholder="Lade..."></textarea>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:8px;">
|
||||||
|
<button class="btn" onclick="saveHighlightTriggers()" style="flex:1;">Speichern</button>
|
||||||
|
<button class="btn secondary" onclick="loadHighlightTriggers()" style="flex:1;">Neu laden</button>
|
||||||
|
</div>
|
||||||
|
<div id="trigger-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tool-Berechtigungen -->
|
<!-- Tool-Berechtigungen -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h2>Tool-Berechtigungen</h2>
|
<h2>Tool-Berechtigungen</h2>
|
||||||
@@ -420,6 +519,7 @@
|
|||||||
bridge: document.getElementById('log-bridge'),
|
bridge: document.getElementById('log-bridge'),
|
||||||
server: document.getElementById('log-server'),
|
server: document.getElementById('log-server'),
|
||||||
pipeline: document.getElementById('log-pipeline'),
|
pipeline: document.getElementById('log-pipeline'),
|
||||||
|
tts: document.getElementById('log-tts'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scroll-Pause pro aktivem Tab
|
// Scroll-Pause pro aktivem Tab
|
||||||
@@ -513,11 +613,51 @@
|
|||||||
if (msg.type === 'state') { updateState(msg.state); return; }
|
if (msg.type === 'state') { updateState(msg.state); return; }
|
||||||
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
|
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
|
||||||
|
|
||||||
|
if (msg.type === 'tts_result') {
|
||||||
|
if (msg.ok) {
|
||||||
|
ttsLog(`\u2705 ${msg.voice}: ${msg.duration}ms, ${msg.size} bytes`);
|
||||||
|
document.getElementById('tts-status').textContent = 'OK';
|
||||||
|
document.getElementById('tts-status').style.color = '#34C759';
|
||||||
|
} else {
|
||||||
|
ttsLog(`\u274C Fehler: ${msg.error}`);
|
||||||
|
document.getElementById('tts-status').textContent = 'Fehler';
|
||||||
|
document.getElementById('tts-status').style.color = '#FF3B30';
|
||||||
|
document.getElementById('tts-last-error').textContent = msg.error;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === 'tts_status') {
|
||||||
|
document.getElementById('tts-default-voice').textContent = msg.defaultVoice || '?';
|
||||||
|
document.getElementById('tts-highlight-voice').textContent = msg.highlightVoice || '?';
|
||||||
|
document.getElementById('tts-status').textContent = msg.ok ? 'OK' : 'Fehler';
|
||||||
|
document.getElementById('tts-status').style.color = msg.ok ? '#34C759' : '#FF3B30';
|
||||||
|
if (msg.voices) ttsLog(`Stimmen: ${msg.voices.join(', ')}`);
|
||||||
|
if (msg.error) { document.getElementById('tts-last-error').textContent = msg.error; ttsLog(`Fehler: ${msg.error}`); }
|
||||||
|
else { document.getElementById('tts-last-error').textContent = '-'; ttsLog('TTS OK'); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === 'agent_activity') {
|
if (msg.type === 'agent_activity') {
|
||||||
updateThinkingIndicator(msg);
|
updateThinkingIndicator(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'trigger_list') {
|
||||||
|
const textarea = document.getElementById('highlight-triggers');
|
||||||
|
textarea.value = (msg.triggers || []).join('\n');
|
||||||
|
document.getElementById('trigger-status').textContent = msg.triggers.length + ' Trigger geladen';
|
||||||
|
document.getElementById('trigger-status').style.color = '#8888AA';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'watchdog') {
|
||||||
|
const colors = { warning: '#FFD60A', fixing: '#FF9500', fixed: '#34C759', error: '#FF3B30' };
|
||||||
|
const color = colors[msg.status] || '#FFD60A';
|
||||||
|
addChat('error', `\u26A0\uFE0F Watchdog: ${msg.message}`, `system — ${msg.status}`);
|
||||||
|
addLog('warn', 'server', `Watchdog: ${msg.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === 'chat_final') {
|
if (msg.type === 'chat_final') {
|
||||||
addChat('received', msg.text, 'chat:final');
|
addChat('received', msg.text, 'chat:final');
|
||||||
return;
|
return;
|
||||||
@@ -991,6 +1131,64 @@
|
|||||||
}, 120000);
|
}, 120000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Stimmen-Config ──────────────────────────
|
||||||
|
function sendVoiceConfig() {
|
||||||
|
const defaultVoice = document.getElementById('diag-default-voice').value;
|
||||||
|
const highlightVoice = document.getElementById('diag-highlight-voice').value;
|
||||||
|
const ttsEnabled = document.getElementById('diag-tts-enabled').checked;
|
||||||
|
send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Highlight-Trigger ────────────────────────
|
||||||
|
function loadHighlightTriggers() {
|
||||||
|
send({ action: 'get_triggers' });
|
||||||
|
}
|
||||||
|
function saveHighlightTriggers() {
|
||||||
|
const text = document.getElementById('highlight-triggers').value;
|
||||||
|
const triggers = text.split('\n').map(t => t.trim()).filter(t => t.length > 0);
|
||||||
|
send({ action: 'save_triggers', triggers });
|
||||||
|
document.getElementById('trigger-status').textContent = 'Gespeichert (' + triggers.length + ' Trigger)';
|
||||||
|
document.getElementById('trigger-status').style.color = '#34C759';
|
||||||
|
}
|
||||||
|
// Beim Tab-Wechsel zu Einstellungen: Trigger laden
|
||||||
|
const origSwitchMainTab = typeof switchMainTab === 'function' ? switchMainTab : null;
|
||||||
|
|
||||||
|
// ── Modus-Wechsel ────────────────────────────
|
||||||
|
let currentMode = 'normal';
|
||||||
|
const MODE_LABELS = { normal: 'Normal', dnd: 'Nicht stoeren', whisper: 'Fluestern', hangar: 'Hangar', gaming: 'Gaming' };
|
||||||
|
|
||||||
|
function setMode(mode) {
|
||||||
|
currentMode = mode;
|
||||||
|
// Visuelles Feedback
|
||||||
|
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||||||
|
btn.style.borderColor = btn.dataset.mode === mode ? '#0096FF' : 'transparent';
|
||||||
|
});
|
||||||
|
document.getElementById('mode-status').textContent = `Aktueller Modus: ${MODE_LABELS[mode] || mode}`;
|
||||||
|
// An Bridge senden via RVS
|
||||||
|
sendToRVS(`ARIA, ${MODE_LABELS[mode]}-Modus`, false);
|
||||||
|
log("info", "server", `Modus gewechselt: ${mode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TTS Diagnose ─────────────────────────────
|
||||||
|
function ttsLog(msg) {
|
||||||
|
const el = document.getElementById('tts-log');
|
||||||
|
const time = new Date().toLocaleTimeString('de-DE');
|
||||||
|
el.innerHTML += `<div>[${time}] ${escapeHtml(msg)}</div>`;
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testTTS(voice) {
|
||||||
|
const text = document.getElementById('tts-test-text').value.trim();
|
||||||
|
if (!text) return;
|
||||||
|
ttsLog(`Teste ${voice}: "${text}"...`);
|
||||||
|
send({ action: 'test_tts', voice, text });
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTTSStatus() {
|
||||||
|
ttsLog('Pruefe TTS-Status...');
|
||||||
|
send({ action: 'check_tts' });
|
||||||
|
}
|
||||||
|
|
||||||
function openLightbox(mediaType, url) {
|
function openLightbox(mediaType, url) {
|
||||||
const lb = document.getElementById('lightbox');
|
const lb = document.getElementById('lightbox');
|
||||||
if (mediaType === 'video') {
|
if (mediaType === 'video') {
|
||||||
@@ -1389,6 +1587,8 @@
|
|||||||
document.querySelectorAll('.main-nav-btn').forEach(b => {
|
document.querySelectorAll('.main-nav-btn').forEach(b => {
|
||||||
if (b.textContent.trim().toLowerCase().includes(tab === 'main' ? 'main' : 'einstellung')) b.classList.add('active');
|
if (b.textContent.trim().toLowerCase().includes(tab === 'main' ? 'main' : 'einstellung')) b.classList.add('active');
|
||||||
});
|
});
|
||||||
|
// Einstellungen: Trigger laden
|
||||||
|
if (tab === 'settings') loadHighlightTriggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Einstellungen: Tool-Berechtigungen ──────────────────
|
// ── Einstellungen: Tool-Berechtigungen ──────────────────
|
||||||
|
|||||||
+186
-2
@@ -336,6 +336,7 @@ function handleGatewayMessage(msg) {
|
|||||||
|
|
||||||
// Genereller Activity-Heartbeat (ARIA denkt)
|
// Genereller Activity-Heartbeat (ARIA denkt)
|
||||||
broadcast({ type: "agent_activity", activity: stream || "thinking" });
|
broadcast({ type: "agent_activity", activity: stream || "thinking" });
|
||||||
|
updateAgentActivity();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +353,8 @@ function handleGatewayMessage(msg) {
|
|||||||
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
||||||
broadcast({ type: "chat_final", text, payload });
|
broadcast({ type: "chat_final", text, payload });
|
||||||
broadcast({ type: "agent_activity", activity: "idle" });
|
broadcast({ type: "agent_activity", activity: "idle" });
|
||||||
|
pendingMessageTime = 0; // Watchdog: Antwort erhalten
|
||||||
|
updateAgentActivity();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +427,7 @@ function sendToGateway(text, isPipeline) {
|
|||||||
const payload = JSON.stringify(msg);
|
const payload = JSON.stringify(msg);
|
||||||
log("debug", "gateway", `RAW >>> ${payload}`);
|
log("debug", "gateway", `RAW >>> ${payload}`);
|
||||||
gatewayWs.send(payload);
|
gatewayWs.send(payload);
|
||||||
|
pendingMessageTime = Date.now(); // Watchdog: Nachricht gesendet
|
||||||
log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
|
log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
|
||||||
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
|
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
|
||||||
|
|
||||||
@@ -499,6 +503,17 @@ function connectRVS(forcePlain) {
|
|||||||
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
|
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
|
||||||
}
|
}
|
||||||
broadcast({ type: "rvs_chat", msg });
|
broadcast({ type: "rvs_chat", msg });
|
||||||
|
} else if (msg.type === "file_saved" && msg.payload) {
|
||||||
|
// Bild/Datei-Upload von der App — im Chat anzeigen
|
||||||
|
const name = msg.payload.name || "?";
|
||||||
|
const serverPath = msg.payload.serverPath || "";
|
||||||
|
const mimeType = msg.payload.mimeType || "";
|
||||||
|
log("info", "rvs", `Datei empfangen: ${name} (${serverPath})`);
|
||||||
|
// Als User-Nachricht mit Pfad broadcasten (Diagnostic zeigt Bilder inline)
|
||||||
|
broadcast({ type: "rvs_chat", msg: {
|
||||||
|
type: "chat",
|
||||||
|
payload: { text: `Anhang: ${name}\n${serverPath}`, sender: "user" }
|
||||||
|
}});
|
||||||
} else if (msg.type === "heartbeat") {
|
} else if (msg.type === "heartbeat") {
|
||||||
// ignorieren
|
// ignorieren
|
||||||
} else {
|
} else {
|
||||||
@@ -534,6 +549,18 @@ function connectRVS(forcePlain) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendToRVS_raw(msgObj) {
|
||||||
|
if (!RVS_HOST || !RVS_TOKEN) return;
|
||||||
|
const proto = RVS_TLS === "true" ? "wss" : "ws";
|
||||||
|
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
|
||||||
|
const freshWs = new WebSocket(url);
|
||||||
|
freshWs.on("open", () => {
|
||||||
|
freshWs.send(JSON.stringify(msgObj));
|
||||||
|
setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 5000);
|
||||||
|
});
|
||||||
|
freshWs.on("error", () => {});
|
||||||
|
}
|
||||||
|
|
||||||
function sendToRVS(text, isPipeline) {
|
function sendToRVS(text, isPipeline) {
|
||||||
if (!RVS_HOST || !RVS_TOKEN) {
|
if (!RVS_HOST || !RVS_TOKEN) {
|
||||||
log("error", "rvs", "Nicht konfiguriert");
|
log("error", "rvs", "Nicht konfiguriert");
|
||||||
@@ -564,8 +591,8 @@ function sendToRVS(text, isPipeline) {
|
|||||||
const resp = JSON.parse(raw.toString());
|
const resp = JSON.parse(raw.toString());
|
||||||
if (resp.type === "chat" && resp.payload) {
|
if (resp.type === "chat" && resp.payload) {
|
||||||
const sender = resp.payload.sender || "?";
|
const sender = resp.payload.sender || "?";
|
||||||
// Eigene Nachrichten nicht nochmal anzeigen (Echo von RVS)
|
// Eigene Nachrichten und STT ignorieren (werden von persistenter Verbindung gehandelt)
|
||||||
if (sender === "diagnostic") return;
|
if (sender === "diagnostic" || sender === "stt") return;
|
||||||
log("info", "rvs", `Chat von ${sender}: "${(resp.payload.text || "").slice(0, 100)}"`);
|
log("info", "rvs", `Chat von ${sender}: "${(resp.payload.text || "").slice(0, 100)}"`);
|
||||||
if (pipelineActive && sender !== "diagnostic") {
|
if (pipelineActive && sender !== "diagnostic") {
|
||||||
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(resp.payload.text || "").slice(0, 120)}"`);
|
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(resp.payload.text || "").slice(0, 120)}"`);
|
||||||
@@ -1006,6 +1033,46 @@ function waitForMessage(ws, timeoutMs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Watchdog: Stuck Run Erkennung ────────────────────────
|
||||||
|
|
||||||
|
let lastAgentActivity = Date.now();
|
||||||
|
let watchdogWarned = false;
|
||||||
|
let pendingMessageTime = 0; // Wann wurde die letzte Nachricht gesendet
|
||||||
|
|
||||||
|
function updateAgentActivity() {
|
||||||
|
lastAgentActivity = Date.now();
|
||||||
|
watchdogWarned = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watchdog prüft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert
|
||||||
|
setInterval(async () => {
|
||||||
|
if (pendingMessageTime === 0) return; // Keine Nachricht gesendet
|
||||||
|
const waitingMs = Date.now() - pendingMessageTime;
|
||||||
|
|
||||||
|
// Nach 2min ohne Agent-Activity: Warnung
|
||||||
|
if (waitingMs > 120000 && !watchdogWarned) {
|
||||||
|
watchdogWarned = true;
|
||||||
|
log("warn", "server", `Watchdog: Keine ARIA-Aktivitaet seit ${Math.round(waitingMs / 1000)}s — moeglicherweise stuck`);
|
||||||
|
broadcast({ type: "watchdog", status: "warning", waitingMs, message: "ARIA reagiert nicht — moeglicherweise stuck Run" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach 5min: Auto-Fix anbieten
|
||||||
|
if (waitingMs > 300000 && watchdogWarned) {
|
||||||
|
log("error", "server", "Watchdog: 5min ohne Antwort — fuehre openclaw doctor --fix aus");
|
||||||
|
broadcast({ type: "watchdog", status: "fixing", message: "Auto-Fix: openclaw doctor --fix" });
|
||||||
|
try {
|
||||||
|
await dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true");
|
||||||
|
log("info", "server", "Watchdog: doctor --fix ausgefuehrt");
|
||||||
|
broadcast({ type: "watchdog", status: "fixed", message: "doctor --fix ausgefuehrt — sende Nachricht erneut" });
|
||||||
|
} catch (err) {
|
||||||
|
log("error", "server", `Watchdog: doctor --fix fehlgeschlagen: ${err.message}`);
|
||||||
|
broadcast({ type: "watchdog", status: "error", message: `Auto-Fix fehlgeschlagen: ${err.message}` });
|
||||||
|
}
|
||||||
|
pendingMessageTime = 0; // Reset
|
||||||
|
watchdogWarned = false;
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
// ── HTTP Server + WebSocket fuer Browser ────────────────
|
// ── HTTP Server + WebSocket fuer Browser ────────────────
|
||||||
|
|
||||||
const htmlPath = path.join(__dirname, "index.html");
|
const htmlPath = path.join(__dirname, "index.html");
|
||||||
@@ -1092,6 +1159,22 @@ wss.on("connection", (ws) => {
|
|||||||
if (ws._sshSock) ws._sshSock.write(msg.data);
|
if (ws._sshSock) ws._sshSock.write(msg.data);
|
||||||
} else if (msg.action === "live_ssh_close") {
|
} else if (msg.action === "live_ssh_close") {
|
||||||
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
|
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
|
||||||
|
} else if (msg.action === "send_voice_config") {
|
||||||
|
// Stimmen-Config an Bridge via RVS senden
|
||||||
|
sendToRVS_raw({ type: "config", payload: {
|
||||||
|
defaultVoice: msg.defaultVoice,
|
||||||
|
highlightVoice: msg.highlightVoice,
|
||||||
|
ttsEnabled: msg.ttsEnabled,
|
||||||
|
}, timestamp: Date.now() });
|
||||||
|
log("info", "server", `Voice-Config gesendet: default=${msg.defaultVoice}, highlight=${msg.highlightVoice}, tts=${msg.ttsEnabled}`);
|
||||||
|
} else if (msg.action === "get_triggers") {
|
||||||
|
handleGetTriggers(ws);
|
||||||
|
} else if (msg.action === "save_triggers") {
|
||||||
|
handleSaveTriggers(ws, msg.triggers || []);
|
||||||
|
} else if (msg.action === "test_tts") {
|
||||||
|
handleTestTTS(ws, msg.voice || "ramona", msg.text || "Test");
|
||||||
|
} else if (msg.action === "check_tts") {
|
||||||
|
handleCheckTTS(ws);
|
||||||
} else if (msg.action === "check_desktop") {
|
} else if (msg.action === "check_desktop") {
|
||||||
checkDesktopAvailable(ws);
|
checkDesktopAvailable(ws);
|
||||||
} else if (msg.action === "load_chat_history") {
|
} else if (msg.action === "load_chat_history") {
|
||||||
@@ -1218,6 +1301,107 @@ function startLiveSSH(clientWs) {
|
|||||||
createReq.end(createBody);
|
createReq.end(createBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Highlight-Trigger ─────────────────────────────────
|
||||||
|
|
||||||
|
const TRIGGERS_FILE = "/shared/config/highlight_triggers.json";
|
||||||
|
|
||||||
|
async function handleGetTriggers(clientWs) {
|
||||||
|
try {
|
||||||
|
// Zuerst aus Shared Volume lesen, dann Fallback auf Bridge-Defaults
|
||||||
|
let triggers;
|
||||||
|
if (fs.existsSync(TRIGGERS_FILE)) {
|
||||||
|
triggers = JSON.parse(fs.readFileSync(TRIGGERS_FILE, "utf-8"));
|
||||||
|
} else {
|
||||||
|
// Defaults aus der Bridge lesen
|
||||||
|
const result = await dockerExec("aria-bridge", `python3 -c "
|
||||||
|
import sys; sys.path.insert(0,'/app')
|
||||||
|
from aria_bridge import EPIC_TRIGGERS
|
||||||
|
print('\\n'.join(EPIC_TRIGGERS))
|
||||||
|
"`);
|
||||||
|
triggers = result.trim().split("\n").filter(t => t);
|
||||||
|
}
|
||||||
|
clientWs.send(JSON.stringify({ type: "trigger_list", triggers }));
|
||||||
|
} catch (err) {
|
||||||
|
clientWs.send(JSON.stringify({ type: "trigger_list", triggers: [], error: err.message }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveTriggers(clientWs, triggers) {
|
||||||
|
try {
|
||||||
|
// In Shared Volume speichern (fuer Bridge lesbar)
|
||||||
|
fs.mkdirSync("/shared/config", { recursive: true });
|
||||||
|
fs.writeFileSync(TRIGGERS_FILE, JSON.stringify(triggers, null, 2));
|
||||||
|
log("info", "server", `${triggers.length} Highlight-Trigger gespeichert`);
|
||||||
|
// Bridge informieren (wird beim naechsten Start geladen)
|
||||||
|
clientWs.send(JSON.stringify({ type: "trigger_list", triggers }));
|
||||||
|
} catch (err) {
|
||||||
|
log("error", "server", `Trigger speichern fehlgeschlagen: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TTS Diagnose ──────────────────────────────────────
|
||||||
|
async function handleTestTTS(clientWs, voice, text) {
|
||||||
|
try {
|
||||||
|
log("info", "server", `TTS-Test: ${voice} — "${text}"`);
|
||||||
|
const result = await dockerExec("aria-bridge", `python3 -c "
|
||||||
|
import time, sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
from piper import PiperVoice
|
||||||
|
import wave, tempfile, os
|
||||||
|
voices = {'ramona': '/voices/de_DE-ramona-low.onnx', 'thorsten': '/voices/de_DE-thorsten-high.onnx'}
|
||||||
|
path = voices.get('${voice}')
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
print('FEHLER: Stimme nicht gefunden')
|
||||||
|
sys.exit(1)
|
||||||
|
v = PiperVoice.load(path)
|
||||||
|
start = time.time()
|
||||||
|
tmp = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
|
||||||
|
with wave.open(tmp.name, 'wb') as wf:
|
||||||
|
wf.setnchannels(1)
|
||||||
|
wf.setsampwidth(2)
|
||||||
|
wf.setframerate(v.config.sample_rate)
|
||||||
|
v.synthesize('${text.replace(/'/g, "\\'")}', wf)
|
||||||
|
size = os.path.getsize(tmp.name)
|
||||||
|
dur = int((time.time() - start) * 1000)
|
||||||
|
os.unlink(tmp.name)
|
||||||
|
print(f'OK:{dur}:{size}')
|
||||||
|
"`);
|
||||||
|
const parts = result.trim().split(":");
|
||||||
|
if (parts[0] === "OK") {
|
||||||
|
clientWs.send(JSON.stringify({ type: "tts_result", ok: true, voice, duration: parts[1], size: parts[2] }));
|
||||||
|
} else {
|
||||||
|
clientWs.send(JSON.stringify({ type: "tts_result", ok: false, voice, error: result.trim() }));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
clientWs.send(JSON.stringify({ type: "tts_result", ok: false, voice, error: err.message }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCheckTTS(clientWs) {
|
||||||
|
try {
|
||||||
|
const result = await dockerExec("aria-bridge", `python3 -c "
|
||||||
|
import os, json
|
||||||
|
voices = {}
|
||||||
|
for name, path in [('ramona', '/voices/de_DE-ramona-low.onnx'), ('thorsten', '/voices/de_DE-thorsten-high.onnx')]:
|
||||||
|
voices[name] = os.path.exists(path)
|
||||||
|
print(json.dumps(voices))
|
||||||
|
"`);
|
||||||
|
const voices = JSON.parse(result.trim());
|
||||||
|
const available = Object.entries(voices).filter(([,v]) => v).map(([k]) => k);
|
||||||
|
const missing = Object.entries(voices).filter(([,v]) => !v).map(([k]) => k);
|
||||||
|
clientWs.send(JSON.stringify({
|
||||||
|
type: "tts_status",
|
||||||
|
ok: missing.length === 0,
|
||||||
|
voices: available,
|
||||||
|
defaultVoice: "ramona",
|
||||||
|
highlightVoice: "thorsten",
|
||||||
|
error: missing.length > 0 ? `Fehlend: ${missing.join(", ")}` : null,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
clientWs.send(JSON.stringify({ type: "tts_status", ok: false, error: err.message }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkDesktopAvailable(clientWs) {
|
function checkDesktopAvailable(clientWs) {
|
||||||
// Pruefen ob VNC auf der VM laeuft (Port 5900/5901)
|
// Pruefen ob VNC auf der VM laeuft (Port 5900/5901)
|
||||||
const checkSock = net.connect({ host: "host.docker.internal", port: 5901 }, () => {
|
const checkSock = net.connect({ host: "host.docker.internal", port: 5901 }, () => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
||||||
- ./aria-data/ssh:/root/.ssh:ro # SSH Keys fuer VM-Zugriff (aria-wohnung)
|
- ./aria-data/ssh:/root/.ssh:ro # SSH Keys fuer VM-Zugriff (aria-wohnung)
|
||||||
|
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
|
||||||
environment:
|
environment:
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash)
|
- SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash)
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
bildupload ghet noch nicht.
|
# erledigt bildupload ghet noch nicht.
|
||||||
|
|
||||||
|
# erledigt
|
||||||
sprachnachrichten werden nicht als zweite nachricht dargestellt, damit man weiß was man gesendet hat
|
sprachnachrichten werden nicht als zweite nachricht dargestellt, damit man weiß was man gesendet hat
|
||||||
cache leeren, bilder werden nicht neu geladen beim antippen.
|
# ende
|
||||||
|
|
||||||
|
|
||||||
|
# erledigt cache leeren, bilder werden nicht neu geladen beim antippen.
|
||||||
autoload geht nicht
|
autoload geht nicht
|
||||||
|
# ende
|
||||||
|
|
||||||
wenn man auf das ohr zum hören klickt stürzt ab
|
wenn man auf das ohr zum hören klickt stürzt ab
|
||||||
|
|
||||||
aria liest die nachrichten nicht vor
|
aria liest die nachrichten nicht vor
|
||||||
autoscroll geht doch noch nicht zur letzten nachricht
|
|
||||||
|
|
||||||
|
# erledigt autoscroll geht doch noch nicht zur letzten nachricht
|
||||||
|
unserer memory brain
|
||||||
|
# ende
|
||||||
|
|
||||||
|
# erledigt bilder im chat größer darstellen
|
||||||
|
# ende
|
||||||
|
|
||||||
|
|
||||||
|
die viper voices downloaden über die diagnostic
|
||||||
|
# ende
|
||||||
+1
-1
@@ -9,7 +9,7 @@ const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
|
|||||||
// Erlaubte Nachrichtentypen — alles andere wird verworfen
|
// Erlaubte Nachrichtentypen — alles andere wird verworfen
|
||||||
const ALLOWED_TYPES = new Set([
|
const ALLOWED_TYPES = new Set([
|
||||||
"chat", "audio", "file", "location", "mode", "log", "event", "heartbeat",
|
"chat", "audio", "file", "location", "mode", "log", "event", "heartbeat",
|
||||||
"file_request", "file_response", "file_saved", "stt_result",
|
"file_request", "file_response", "file_saved", "stt_result", "config",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Token-Raum: token -> { clients: Set<ws> }
|
// Token-Raum: token -> { clients: Set<ws> }
|
||||||
|
|||||||
Reference in New Issue
Block a user