Compare commits

..

9 Commits

Author SHA1 Message Date
duffyduck 9c43b875f4 release: bump version to 0.0.2.2 2026-03-29 19:04:31 +02:00
duffyduck 63560e290b two speed 2026-03-29 19:03:40 +02:00
duffyduck 1ab8a6a2fe addes speed config for voice 2026-03-29 18:50:09 +02:00
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
8 changed files with 255 additions and 73 deletions
+2 -2
View File
@@ -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 200 versionCode 202
versionName "0.0.2.0" versionName "0.0.2.2"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.0.2.0", "version": "0.0.2.2",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+59 -1
View File
@@ -74,6 +74,8 @@ const SettingsScreen: React.FC = () => {
const [ttsEnabled, setTtsEnabled] = useState(true); const [ttsEnabled, setTtsEnabled] = useState(true);
const [defaultVoice, setDefaultVoice] = useState('ramona'); const [defaultVoice, setDefaultVoice] = useState('ramona');
const [highlightVoice, setHighlightVoice] = useState('thorsten'); const [highlightVoice, setHighlightVoice] = useState('thorsten');
const [speedRamona, setSpeedRamona] = useState(1.0);
const [speedThorsten, setSpeedThorsten] = useState(1.0);
const [editingPath, setEditingPath] = useState(false); const [editingPath, setEditingPath] = useState(false);
const [tempPath, setTempPath] = useState(''); const [tempPath, setTempPath] = useState('');
@@ -103,6 +105,12 @@ const SettingsScreen: React.FC = () => {
AsyncStorage.getItem('aria_highlight_voice').then(saved => { AsyncStorage.getItem('aria_highlight_voice').then(saved => {
if (saved) setHighlightVoice(saved); if (saved) setHighlightVoice(saved);
}); });
AsyncStorage.getItem('aria_speed_ramona').then(saved => {
if (saved) setSpeedRamona(parseFloat(saved));
});
AsyncStorage.getItem('aria_speed_thorsten').then(saved => {
if (saved) setSpeedThorsten(parseFloat(saved));
});
}, []); }, []);
// Speichergroesse berechnen // Speichergroesse berechnen
@@ -521,6 +529,56 @@ const SettingsScreen: React.FC = () => {
</View> </View>
</View> </View>
{/* Sprechgeschwindigkeit Ramona */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Ramona Speed: {speedRamona.toFixed(1)}x</Text>
<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={() => {
setSpeedRamona(speed);
AsyncStorage.setItem('aria_speed_ramona', String(speed));
rvs.send('config' as any, { speedRamona: speed });
}}
style={{
paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6,
backgroundColor: speedRamona === speed ? '#0096FF' : '#1E1E2E',
}}
>
<Text style={{color: speedRamona === speed ? '#fff' : '#8888AA', fontSize: 12, fontWeight: '600'}}>
{speed}x
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Sprechgeschwindigkeit Thorsten */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Thorsten Speed: {speedThorsten.toFixed(1)}x</Text>
<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={() => {
setSpeedThorsten(speed);
AsyncStorage.setItem('aria_speed_thorsten', String(speed));
rvs.send('config' as any, { speedThorsten: speed });
}}
style={{
paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6,
backgroundColor: speedThorsten === speed ? '#0096FF' : '#1E1E2E',
}}
>
<Text style={{color: speedThorsten === speed ? '#fff' : '#8888AA', fontSize: 12, fontWeight: '600'}}>
{speed}x
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Highlight-Trigger Info */} {/* Highlight-Trigger Info */}
<View style={{marginTop: 16, padding: 10, backgroundColor: '#1E1E2E', borderRadius: 8}}> <View style={{marginTop: 16, padding: 10, backgroundColor: '#1E1E2E', borderRadius: 8}}>
<Text style={styles.toggleLabel}>{'\u26A1'} Highlight-Trigger</Text> <Text style={styles.toggleLabel}>{'\u26A1'} Highlight-Trigger</Text>
@@ -690,7 +748,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.2.0 </Text> <Text style={styles.aboutVersion}>Version 0.0.2.2 </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.
+103 -12
View File
@@ -38,6 +38,7 @@ import websockets
from faster_whisper import WhisperModel from faster_whisper import WhisperModel
from openwakeword.model import Model as WakeWordModel from openwakeword.model import Model as WakeWordModel
from piper import PiperVoice from piper import PiperVoice
from piper.config import SynthesisConfig
from modes import Mode, detect_mode_switch, should_speak from modes import Mode, detect_mode_switch, should_speak
@@ -131,6 +132,7 @@ class VoiceEngine:
self.voices: dict[str, PiperVoice] = {} self.voices: dict[str, PiperVoice] = {}
self.default_voice = "ramona" self.default_voice = "ramona"
self.highlight_voice = "thorsten" self.highlight_voice = "thorsten"
self.speech_speed = {"ramona": 1.0, "thorsten": 1.0}
def initialize(self) -> None: def initialize(self) -> None:
"""Laedt die Piper-Stimmen aus dem Voices-Verzeichnis.""" """Laedt die Piper-Stimmen aus dem Voices-Verzeichnis."""
@@ -199,20 +201,50 @@ class VoiceEngine:
return None return None
try: 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
speed = self.speech_speed.get(voice_name, 1.0)
syn_config = SynthesisConfig(length_scale=1.0 / max(0.3, speed))
with wave.open(tmp_path, "wb") as wav_file:
voice.synthesize_wav(sentence, wav_file, syn_config=syn_config)
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: 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: audio_data = Path(final_path).read_bytes()
voice.synthesize_wav(text, wav_file) Path(final_path).unlink(missing_ok=True)
audio_data = Path(tmp_path).read_bytes()
Path(tmp_path).unlink(missing_ok=True)
logger.info( logger.info(
"TTS: %d bytes erzeugt mit %s'%s'", "TTS: %d bytes erzeugt mit %s (%d Saetze)'%s'",
len(audio_data), len(audio_data),
voice_name, voice_name,
len(sentences),
text[:60], text[:60],
) )
return audio_data return audio_data
@@ -457,6 +489,23 @@ class ARIABridge:
# Komponenten # Komponenten
self.voice_engine = VoiceEngine(VOICES_DIR) 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.voice_engine.speech_speed = {
"ramona": vc.get("speedRamona", 1.0),
"thorsten": vc.get("speedThorsten", 1.0),
}
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( self.stt_engine = STTEngine(
model_size=self.config.get("WHISPER_MODEL", WHISPER_MODEL), model_size=self.config.get("WHISPER_MODEL", WHISPER_MODEL),
language=self.config.get("WHISPER_LANGUAGE", WHISPER_LANGUAGE), language=self.config.get("WHISPER_LANGUAGE", WHISPER_LANGUAGE),
@@ -484,7 +533,9 @@ class ARIABridge:
# Audio-Hardware pruefen (fuer lokales Mikro/Lautsprecher) # Audio-Hardware pruefen (fuer lokales Mikro/Lautsprecher)
self.audio_available = False self.audio_available = False
try: try:
sd.query_devices() devices = sd.query_devices()
# Pruefen ob ein Output-Device existiert
sd.query_devices(kind='output')
self.audio_available = True self.audio_available = True
logger.info("Audio-Geraet gefunden — Wake-Word und lokale TTS aktiv") logger.info("Audio-Geraet gefunden — Wake-Word und lokale TTS aktiv")
self.stt_engine.initialize() self.stt_engine.initialize()
@@ -913,10 +964,22 @@ class ARIABridge:
retry_delay = min(retry_delay * 2, 30) retry_delay = min(retry_delay * 2, 30)
async def _rvs_heartbeat(self) -> None: 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: while True:
await asyncio.sleep(25) await asyncio.sleep(15)
if self.ws_rvs: 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: try:
await self.ws_rvs.send(json.dumps({ await self.ws_rvs.send(json.dumps({
"type": "heartbeat", "type": "heartbeat",
@@ -951,20 +1014,48 @@ class ARIABridge:
return return
elif msg_type == "config": elif msg_type == "config":
# Konfiguration von App/Diagnostic empfangen # Konfiguration von App/Diagnostic empfangen + persistent speichern
changed = False
if "defaultVoice" in payload: if "defaultVoice" in payload:
new_voice = payload["defaultVoice"] new_voice = payload["defaultVoice"]
if new_voice in self.voice_engine.voices: if new_voice in self.voice_engine.voices:
self.voice_engine.default_voice = new_voice self.voice_engine.default_voice = new_voice
logger.info("[rvs] Standard-Stimme gewechselt: %s", new_voice) logger.info("[rvs] Standard-Stimme gewechselt: %s", new_voice)
changed = True
if "highlightVoice" in payload: if "highlightVoice" in payload:
new_voice = payload["highlightVoice"] new_voice = payload["highlightVoice"]
if new_voice in self.voice_engine.voices: if new_voice in self.voice_engine.voices:
self.voice_engine.highlight_voice = new_voice self.voice_engine.highlight_voice = new_voice
logger.info("[rvs] Highlight-Stimme gewechselt: %s", new_voice) logger.info("[rvs] Highlight-Stimme gewechselt: %s", new_voice)
changed = True
if "ttsEnabled" in payload: if "ttsEnabled" in payload:
self.tts_enabled = bool(payload["ttsEnabled"]) self.tts_enabled = bool(payload["ttsEnabled"])
logger.info("[rvs] TTS %s", "aktiviert" if self.tts_enabled else "deaktiviert") logger.info("[rvs] TTS %s", "aktiviert" if self.tts_enabled else "deaktiviert")
changed = True
if "speedRamona" in payload:
self.voice_engine.speech_speed["ramona"] = max(0.3, min(2.0, float(payload["speedRamona"])))
logger.info("[rvs] Speed Ramona: %.1f", self.voice_engine.speech_speed["ramona"])
changed = True
if "speedThorsten" in payload:
self.voice_engine.speech_speed["thorsten"] = max(0.3, min(2.0, float(payload["speedThorsten"])))
logger.info("[rvs] Speed Thorsten: %.1f", self.voice_engine.speech_speed["thorsten"])
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),
"speedRamona": self.voice_engine.speech_speed.get("ramona", 1.0),
"speedThorsten": self.voice_engine.speech_speed.get("thorsten", 1.0),
}
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 return
text = payload.get("text", "") text = payload.get("text", "")
if text: if text:
+44 -4
View File
@@ -414,10 +414,32 @@
<option value="ramona">Ramona (weiblich)</option> <option value="ramona">Ramona (weiblich)</option>
</select> </select>
</div> </div>
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<label style="color:#8888AA;font-size:12px;">TTS aktiv:</label> <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> <label class="toggle"><input type="checkbox" id="diag-tts-enabled" checked onchange="sendVoiceConfig()"><span class="slider"></span></label>
</div> </div>
<div style="margin-bottom:4px;">
<label style="color:#8888AA;font-size:12px;">Ramona Speed: <span id="speed-ramona-label">1.0x</span></label>
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<span style="color:#555570;font-size:11px;">0.5x</span>
<input type="range" id="diag-speed-ramona" min="0.5" max="2.0" step="0.1" value="1.0"
oninput="document.getElementById('speed-ramona-label').textContent=this.value+'x'"
onchange="sendVoiceConfig()"
style="flex:1;accent-color:#0096FF;">
<span style="color:#555570;font-size:11px;">2.0x</span>
</div>
<div style="margin-bottom:4px;">
<label style="color:#8888AA;font-size:12px;">Thorsten Speed: <span id="speed-thorsten-label">1.0x</span></label>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span style="color:#555570;font-size:11px;">0.5x</span>
<input type="range" id="diag-speed-thorsten" min="0.5" max="2.0" step="0.1" value="1.0"
oninput="document.getElementById('speed-thorsten-label').textContent=this.value+'x'"
onchange="sendVoiceConfig()"
style="flex:1;accent-color:#0096FF;">
<span style="color:#555570;font-size:11px;">2.0x</span>
</div>
</div> </div>
</div> </div>
@@ -642,6 +664,19 @@
return; 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;
const sr = msg.speedRamona || 1.0;
const st = msg.speedThorsten || 1.0;
document.getElementById('diag-speed-ramona').value = sr;
document.getElementById('speed-ramona-label').textContent = sr + 'x';
document.getElementById('diag-speed-thorsten').value = st;
document.getElementById('speed-thorsten-label').textContent = st + 'x';
return;
}
if (msg.type === 'trigger_list') { if (msg.type === 'trigger_list') {
const textarea = document.getElementById('highlight-triggers'); const textarea = document.getElementById('highlight-triggers');
textarea.value = (msg.triggers || []).join('\n'); textarea.value = (msg.triggers || []).join('\n');
@@ -1136,7 +1171,9 @@
const defaultVoice = document.getElementById('diag-default-voice').value; const defaultVoice = document.getElementById('diag-default-voice').value;
const highlightVoice = document.getElementById('diag-highlight-voice').value; const highlightVoice = document.getElementById('diag-highlight-voice').value;
const ttsEnabled = document.getElementById('diag-tts-enabled').checked; const ttsEnabled = document.getElementById('diag-tts-enabled').checked;
send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled }); const speedRamona = parseFloat(document.getElementById('diag-speed-ramona').value);
const speedThorsten = parseFloat(document.getElementById('diag-speed-thorsten').value);
send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled, speedRamona, speedThorsten });
} }
// ── Highlight-Trigger ──────────────────────── // ── Highlight-Trigger ────────────────────────
@@ -1587,8 +1624,11 @@
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 // Einstellungen: Config + Trigger laden
if (tab === 'settings') loadHighlightTriggers(); if (tab === 'settings') {
loadHighlightTriggers();
send({ action: 'get_voice_config' });
}
} }
// ── Einstellungen: Tool-Berechtigungen ────────────────── // ── Einstellungen: Tool-Berechtigungen ──────────────────
+42 -49
View File
@@ -562,54 +562,22 @@ function sendToRVS_raw(msgObj) {
} }
function sendToRVS(text, isPipeline) { function sendToRVS(text, isPipeline) {
if (!RVS_HOST || !RVS_TOKEN) { // Ueber Gateway senden (zuverlaessig) UND an RVS fuer App-Sichtbarkeit
log("error", "rvs", "Nicht konfiguriert"); // Die Bridge empfaengt RVS-Nachrichten von der App zuverlaessig,
if (isPipeline) pipelineEnd(false, "RVS nicht konfiguriert"); // aber die Diagnostic→RVS→Bridge Route hat Zombie-Probleme.
return false; // Deshalb: Gateway fuer ARIA, RVS nur fuer App-Anzeige.
}
// Frische WebSocket-Verbindung fuer jede Nachricht (Zombie-Schutz) // 1. An Gateway senden (damit ARIA antwortet)
const proto = RVS_TLS === "true" ? "wss" : "ws"; const gatewayOk = sendToGateway(text, isPipeline);
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
const msg = JSON.stringify({ // 2. An RVS senden (damit die App die Nachricht sieht)
sendToRVS_raw({
type: "chat", type: "chat",
payload: { text, sender: "diagnostic" }, payload: { text, sender: "diagnostic" },
timestamp: Date.now(), timestamp: Date.now(),
}); });
log("info", "rvs", `Sende via frische Verbindung: ${url.split('?')[0]}`); return gatewayOk;
const freshWs = new WebSocket(url);
freshWs.on("open", () => {
freshWs.send(msg);
log("info", "rvs", `Gesendet via RVS: "${text}"`);
// Verbindung offen lassen fuer Antwort-Empfang, nach 5min schliessen
setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 300000);
});
freshWs.on("message", (raw) => {
try {
const resp = JSON.parse(raw.toString());
if (resp.type === "chat" && resp.payload) {
const sender = resp.payload.sender || "?";
// Eigene Nachrichten und STT ignorieren (werden von persistenter Verbindung gehandelt)
if (sender === "diagnostic" || sender === "stt") return;
log("info", "rvs", `Chat von ${sender}: "${(resp.payload.text || "").slice(0, 100)}"`);
if (pipelineActive && sender !== "diagnostic") {
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(resp.payload.text || "").slice(0, 120)}"`);
}
broadcast({ type: "rvs_chat", msg: resp });
} else if (resp.type !== "heartbeat") {
log("debug", "rvs", `Nachricht: ${JSON.stringify(resp).slice(0, 150)}`);
}
} catch {}
});
freshWs.on("error", (err) => {
log("error", "rvs", `Sende-Fehler: ${err.message}`);
if (isPipeline) pipelineEnd(false, `RVS Fehler: ${err.message}`);
});
if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`);
return true;
} }
// ── Claude Proxy Test ──────────────────────────────────── // ── Claude Proxy Test ────────────────────────────────────
@@ -1159,14 +1127,23 @@ 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 === "get_voice_config") {
handleGetVoiceConfig(ws);
} else if (msg.action === "send_voice_config") { } else if (msg.action === "send_voice_config") {
// Stimmen-Config an Bridge via RVS senden // Stimmen-Config persistent speichern + an Bridge via RVS senden
sendToRVS_raw({ type: "config", payload: { const voiceConfig = {
defaultVoice: msg.defaultVoice, defaultVoice: msg.defaultVoice || "ramona",
highlightVoice: msg.highlightVoice, highlightVoice: msg.highlightVoice || "thorsten",
ttsEnabled: msg.ttsEnabled, ttsEnabled: msg.ttsEnabled !== false,
}, timestamp: Date.now() }); speedRamona: msg.speedRamona || 1.0,
log("info", "server", `Voice-Config gesendet: default=${msg.defaultVoice}, highlight=${msg.highlightVoice}, tts=${msg.ttsEnabled}`); speedThorsten: msg.speedThorsten || 1.0,
};
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") { } else if (msg.action === "get_triggers") {
handleGetTriggers(ws); handleGetTriggers(ws);
} else if (msg.action === "save_triggers") { } else if (msg.action === "save_triggers") {
@@ -1301,6 +1278,22 @@ function startLiveSSH(clientWs) {
createReq.end(createBody); 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 ───────────────────────────────── // ── Highlight-Trigger ─────────────────────────────────
const TRIGGERS_FILE = "/shared/config/highlight_triggers.json"; const TRIGGERS_FILE = "/shared/config/highlight_triggers.json";
+1 -1
View File
@@ -100,7 +100,7 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- ./aria-data/config/diag-state:/data # Persistenter State (aktive Session etc.) - ./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: environment:
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-} - ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
- PROXY_URL=http://proxy:3456 - PROXY_URL=http://proxy:3456
+3 -3
View File
@@ -1,5 +1,5 @@
# erledigt bildupload ghet noch nicht. # erledigt bildupload ghet noch nicht.
# ende
# erledigt # 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
# ende # ende
@@ -11,8 +11,8 @@ autoload geht nicht
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 # erledigt aria liest die nachrichten nicht vor
#ende
# erledigt autoscroll geht doch noch nicht zur letzten nachricht # erledigt autoscroll geht doch noch nicht zur letzten nachricht
unserer memory brain unserer memory brain