Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c43b875f4 | |||
| 63560e290b | |||
| 1ab8a6a2fe | |||
| a2c0196e05 | |||
| 680f7a64e2 | |||
| 4893616a5a | |||
| 04e8c0245d | |||
| 10cefaf1cd | |||
| adbb1fe80a |
@@ -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,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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user