Compare commits

..

26 Commits

Author SHA1 Message Date
duffyduck a2c0196e05 release: bump version to 0.0.2.1 2026-03-29 18:49:37 +02:00
duffyduck 680f7a64e2 slpit setnteces 2026-03-29 18:42:24 +02:00
duffyduck 4893616a5a playback issue 2026-03-29 18:36:00 +02:00
duffyduck 04e8c0245d voiice settings permanent 2026-03-29 18:23:31 +02:00
duffyduck 10cefaf1cd changed connection model 2026-03-29 18:12:26 +02:00
duffyduck adbb1fe80a changed docker file 2026-03-29 17:46:27 +02:00
duffyduck 79c50aedcc release: bump version to 0.0.2.0 2026-03-29 17:42:23 +02:00
duffyduck eb72b35e23 added voice settings in adroid app and diagnostic, higlight trigger in app und diagnostic
change voicec
2026-03-29 17:41:28 +02:00
duffyduck bbd02d46a6 changed issue md 2026-03-29 17:28:40 +02:00
duffyduck 3d3c8ce973 fixed tts format, added trigger words settings 2026-03-29 17:27:43 +02:00
duffyduck 562f929056 added setting for states and voices in setting diagnostic, added states in diagnostic, added watchdog and debug tts do diagnostic 2026-03-29 17:12:25 +02:00
duffyduck ff03d8ce62 release: bump version to 0.0.1.9 2026-03-29 17:11:33 +02:00
duffyduck 8281131432 tts fix big pictures 2026-03-29 17:02:02 +02:00
duffyduck 8a6bd4e0e7 voice message are send double to diagnostic 2026-03-29 16:50:48 +02:00
duffyduck 1b4df0565a wait at an attachment for instructions, show picture in diagnostic chat 2026-03-29 16:42:56 +02:00
duffyduck eb3692ef81 fixed arai proxy shared volume 2026-03-29 16:34:55 +02:00
duffyduck 46a9ac9f84 release: bump version to 0.0.1.8 2026-03-29 16:25:37 +02:00
duffyduck a012ec65ef filter own sender to hide own messages, these ar sendet from rvs twice 2026-03-29 16:15:10 +02:00
duffyduck b86c4a0d1a fixed double diagnostic message 2026-03-29 16:12:24 +02:00
duffyduck 11de9a01b9 error through loops no message received, fixed 2026-03-29 16:08:37 +02:00
duffyduck 80dec2daf9 reset connection as every send message 2026-03-29 16:04:43 +02:00
duffyduck da591bb53c fixed fallback issue clodes before sessions 2026-03-29 15:58:39 +02:00
duffyduck 7545c9c823 check still open 2026-03-29 15:53:11 +02:00
duffyduck ecc3d59a8f change rvs server 2026-03-29 15:40:17 +02:00
duffyduck b8862f025b fixed, thinking in webgui 2026-03-29 15:10:41 +02:00
duffyduck db20a07b27 fixed time out aria-core 2026-03-29 14:56:55 +02:00
11 changed files with 930 additions and 76 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "0.0.1.7"
versionCode 201
versionName "0.0.2.1"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.0.1.7",
"version": "0.0.2.1",
"private": true,
"scripts": {
"android": "react-native run-android",
+33 -3
View File
@@ -90,6 +90,7 @@ const ChatScreen: React.FC = () => {
const [showCameraUpload, setShowCameraUpload] = useState(false);
const [gpsEnabled, setGpsEnabled] = useState(false);
const [wakeWordActive, setWakeWordActive] = useState(false);
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
const flatListRef = useRef<FlatList>(null);
const messageIdCounter = useRef(0);
@@ -525,12 +526,12 @@ const ChatScreen: React.FC = () => {
{item.attachments?.map((att, idx) => (
<View key={idx}>
{att.type === 'image' && att.uri ? (
<TouchableOpacity onPress={() => setFullscreenImage(att.uri || null)} activeOpacity={0.8}>
<Image
source={{ uri: att.uri }}
style={styles.attachmentImage}
resizeMode="contain"
resizeMode="cover"
onError={() => {
// Bild nicht mehr verfuegbar — Placeholder setzen
setMessages(prev => prev.map(m =>
m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) =>
i === idx ? { ...a, uri: undefined } : a
@@ -538,6 +539,7 @@ const ChatScreen: React.FC = () => {
));
}}
/>
</TouchableOpacity>
) : att.type === 'image' && !att.uri ? (
<TouchableOpacity
style={styles.attachmentFile}
@@ -675,6 +677,23 @@ const ChatScreen: React.FC = () => {
)}
</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 */}
<Modal visible={showFileUpload} transparent animationType="slide">
<View style={styles.modalOverlay}>
@@ -757,7 +776,8 @@ const styles = StyleSheet.create({
},
attachmentImage: {
width: '100%',
height: 200,
minHeight: 200,
maxHeight: 400,
borderRadius: 8,
marginBottom: 6,
backgroundColor: '#0D0D1A',
@@ -867,6 +887,16 @@ const styles = StyleSheet.create({
wakeWordIcon: {
fontSize: 16,
},
fullscreenOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.95)',
justifyContent: 'center',
alignItems: 'center',
},
fullscreenImage: {
width: '100%',
height: '100%',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.6)',
+168 -1
View File
@@ -71,6 +71,10 @@ const SettingsScreen: React.FC = () => {
const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH);
const [autoDownload, setAutoDownload] = useState(true);
const [storageSize, setStorageSize] = useState('...');
const [ttsEnabled, setTtsEnabled] = useState(true);
const [defaultVoice, setDefaultVoice] = useState('ramona');
const [highlightVoice, setHighlightVoice] = useState('thorsten');
const [speechSpeed, setSpeechSpeed] = useState(1.0);
const [editingPath, setEditingPath] = useState(false);
const [tempPath, setTempPath] = useState('');
@@ -91,6 +95,18 @@ const SettingsScreen: React.FC = () => {
AsyncStorage.getItem('aria_auto_download').then(saved => {
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);
});
AsyncStorage.getItem('aria_speech_speed').then(saved => {
if (saved) setSpeechSpeed(parseFloat(saved));
});
}, []);
// Speichergroesse berechnen
@@ -442,6 +458,125 @@ const SettingsScreen: React.FC = () => {
</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>
{/* Sprechgeschwindigkeit */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Sprechgeschwindigkeit: {speechSpeed.toFixed(1)}x</Text>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 8}}>
<Text style={{color: '#555570', fontSize: 11}}>0.5x</Text>
<View style={{flex: 1}}>
<TouchableOpacity
style={{height: 30, justifyContent: 'center'}}
onPress={(e) => {
const layout = e.nativeEvent;
// Einfacher Tap-basierter Slider
}}
>
<View style={{height: 4, backgroundColor: '#2A2A3E', borderRadius: 2}}>
<View style={{height: 4, backgroundColor: '#0096FF', borderRadius: 2, width: `${((speechSpeed - 0.5) / 1.5) * 100}%`}} />
</View>
</TouchableOpacity>
</View>
<Text style={{color: '#555570', fontSize: 11}}>2.0x</Text>
</View>
<View style={{flexDirection: 'row', justifyContent: 'space-around', marginTop: 8}}>
{[0.5, 0.75, 1.0, 1.25, 1.5, 2.0].map(speed => (
<TouchableOpacity
key={speed}
onPress={() => {
setSpeechSpeed(speed);
AsyncStorage.setItem('aria_speech_speed', String(speed));
rvs.send('config' as any, { speechSpeed: speed });
}}
style={{
paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6,
backgroundColor: speechSpeed === speed ? '#0096FF' : '#1E1E2E',
}}
>
<Text style={{color: speechSpeed === speed ? '#fff' : '#8888AA', fontSize: 12, fontWeight: '600'}}>
{speed}x
</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 === */}
<Text style={styles.sectionTitle}>Anhang-Speicher</Text>
<View style={styles.card}>
@@ -601,7 +736,7 @@ const SettingsScreen: React.FC = () => {
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
<View style={styles.card}>
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
<Text style={styles.aboutVersion}>Version 0.0.1.6 </Text>
<Text style={styles.aboutVersion}>Version 0.0.2.1 </Text>
<Text style={styles.aboutInfo}>
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
Gebaut mit React Native + TypeScript.
@@ -744,6 +879,38 @@ const styles = StyleSheet.create({
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
storagePathText: {
color: '#0096FF',
+136 -22
View File
@@ -72,7 +72,7 @@ BLOCK_SIZE = 1280 # 80ms bei 16kHz — gut fuer Wake-Word-Erkennung
RECORD_SECONDS = 8 # Max. Aufnahmedauer nach Wake-Word
# Epische Trigger — bei diesen Woertern spricht Thorsten
EPIC_TRIGGERS = [
EPIC_TRIGGERS_DEFAULT = [
"deploy",
"erfolgreich",
"alarm",
@@ -84,6 +84,24 @@ EPIC_TRIGGERS = [
"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]:
"""Laedt Konfiguration aus /config/aria.env."""
@@ -111,6 +129,8 @@ class VoiceEngine:
def __init__(self, voices_dir: Path) -> None:
self.voices_dir = voices_dir
self.voices: dict[str, PiperVoice] = {}
self.default_voice = "ramona"
self.highlight_voice = "thorsten"
def initialize(self) -> None:
"""Laedt die Piper-Stimmen aus dem Voices-Verzeichnis."""
@@ -154,14 +174,14 @@ class VoiceEngine:
if requested_voice and requested_voice in self.voices:
return requested_voice
# Epische Trigger pruefen
# Highlight-Trigger pruefen
text_lower = text.lower()
for trigger in EPIC_TRIGGERS:
if trigger in text_lower:
logger.info("Epischer Trigger erkannt: '%s'Thorsten spricht", trigger)
return "thorsten"
logger.info("Highlight-Trigger erkannt: '%s'%s spricht", trigger, self.highlight_voice)
return self.highlight_voice
return "ramona"
return self.default_voice
def synthesize(self, text: str, voice_name: str = "ramona") -> Optional[bytes]:
"""Erzeugt Audio-Daten aus Text mit der gewaehlten Stimme.
@@ -179,20 +199,48 @@ class VoiceEngine:
return None
try:
# Piper gibt PCM-Samples zurueck, wir schreiben sie als WAV
# Langen Text in Saetze aufteilen (Piper hat Limits bei langen Texten)
import re
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
# Markdown-Formatierung entfernen
sentences = [re.sub(r'\*\*([^*]+)\*\*', r'\1', s).strip() for s in sentences if s.strip()]
if not sentences:
return None
# Jeden Satz einzeln synthetisieren und WAVs zusammenfuegen
all_audio = b""
sample_rate = None
for sentence in sentences:
if not sentence:
continue
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
tmp_path = tmp.name
with wave.open(tmp_path, "wb") as wav_file:
voice.synthesize_wav(sentence, wav_file)
with wave.open(tmp_path, "rb") as wav_file:
if sample_rate is None:
sample_rate = wav_file.getframerate()
all_audio += wav_file.readframes(wav_file.getnframes())
Path(tmp_path).unlink(missing_ok=True)
# Zusammengefuegtes WAV erstellen
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
tmp_path = tmp.name
final_path = tmp.name
with wave.open(final_path, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate or 22050)
wav_file.writeframes(all_audio)
with wave.open(tmp_path, "wb") as wav_file:
voice.synthesize(text, wav_file)
audio_data = Path(tmp_path).read_bytes()
Path(tmp_path).unlink(missing_ok=True)
audio_data = Path(final_path).read_bytes()
Path(final_path).unlink(missing_ok=True)
logger.info(
"TTS: %d bytes erzeugt mit %s'%s'",
"TTS: %d bytes erzeugt mit %s (%d Saetze)'%s'",
len(audio_data),
voice_name,
len(sentences),
text[:60],
)
return audio_data
@@ -437,6 +485,19 @@ class ARIABridge:
# Komponenten
self.voice_engine = VoiceEngine(VOICES_DIR)
self.tts_enabled = True
# Gespeicherte Voice-Config laden
try:
vc_path = "/shared/config/voice_config.json"
if os.path.exists(vc_path):
with open(vc_path) as f:
vc = json.load(f)
self.voice_engine.default_voice = vc.get("defaultVoice", "ramona")
self.voice_engine.highlight_voice = vc.get("highlightVoice", "thorsten")
self.tts_enabled = vc.get("ttsEnabled", True)
logger.info("Voice-Config geladen: %s", vc)
except Exception as e:
logger.warning("Voice-Config laden fehlgeschlagen: %s", e)
self.stt_engine = STTEngine(
model_size=self.config.get("WHISPER_MODEL", WHISPER_MODEL),
language=self.config.get("WHISPER_LANGUAGE", WHISPER_LANGUAGE),
@@ -464,7 +525,9 @@ class ARIABridge:
# Audio-Hardware pruefen (fuer lokales Mikro/Lautsprecher)
self.audio_available = False
try:
sd.query_devices()
devices = sd.query_devices()
# Pruefen ob ein Output-Device existiert
sd.query_devices(kind='output')
self.audio_available = True
logger.info("Audio-Geraet gefunden — Wake-Word und lokale TTS aktiv")
self.stt_engine.initialize()
@@ -773,7 +836,7 @@ class ARIABridge:
})
# 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)
if audio_data:
audio_b64 = base64.b64encode(audio_data).decode("ascii")
@@ -893,10 +956,22 @@ class ARIABridge:
retry_delay = min(retry_delay * 2, 30)
async def _rvs_heartbeat(self) -> None:
"""Sendet Heartbeats an den RVS damit die Verbindung offen bleibt."""
"""Sendet Heartbeats + WebSocket Pings an den RVS damit die Verbindung offen bleibt."""
while True:
await asyncio.sleep(25)
await asyncio.sleep(15)
if self.ws_rvs:
try:
# WebSocket Protocol-Level Ping (haelt TCP-Verbindung am Leben)
pong = await self.ws_rvs.ping()
await asyncio.wait_for(pong, timeout=10)
except Exception:
logger.warning("[rvs] Ping fehlgeschlagen — Verbindung tot, erzwinge Reconnect")
try:
await self.ws_rvs.close()
except Exception:
pass
self.ws_rvs = None
break
try:
await self.ws_rvs.send(json.dumps({
"type": "heartbeat",
@@ -927,8 +1002,43 @@ class ARIABridge:
if msg_type == "chat":
# Nur User-Nachrichten weiterleiten — ARIA/Diagnostic-Antworten ignorieren (sonst Loop!)
sender = payload.get("sender", "")
if sender in ("aria", "diagnostic", "stt"):
if sender in ("aria", "stt"):
return
elif msg_type == "config":
# Konfiguration von App/Diagnostic empfangen + persistent speichern
changed = False
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)
changed = True
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)
changed = True
if "ttsEnabled" in payload:
self.tts_enabled = bool(payload["ttsEnabled"])
logger.info("[rvs] TTS %s", "aktiviert" if self.tts_enabled else "deaktiviert")
changed = True
# Persistent speichern in Shared Volume
if changed:
try:
os.makedirs("/shared/config", exist_ok=True)
config_data = {
"defaultVoice": self.voice_engine.default_voice,
"highlightVoice": self.voice_engine.highlight_voice,
"ttsEnabled": getattr(self, "tts_enabled", True),
}
with open("/shared/config/voice_config.json", "w") as f:
json.dump(config_data, f, indent=2)
logger.info("[rvs] Voice-Config gespeichert: %s", config_data)
except Exception as e:
logger.warning("[rvs] Config speichern fehlgeschlagen: %s", e)
return
text = payload.get("text", "")
if text:
logger.info("[rvs] App-Chat: '%s'", text[:80])
@@ -984,7 +1094,8 @@ class ARIABridge:
text = (f"Stefan hat dir ein Bild geschickt: {file_name}"
f"{f' ({width}x{height}px)' if width else ''}"
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")
# Dann App informieren (optional, darf nicht crashen)
try:
@@ -1006,7 +1117,8 @@ class ARIABridge:
# ERST an aria-core senden
text = (f"Stefan hat dir eine Datei geschickt: {file_name}"
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")
try:
await self._send_to_rvs({
@@ -1216,8 +1328,10 @@ class ARIABridge:
logger.info("Keine Sprache erkannt — ignoriert")
except sd.PortAudioError:
logger.error("Audio-Geraet nicht verfuegbar — warte 5 Sekunden")
await asyncio.sleep(5)
if not hasattr(self, '_audio_warned'):
logger.warning("Audio-Geraet nicht verfuegbar — lokales Mikrofon deaktiviert (kein Spam mehr)")
self._audio_warned = True
await asyncio.sleep(60) # 60s statt 5s — spart Log-Spam
except Exception:
logger.exception("Fehler in der Audio-Schleife")
await asyncio.sleep(1)
+271
View File
@@ -201,6 +201,9 @@
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
</div>
<div class="chat-box" id="chat-box"></div>
<div id="thinking-indicator" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:0 0 6px 6px;margin-top:-8px;margin-bottom:8px;">
<span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text">ARIA denkt...</span>
</div>
<div class="input-row">
<input type="text" id="chat-input" placeholder="Nachricht an ARIA...">
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
@@ -216,6 +219,9 @@
<button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button>
</div>
<div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div>
<div id="thinking-indicator-fs" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:6px;margin-top:4px;">
<span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text-fs">ARIA denkt...</span>
</div>
<div class="input-row" style="margin-top:8px;">
<input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}">
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
@@ -277,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="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="tts" onclick="switchTab('tts')" style="border-color:#34C75944;color:#34C759">TTS</button>
</div>
</div>
<div class="log-panel">
@@ -296,6 +303,36 @@
<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-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>
@@ -334,6 +371,74 @@
<!-- ══════ TAB: Einstellungen ══════ -->
<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;">&#x1F7E2;</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;">&#x1F534;</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;">&#x1F7E1;</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;">&#x2708;&#xFE0F;</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;">&#x1F3AE;</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 -->
<div class="settings-section">
<h2>Tool-Berechtigungen</h2>
@@ -414,6 +519,7 @@
bridge: document.getElementById('log-bridge'),
server: document.getElementById('log-server'),
pipeline: document.getElementById('log-pipeline'),
tts: document.getElementById('log-tts'),
};
// Scroll-Pause pro aktivem Tab
@@ -507,6 +613,58 @@
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 === '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') {
updateThinkingIndicator(msg);
return;
}
if (msg.type === 'voice_config') {
document.getElementById('diag-default-voice').value = msg.defaultVoice || 'ramona';
document.getElementById('diag-highlight-voice').value = msg.highlightVoice || 'thorsten';
document.getElementById('diag-tts-enabled').checked = msg.ttsEnabled !== false;
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') {
addChat('received', msg.text, 'chat:final');
return;
@@ -883,6 +1041,10 @@
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
});
const html = `${linked}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
// In beide Chat-Boxen schreiben (normal + Vollbild)
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
@@ -930,6 +1092,110 @@
if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen();
});
// ── Thinking-Indikator ─────────────────────────────
let thinkingTimeout = null;
const TOOL_LABELS = {
'Bash': '\uD83D\uDDA5\uFE0F Shell-Befehl',
'WebFetch': '\uD83C\uDF10 Webseite abrufen',
'WebSearch': '\uD83D\uDD0D Suche',
'Read': '\uD83D\uDCC4 Datei lesen',
'Write': '\u270D\uFE0F Datei schreiben',
'Edit': '\u270D\uFE0F Datei bearbeiten',
'Grep': '\uD83D\uDD0D Code durchsuchen',
'Glob': '\uD83D\uDCC1 Dateien suchen',
'Agent': '\uD83E\uDD16 Sub-Agent',
};
function updateThinkingIndicator(msg) {
const indicators = [
document.getElementById('thinking-indicator'),
document.getElementById('thinking-indicator-fs'),
];
const texts = [
document.getElementById('thinking-text'),
document.getElementById('thinking-text-fs'),
];
if (msg.activity === 'idle') {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
if (thinkingTimeout) { clearTimeout(thinkingTimeout); thinkingTimeout = null; }
return;
}
let label = 'ARIA denkt...';
if (msg.activity === 'tool' && msg.tool) {
label = TOOL_LABELS[msg.tool] || `\uD83D\uDD27 ${msg.tool}`;
} else if (msg.activity === 'assistant') {
label = 'ARIA schreibt...';
}
indicators.forEach(el => { if (el) el.style.display = 'block'; });
texts.forEach(el => { if (el) el.textContent = label; });
// Auto-Hide nach 2min (falls idle Event verpasst wird — ARIA arbeitet max 15min)
if (thinkingTimeout) clearTimeout(thinkingTimeout);
thinkingTimeout = setTimeout(() => {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
}, 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) {
const lb = document.getElementById('lightbox');
if (mediaType === 'video') {
@@ -1328,6 +1594,11 @@
document.querySelectorAll('.main-nav-btn').forEach(b => {
if (b.textContent.trim().toLowerCase().includes(tab === 'main' ? 'main' : 'einstellung')) b.classList.add('active');
});
// Einstellungen: Config + Trigger laden
if (tab === 'settings') {
loadHighlightTriggers();
send({ action: 'get_voice_config' });
}
}
// ── Einstellungen: Tool-Berechtigungen ──────────────────
+284 -35
View File
@@ -74,8 +74,8 @@ function pipelineStart(method, text) {
pipelineStartTime = Date.now();
if (pipelineTimeout) clearTimeout(pipelineTimeout);
pipelineTimeout = setTimeout(() => {
if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 60s");
}, 60000);
if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 10min");
}, 600000);
plog(`━━━ Pipeline Start: ${method} ━━━`);
plog(`Nachricht: "${text}"`);
}
@@ -319,10 +319,24 @@ function handleGatewayMessage(msg) {
if (event === "agent") {
const data = payload.data || {};
const delta = data.delta || "";
if (delta && payload.stream === "assistant") {
const stream = payload.stream || "";
if (delta && stream === "assistant") {
broadcast({ type: "chat_delta", delta, payload });
}
// agent Events nicht einzeln loggen (zu viele)
// Tool-Nutzung erkennen und broadcasten
if (stream === "tool_use" || data.type === "tool_use") {
const toolName = data.name || data.tool || payload.tool || "";
if (toolName) {
broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
log("info", "gateway", `Tool: ${toolName}`);
}
}
// Genereller Activity-Heartbeat (ARIA denkt)
broadcast({ type: "agent_activity", activity: stream || "thinking" });
updateAgentActivity();
return;
}
@@ -338,6 +352,9 @@ function handleGatewayMessage(msg) {
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
broadcast({ type: "chat_final", text, payload });
broadcast({ type: "agent_activity", activity: "idle" });
pendingMessageTime = 0; // Watchdog: Antwort erhalten
updateAgentActivity();
return;
}
@@ -410,17 +427,11 @@ function sendToGateway(text, isPipeline) {
const payload = JSON.stringify(msg);
log("debug", "gateway", `RAW >>> ${payload}`);
gatewayWs.send(payload);
pendingMessageTime = Date.now(); // Watchdog: Nachricht gesendet
log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
// Nachricht auch an RVS senden damit die App sie sieht
if (rvsWs && rvsWs.readyState === WebSocket.OPEN) {
rvsWs.send(JSON.stringify({
type: "chat",
payload: { text, sender: "diagnostic" },
timestamp: Date.now(),
}));
}
// Gateway-Nachrichten NICHT an RVS senden (sonst doppelter ARIA-Request via Bridge)
return true;
}
@@ -434,7 +445,13 @@ function connectRVS(forcePlain) {
return;
}
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws (wenn erlaubt)
// Alte Verbindung sauber schliessen
if (rvsWs) {
try { rvsWs.removeAllListeners(); rvsWs.close(); } catch (_) {}
rvsWs = null;
}
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws
const useTls = RVS_TLS === "true" && !forcePlain;
const proto = useTls ? "wss" : "ws";
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
@@ -443,7 +460,18 @@ function connectRVS(forcePlain) {
broadcastState();
log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`);
const ws = new WebSocket(url);
let ws;
try {
ws = new WebSocket(url);
} catch (err) {
log("error", "rvs", `WebSocket erstellen fehlgeschlagen: ${err.message}`);
if (useTls && RVS_TLS_FALLBACK === "true") {
connectRVS(true);
}
return;
}
let fallbackTriggered = false;
ws.on("open", () => {
log("info", "rvs", `Verbunden (${proto})`);
@@ -451,6 +479,16 @@ function connectRVS(forcePlain) {
state.rvs.lastError = null;
rvsWs = ws;
broadcastState();
// Keepalive: alle 25s ein Ping senden damit die Verbindung nicht stirbt
const keepalive = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
try { ws.ping(); } catch (_) {}
} else {
clearInterval(keepalive);
}
}, 25000);
ws._keepalive = keepalive;
});
ws.on("message", (raw) => {
@@ -458,11 +496,24 @@ function connectRVS(forcePlain) {
const msg = JSON.parse(raw.toString());
if (msg.type === "chat" && msg.payload) {
const sender = msg.payload.sender || "?";
// Eigene Nachrichten ignorieren (Echo)
if (sender === "diagnostic") return;
log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`);
if (pipelineActive && sender !== "diagnostic") {
if (pipelineActive) {
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
}
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") {
// ignorieren
} else {
@@ -473,10 +524,13 @@ function connectRVS(forcePlain) {
ws.on("close", () => {
log("warn", "rvs", "Verbindung geschlossen");
if (ws._keepalive) clearInterval(ws._keepalive);
state.rvs.status = "disconnected";
rvsWs = null;
if (rvsWs === ws) rvsWs = null;
broadcastState();
setTimeout(() => connectRVS(), 5000);
if (!fallbackTriggered) {
setTimeout(() => connectRVS(), 5000);
}
});
ws.on("error", (err) => {
@@ -484,31 +538,46 @@ function connectRVS(forcePlain) {
state.rvs.lastError = err.message;
broadcastState();
// TLS Fallback: wenn wss fehlschlaegt und Fallback erlaubt → ws versuchen
if (useTls && RVS_TLS_FALLBACK === "true") {
// TLS Fallback
if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
fallbackTriggered = true;
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
ws.removeAllListeners();
try { ws.close(); } catch (_) {}
try { ws.removeAllListeners(); ws.close(); } catch (_) {}
if (rvsWs === ws) rvsWs = null;
connectRVS(true);
}
});
}
function sendToRVS(text, isPipeline) {
if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) {
log("error", "rvs", "Nicht verbunden");
if (isPipeline) pipelineEnd(false, "RVS nicht verbunden");
return false;
}
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", () => {});
}
rvsWs.send(JSON.stringify({
function sendToRVS(text, isPipeline) {
// Ueber Gateway senden (zuverlaessig) UND an RVS fuer App-Sichtbarkeit
// Die Bridge empfaengt RVS-Nachrichten von der App zuverlaessig,
// aber die Diagnostic→RVS→Bridge Route hat Zombie-Probleme.
// Deshalb: Gateway fuer ARIA, RVS nur fuer App-Anzeige.
// 1. An Gateway senden (damit ARIA antwortet)
const gatewayOk = sendToGateway(text, isPipeline);
// 2. An RVS senden (damit die App die Nachricht sieht)
sendToRVS_raw({
type: "chat",
payload: { text, sender: "diagnostic" },
timestamp: Date.now(),
}));
log("info", "rvs", `Gesendet via RVS: "${text}"`);
if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`);
return true;
});
return gatewayOk;
}
// ── Claude Proxy Test ────────────────────────────────────
@@ -526,7 +595,7 @@ async function testProxy(prompt) {
const modelsRes = await fetch(healthUrl, {
headers: { "Authorization": "Bearer not-needed" },
signal: AbortSignal.timeout(10000),
signal: AbortSignal.timeout(30000),
});
if (!modelsRes.ok) {
@@ -553,7 +622,7 @@ async function testProxy(prompt) {
}
// Schritt 2: Chat Completion testen (kurzer Prompt)
const testPrompt = prompt || "Antworte mit genau einem Wort: Ping";
const testPrompt = prompt || "Antworte in einem Satz: Wer bist du und funktionierst du?";
log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`);
const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, {
@@ -567,7 +636,7 @@ async function testProxy(prompt) {
messages: [{ role: "user", content: testPrompt }],
max_tokens: 200,
}),
signal: AbortSignal.timeout(30000),
signal: AbortSignal.timeout(120000), // 2min — Cold Start braucht Zeit
});
if (!chatRes.ok) {
@@ -932,6 +1001,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 ────────────────
const htmlPath = path.join(__dirname, "index.html");
@@ -1018,6 +1127,29 @@ wss.on("connection", (ws) => {
if (ws._sshSock) ws._sshSock.write(msg.data);
} else if (msg.action === "live_ssh_close") {
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
} else if (msg.action === "get_voice_config") {
handleGetVoiceConfig(ws);
} else if (msg.action === "send_voice_config") {
// Stimmen-Config persistent speichern + an Bridge via RVS senden
const voiceConfig = {
defaultVoice: msg.defaultVoice || "ramona",
highlightVoice: msg.highlightVoice || "thorsten",
ttsEnabled: msg.ttsEnabled !== false,
};
try {
fs.mkdirSync("/shared/config", { recursive: true });
fs.writeFileSync("/shared/config/voice_config.json", JSON.stringify(voiceConfig, null, 2));
} catch {}
sendToRVS_raw({ type: "config", payload: voiceConfig, timestamp: Date.now() });
log("info", "server", `Voice-Config gespeichert+gesendet: default=${voiceConfig.defaultVoice}, highlight=${voiceConfig.highlightVoice}, tts=${voiceConfig.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") {
checkDesktopAvailable(ws);
} else if (msg.action === "load_chat_history") {
@@ -1144,6 +1276,123 @@ function startLiveSSH(clientWs) {
createReq.end(createBody);
}
// ── Voice-Config laden ────────────────────────────────
function handleGetVoiceConfig(clientWs) {
try {
const configPath = "/shared/config/voice_config.json";
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
clientWs.send(JSON.stringify({ type: "voice_config", ...config }));
} else {
clientWs.send(JSON.stringify({ type: "voice_config", defaultVoice: "ramona", highlightVoice: "thorsten", ttsEnabled: true }));
}
} catch (err) {
clientWs.send(JSON.stringify({ type: "voice_config", defaultVoice: "ramona", highlightVoice: "thorsten", ttsEnabled: true }));
}
}
// ── 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) {
// Pruefen ob VNC auf der VM laeuft (Port 5900/5901)
const checkSock = net.connect({ host: "host.docker.internal", port: 5901 }, () => {
+2 -1
View File
@@ -19,6 +19,7 @@ services:
volumes:
- ~/.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-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
environment:
- HOST=0.0.0.0
- SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash)
@@ -99,7 +100,7 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./aria-data/config/diag-state:/data # Persistenter State (aktive Session etc.)
- aria-shared:/shared:ro # Shared Volume (Uploads lesen fuer Vorschau)
- aria-shared:/shared # Shared Volume (Uploads + Config)
environment:
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
- PROXY_URL=http://proxy:3456
+23 -3
View File
@@ -1,6 +1,26 @@
bildupload ghet noch nicht.
# erledigt bildupload ghet noch nicht.
# ende
# erledigt
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
# ende
wenn man auf das ohr zum hören klickt stürzt ab
aria liest die nachrichten nicht vor
# erledigt aria liest die nachrichten nicht vor
#ende
# 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
+9 -7
View File
@@ -58,17 +58,19 @@ echo -e "${GREEN}[1/5] Versionsnummern auf $VERSION setzen...${NC}"
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" android/package.json
echo -e " ${GREEN}${NC} package.json → $VERSION"
# build.gradle: versionName + versionCode (aus Major.Minor.Patch berechnen)
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH=$(echo "$VERSION" | cut -d. -f3)
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
# build.gradle: versionName + versionCode (aus Version berechnen)
# Unterstuetzt 3-stellig (1.2.3) und 4-stellig (0.0.1.7)
IFS='.' read -ra VER_PARTS <<< "$VERSION"
V1=${VER_PARTS[0]:-0}; V2=${VER_PARTS[1]:-0}; V3=${VER_PARTS[2]:-0}; V4=${VER_PARTS[3]:-0}
VERSION_CODE=$((V1 * 1000000 + V2 * 10000 + V3 * 100 + V4))
# Mindestens 1 (Android erfordert versionCode >= 1)
[ "$VERSION_CODE" -lt 1 ] && VERSION_CODE=1
sed -i "s/versionName \"[^\"]*\"/versionName \"$VERSION\"/" android/android/app/build.gradle
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/android/app/build.gradle
echo -e " ${GREEN}${NC} build.gradle → versionName $VERSION, versionCode $VERSION_CODE"
# SettingsScreen: Anzeige-Version
sed -i "s/Version [0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]* [^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx
# SettingsScreen: Anzeige-Version (beliebiges Versionsformat)
sed -i "s/Version [0-9][0-9.]*[^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx
echo -e " ${GREEN}${NC} SettingsScreen → Version $VERSION"
echo ""
+1 -1
View File
@@ -9,7 +9,7 @@ const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
// Erlaubte Nachrichtentypen — alles andere wird verworfen
const ALLOWED_TYPES = new Set([
"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> }