TLS Fallback (Bridge → RVS)
Audio-Rendering fuer App (Piper TTS via RVS) Chat-Persistenz (AsyncStorage, 500 Nachrichten)
This commit is contained in:
parent
b5f1bf6d2c
commit
e951fc712f
|
|
@ -9,6 +9,9 @@ ARIA_AUTH_TOKEN=change-me-to-a-long-random-string
|
|||
RVS_HOST=rvs.example.de
|
||||
RVS_PORT=443
|
||||
RVS_TLS=true
|
||||
# Bei TLS-Fehler automatisch auf ws:// (ohne TLS) fallback?
|
||||
# true = Fallback erlaubt, false = nur mit TLS verbinden
|
||||
RVS_TLS_FALLBACK=true
|
||||
RVS_TOKEN=
|
||||
|
||||
# Gitea (for release.sh — Kennwort wird interaktiv abgefragt)
|
||||
|
|
|
|||
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -57,6 +57,23 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
|
|||
- `WakeWordDetector` umgebaut — sucht Custom-Modell `/voices/wake_aria.onnx`, Fallback auf eingebautes `hey_jarvis`
|
||||
- Alter Code crashte: `wakeword_models=["aria"]` erwartet Dateipfad, kein Keyword
|
||||
|
||||
**TLS Fallback (Bridge → RVS)**
|
||||
- Bridge versucht zuerst `wss://` (TLS), bei `ssl.SSLError` automatisch Fallback auf `ws://`
|
||||
- Konfigurierbar über `RVS_TLS_FALLBACK=true` in `.env`
|
||||
- Loggt deutlich wenn TLS gewollt aber nicht verfügbar ist
|
||||
|
||||
**Audio-Rendering für App (Piper TTS via RVS)**
|
||||
- Bridge rendert Piper TTS → WAV → base64, sendet Text UND Audio gleichzeitig über RVS
|
||||
- App spielt Audio ab und zeigt Text parallel — Modus entscheidet ob Sprache oder nur Text
|
||||
- Voice Engine initialisiert IMMER (auch ohne Soundkarte in der VM)
|
||||
- STT/Wake-Word nur wenn Audio-Hardware vorhanden — graceful degradation
|
||||
- Neue Dependency: `react-native-fs` (base64 → temp WAV → Sound abspielen)
|
||||
|
||||
**Chat-Persistenz (Android App)**
|
||||
- Chat-Verlauf wird in AsyncStorage gespeichert (letzte 500 Nachrichten)
|
||||
- Beim App-Start automatisch geladen — Konversation bleibt erhalten
|
||||
- Linearer 1:1 Chat, keine Threads
|
||||
|
||||
**Neues Script: `get-voices.sh`**
|
||||
- Lädt Piper Stimmen (Ramona + Thorsten) von HuggingFace herunter
|
||||
- Neuer Installationsschritt in README
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ services:
|
|||
- RVS_HOST=${RVS_HOST:-}
|
||||
- RVS_PORT=${RVS_PORT:-443}
|
||||
- RVS_TLS=${RVS_TLS:-true}
|
||||
- RVS_TLS_FALLBACK=${RVS_TLS_FALLBACK:-true}
|
||||
- RVS_TOKEN=${RVS_TOKEN:-}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -27,6 +27,8 @@ import com.imagepicker.ImagePickerPackage;
|
|||
import com.zoontek.rnpermissions.RNPermissionsPackage;
|
||||
// react-native-camera-kit
|
||||
import com.rncamerakit.RNCameraKitPackage;
|
||||
// @react-native-async-storage/async-storage
|
||||
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
|
||||
|
||||
public class PackageList {
|
||||
private Application application;
|
||||
|
|
@ -79,7 +81,8 @@ public class PackageList {
|
|||
new GeolocationPackage(),
|
||||
new ImagePickerPackage(),
|
||||
new RNPermissionsPackage(),
|
||||
new RNCameraKitPackage()
|
||||
new RNCameraKitPackage(),
|
||||
new AsyncStoragePackage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -301,6 +301,19 @@
|
|||
symbolFile="R.txt"
|
||||
externalAnnotations="annotations.zip"
|
||||
proguardRules="proguard.txt"/>
|
||||
<library
|
||||
name="/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/android@@:react-native-async-storage_async-storage::release"
|
||||
jars="/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/.transforms/2be425a3c2bdc5ca7e7f9aab0d2b9fd2/transformed/out/jars/classes.jar:/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/.transforms/2be425a3c2bdc5ca7e7f9aab0d2b9fd2/transformed/out/jars/libs/R.jar"
|
||||
resolved="AriaCockpit:react-native-async-storage_async-storage:unspecified"
|
||||
folder="/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/.transforms/2be425a3c2bdc5ca7e7f9aab0d2b9fd2/transformed/out"
|
||||
manifest="AndroidManifest.xml"
|
||||
resFolder="res"
|
||||
assetsFolder="assets"
|
||||
lintJar="lint.jar"
|
||||
publicResources="public.txt"
|
||||
symbolFile="R.txt"
|
||||
externalAnnotations="annotations.zip"
|
||||
proguardRules="proguard.txt"/>
|
||||
<library
|
||||
name="org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21@jar"
|
||||
jars="/home/duffy/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.8.21/7473b8cd3c0ef9932345baf569bc398e8a717046/kotlin-stdlib-jdk7-1.8.21.jar"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -301,6 +301,19 @@
|
|||
symbolFile="R.txt"
|
||||
externalAnnotations="annotations.zip"
|
||||
proguardRules="proguard.txt"/>
|
||||
<library
|
||||
name="/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/android@@:react-native-async-storage_async-storage::release"
|
||||
jars="/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/.transforms/2be425a3c2bdc5ca7e7f9aab0d2b9fd2/transformed/out/jars/classes.jar:/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/.transforms/2be425a3c2bdc5ca7e7f9aab0d2b9fd2/transformed/out/jars/libs/R.jar"
|
||||
resolved="AriaCockpit:react-native-async-storage_async-storage:unspecified"
|
||||
folder="/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/.transforms/2be425a3c2bdc5ca7e7f9aab0d2b9fd2/transformed/out"
|
||||
manifest="AndroidManifest.xml"
|
||||
resFolder="res"
|
||||
assetsFolder="assets"
|
||||
lintJar="lint.jar"
|
||||
publicResources="public.txt"
|
||||
symbolFile="R.txt"
|
||||
externalAnnotations="annotations.zip"
|
||||
proguardRules="proguard.txt"/>
|
||||
<library
|
||||
name="org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21@jar"
|
||||
jars="/home/duffy/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.8.21/7473b8cd3c0ef9932345baf569bc398e8a717046/kotlin-stdlib-jdk7-1.8.21.jar"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
|||
#Mon Mar 09 00:30:03 CET 2026
|
||||
#Tue Mar 10 18:02:18 CET 2026
|
||||
base.2=/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/android/app/build/intermediates/dex/release/mergeDexRelease/classes2.dex
|
||||
path.2=classes2.dex
|
||||
base.1=/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/android/app/build/intermediates/global_synthetics_dex/release/classes.dex
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -12,6 +12,7 @@ MERGED from [:react-native-community_geolocation] /home/duffy/Dokumente/programm
|
|||
MERGED from [:react-native-image-picker] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/react-native-image-picker/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:2:1-19:12
|
||||
MERGED from [:react-native-permissions] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/react-native-permissions/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:2:1-7:12
|
||||
MERGED from [:react-native-camera-kit] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/react-native-camera-kit/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:2:1-11:12
|
||||
MERGED from [:react-native-async-storage_async-storage] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:2:1-7:12
|
||||
MERGED from [com.facebook.react:react-android:0.73.4] /home/duffy/.gradle/caches/transforms-3/158b871a21f85526704b0e97f3449ad0/transformed/jetified-react-android-0.73.4-release/AndroidManifest.xml:2:1-12:12
|
||||
MERGED from [com.facebook.fresco:fresco:3.1.3] /home/duffy/.gradle/caches/transforms-3/73ba53becf6a818dacbfe76ccb7dfd5a/transformed/jetified-fresco-3.1.3/AndroidManifest.xml:2:1-7:12
|
||||
MERGED from [com.facebook.fresco:imagepipeline-okhttp3:3.1.3] /home/duffy/.gradle/caches/transforms-3/78816c54b596e77b24ddd4d407d9d3f7/transformed/jetified-imagepipeline-okhttp3-3.1.3/AndroidManifest.xml:2:1-7:12
|
||||
|
|
@ -220,6 +221,8 @@ MERGED from [:react-native-permissions] /home/duffy/Dokumente/programmierung/ARI
|
|||
MERGED from [:react-native-permissions] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/react-native-permissions/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:5:5-44
|
||||
MERGED from [:react-native-camera-kit] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/react-native-camera-kit/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:5:5-44
|
||||
MERGED from [:react-native-camera-kit] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/react-native-camera-kit/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:5:5-44
|
||||
MERGED from [:react-native-async-storage_async-storage] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:5:5-44
|
||||
MERGED from [:react-native-async-storage_async-storage] /home/duffy/Dokumente/programmierung/ARIA-AGENT/android/node_modules/@react-native-async-storage/async-storage/android/build/intermediates/merged_manifest/release/AndroidManifest.xml:5:5-44
|
||||
MERGED from [com.facebook.react:react-android:0.73.4] /home/duffy/.gradle/caches/transforms-3/158b871a21f85526704b0e97f3449ad0/transformed/jetified-react-android-0.73.4-release/AndroidManifest.xml:10:5-44
|
||||
MERGED from [com.facebook.react:react-android:0.73.4] /home/duffy/.gradle/caches/transforms-3/158b871a21f85526704b0e97f3449ad0/transformed/jetified-react-android-0.73.4-release/AndroidManifest.xml:10:5-44
|
||||
MERGED from [com.facebook.fresco:fresco:3.1.3] /home/duffy/.gradle/caches/transforms-3/73ba53becf6a818dacbfe76ccb7dfd5a/transformed/jetified-fresco-3.1.3/AndroidManifest.xml:5:5-44
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -8,6 +8,7 @@
|
|||
"name": "aria-cockpit",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.21.0",
|
||||
"@react-native-community/geolocation": "^3.2.1",
|
||||
"@react-navigation/bottom-tabs": "^6.5.11",
|
||||
"@react-navigation/native": "^6.1.9",
|
||||
|
|
@ -2848,6 +2849,18 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-async-storage/async-storage": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz",
|
||||
"integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"merge-options": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "^0.0.0-0 || >=0.60 <1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/cli": {
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.2.tgz",
|
||||
|
|
@ -7682,6 +7695,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||
|
|
@ -9137,6 +9159,18 @@
|
|||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-options": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-plain-obj": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
"react-native-image-picker": "^7.1.0",
|
||||
"react-native-permissions": "^4.1.4",
|
||||
"react-native-camera-kit": "^13.0.0",
|
||||
"@react-native-async-storage/async-storage": "^1.21.0"
|
||||
"@react-native-async-storage/async-storage": "^1.21.0",
|
||||
"react-native-fs": "^2.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
StyleSheet,
|
||||
Modal,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||
import audioService from '../services/audio';
|
||||
import VoiceButton from '../components/VoiceButton';
|
||||
|
|
@ -41,6 +42,11 @@ interface ChatMessage {
|
|||
attachments?: Attachment[];
|
||||
}
|
||||
|
||||
// --- Konstanten ---
|
||||
|
||||
const CHAT_STORAGE_KEY = 'aria_chat_messages';
|
||||
const MAX_STORED_MESSAGES = 500;
|
||||
|
||||
// --- Komponente ---
|
||||
|
||||
const ChatScreen: React.FC = () => {
|
||||
|
|
@ -60,10 +66,26 @@ const ChatScreen: React.FC = () => {
|
|||
return `msg_${Date.now()}_${messageIdCounter.current}`;
|
||||
};
|
||||
|
||||
// GPS-Einstellung aus Settings laden (vereinfacht)
|
||||
// Chat-Verlauf aus AsyncStorage laden
|
||||
useEffect(() => {
|
||||
// In Produktion: AsyncStorage oder Context verwenden
|
||||
// Hier Platzhalter - GPS Toggle kommt aus SettingsScreen
|
||||
const loadMessages = async () => {
|
||||
try {
|
||||
const stored = await AsyncStorage.getItem(CHAT_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed: ChatMessage[] = JSON.parse(stored);
|
||||
setMessages(parsed);
|
||||
// ID-Counter auf hoechsten Wert setzen um Kollisionen zu vermeiden
|
||||
const maxId = parsed.reduce((max, msg) => {
|
||||
const num = parseInt(msg.id.split('_').pop() || '0', 10);
|
||||
return num > max ? num : max;
|
||||
}, 0);
|
||||
messageIdCounter.current = maxId;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Chat] Fehler beim Laden des Verlaufs:', err);
|
||||
}
|
||||
};
|
||||
loadMessages();
|
||||
}, []);
|
||||
|
||||
// RVS-Nachrichten abonnieren
|
||||
|
|
@ -99,6 +121,15 @@ const ChatScreen: React.FC = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Chat-Verlauf in AsyncStorage speichern (letzte N Nachrichten)
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) return;
|
||||
const toStore = messages.slice(-MAX_STORED_MESSAGES);
|
||||
AsyncStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(toStore)).catch(err =>
|
||||
console.error('[Chat] Fehler beim Speichern:', err),
|
||||
);
|
||||
}, [messages]);
|
||||
|
||||
// Auto-Scroll bei neuen Nachrichten
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { Platform, PermissionsAndroid } from 'react-native';
|
||||
import Sound from 'react-native-sound';
|
||||
import RNFS from 'react-native-fs';
|
||||
|
||||
// --- Typen ---
|
||||
|
||||
|
|
@ -127,18 +128,20 @@ class AudioService {
|
|||
|
||||
/** Base64-kodiertes Audio abspielen (z.B. TTS-Antwort von ARIA) */
|
||||
async playAudio(base64Data: string): Promise<void> {
|
||||
if (!base64Data) return;
|
||||
|
||||
// Laufende Wiedergabe stoppen
|
||||
this.stopPlayback();
|
||||
|
||||
try {
|
||||
// Base64-Daten in temporaere Datei schreiben und abspielen
|
||||
// In Produktion: react-native-fs + Sound kombinieren
|
||||
const tmpPath = `${Platform.OS === 'android' ? '/data/user/0/' : ''}aria_tts_temp.wav`;
|
||||
// Base64 → temporaere WAV-Datei → Sound abspielen
|
||||
const tmpPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`;
|
||||
await RNFS.writeFile(tmpPath, base64Data, 'base64');
|
||||
|
||||
// Platzhalter: Sound aus Datei laden
|
||||
this.currentSound = new Sound(tmpPath, '', (error) => {
|
||||
if (error) {
|
||||
console.error('[Audio] Fehler beim Laden:', error);
|
||||
RNFS.unlink(tmpPath).catch(() => {});
|
||||
return;
|
||||
}
|
||||
this.currentSound?.play((success) => {
|
||||
|
|
@ -149,6 +152,8 @@ class AudioService {
|
|||
}
|
||||
this.currentSound?.release();
|
||||
this.currentSound = null;
|
||||
// Temp-Datei aufraeumen
|
||||
RNFS.unlink(tmpPath).catch(() => {});
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ Stimmen:
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import ssl
|
||||
import sys
|
||||
import tempfile
|
||||
import wave
|
||||
|
|
@ -53,6 +55,7 @@ CORE_WS_URL = os.getenv("ARIA_CORE_WS", "ws://aria:8080")
|
|||
RVS_HOST = os.getenv("RVS_HOST", "") # z.B. rvs.hackersoft.de
|
||||
RVS_PORT = os.getenv("RVS_PORT", "443") # Port des RVS
|
||||
RVS_TLS = os.getenv("RVS_TLS", "true") # true = wss://, false = ws://
|
||||
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true") # Bei TLS-Fehler ws:// versuchen
|
||||
RVS_TOKEN = os.getenv("RVS_TOKEN", "") # Pairing-Token (gleich wie in der App)
|
||||
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "small")
|
||||
WHISPER_LANGUAGE = os.getenv("WHISPER_LANGUAGE", "de")
|
||||
|
|
@ -311,30 +314,34 @@ class WakeWordDetector:
|
|||
self.wake_word_key: str = ""
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""Laedt das Wake-Word-Modell."""
|
||||
"""Laedt das Wake-Word-Modell.
|
||||
|
||||
Hinweis: WakeWordModel() wird OHNE Argumente aufgerufen.
|
||||
Aeltere openwakeword-Versionen leiten unbekannte kwargs
|
||||
an AudioFeatures weiter, was zum Crash fuehrt.
|
||||
"""
|
||||
logger.info("Lade Wake-Word-Modell...")
|
||||
|
||||
custom_path = Path(self.CUSTOM_MODEL_PATH)
|
||||
if custom_path.exists():
|
||||
# Custom "aria" Modell vorhanden
|
||||
self.model = WakeWordModel(
|
||||
wakeword_models=[str(custom_path)],
|
||||
)
|
||||
self.wake_word_key = custom_path.stem
|
||||
logger.info("Custom Wake-Word-Modell geladen: %s", custom_path)
|
||||
else:
|
||||
# Fallback auf eingebautes Modell
|
||||
self.model = WakeWordModel()
|
||||
# Alle eingebauten Modelle laden (ohne kwargs — Kompatibilitaet!)
|
||||
self.model = WakeWordModel()
|
||||
|
||||
# Verfuegbare Modelle ermitteln
|
||||
available = list(self.model.models.keys()) if hasattr(self.model, 'models') else []
|
||||
logger.info("Verfuegbare Wake-Words: %s", ", ".join(available) if available else "(keine)")
|
||||
|
||||
# Bestes Modell auswaehlen
|
||||
if self.FALLBACK_MODEL in available:
|
||||
self.wake_word_key = self.FALLBACK_MODEL
|
||||
logger.warning(
|
||||
"Kein Custom-Modell (%s) — nutze Fallback '%s'",
|
||||
self.CUSTOM_MODEL_PATH,
|
||||
self.FALLBACK_MODEL,
|
||||
)
|
||||
logger.info(
|
||||
"Tipp: Custom Wake-Word trainieren → "
|
||||
"https://github.com/dscripka/openWakeWord#training-new-models"
|
||||
)
|
||||
elif available:
|
||||
self.wake_word_key = available[0]
|
||||
else:
|
||||
self.wake_word_key = self.FALLBACK_MODEL
|
||||
|
||||
logger.info("Wake-Word aktiv: '%s'", self.wake_word_key)
|
||||
logger.info(
|
||||
"Tipp: Custom 'aria' Wake-Word trainieren → "
|
||||
"https://github.com/dscripka/openWakeWord#training-new-models"
|
||||
)
|
||||
|
||||
def detect(self, audio_chunk: np.ndarray) -> bool:
|
||||
"""Prueft ob das Wake-Word im Audio-Chunk enthalten ist.
|
||||
|
|
@ -351,10 +358,10 @@ class WakeWordDetector:
|
|||
prediction = self.model.predict(audio_chunk)
|
||||
|
||||
# openwakeword gibt Scores pro Modell zurueck
|
||||
score = prediction.get(self.wake_word_key, 0)
|
||||
if score > self.THRESHOLD:
|
||||
logger.info("Wake-Word erkannt! (Score: %.2f)", score)
|
||||
return True
|
||||
for key, score in prediction.items():
|
||||
if score > self.THRESHOLD:
|
||||
logger.info("Wake-Word '%s' erkannt! (Score: %.2f)", key, score)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
|
@ -402,13 +409,20 @@ class ARIABridge:
|
|||
rvs_host = self.config.get("RVS_HOST", RVS_HOST)
|
||||
rvs_port = self.config.get("RVS_PORT", RVS_PORT)
|
||||
rvs_tls = self.config.get("RVS_TLS", RVS_TLS).lower() == "true"
|
||||
self.rvs_tls_fallback = self.config.get("RVS_TLS_FALLBACK", RVS_TLS_FALLBACK).lower() == "true"
|
||||
self.rvs_token = self.config.get("RVS_TOKEN", RVS_TOKEN)
|
||||
# URL zusammenbauen
|
||||
# URLs zusammenbauen (primaer + fallback)
|
||||
if rvs_host:
|
||||
proto = "wss" if rvs_tls else "ws"
|
||||
self.rvs_url = f"{proto}://{rvs_host}:{rvs_port}"
|
||||
# Fallback-URL (ohne TLS) nur wenn TLS aktiv und Fallback erlaubt
|
||||
if rvs_tls and self.rvs_tls_fallback:
|
||||
self.rvs_url_fallback = f"ws://{rvs_host}:{rvs_port}"
|
||||
else:
|
||||
self.rvs_url_fallback = ""
|
||||
else:
|
||||
self.rvs_url = ""
|
||||
self.rvs_url_fallback = ""
|
||||
self.current_mode = Mode.NORMAL
|
||||
self.running = False
|
||||
|
||||
|
|
@ -425,21 +439,30 @@ class ARIABridge:
|
|||
self.ws_rvs: Optional[websockets.WebSocketClientProtocol] = None
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""Initialisiert alle Komponenten."""
|
||||
"""Initialisiert alle Komponenten.
|
||||
|
||||
Audio-Komponenten (TTS, STT, Wake-Word) sind optional —
|
||||
wenn kein Audio-Geraet vorhanden ist (z.B. VM ohne Soundkarte),
|
||||
laeuft die Bridge trotzdem als reiner RVS-Relay.
|
||||
"""
|
||||
logger.info("=" * 50)
|
||||
logger.info("ARIA Voice Bridge startet...")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# PulseAudio-Server pruefen
|
||||
pulse_server = os.getenv("PULSE_SERVER")
|
||||
if pulse_server:
|
||||
logger.info("PulseAudio Server: %s", pulse_server)
|
||||
else:
|
||||
logger.warning("Kein PULSE_SERVER gesetzt — verwende Standard-Audio")
|
||||
|
||||
# Voice-Engine IMMER laden — rendert Audio fuer die App (auch ohne Soundkarte)
|
||||
self.voice_engine.initialize()
|
||||
self.stt_engine.initialize()
|
||||
self.wake_word.initialize()
|
||||
|
||||
# Audio-Hardware pruefen (fuer lokales Mikro/Lautsprecher)
|
||||
self.audio_available = False
|
||||
try:
|
||||
sd.query_devices()
|
||||
self.audio_available = True
|
||||
logger.info("Audio-Geraet gefunden — Wake-Word und lokale TTS aktiv")
|
||||
self.stt_engine.initialize()
|
||||
self.wake_word.initialize()
|
||||
except (sd.PortAudioError, Exception):
|
||||
logger.warning("Kein Audio-Geraet — Wake-Word und lokale TTS deaktiviert")
|
||||
logger.info("Piper TTS rendert Audio fuer die App (via RVS)")
|
||||
|
||||
logger.info("Alle Komponenten initialisiert")
|
||||
logger.info("aria-core: %s", self.ws_url)
|
||||
|
|
@ -516,20 +539,39 @@ class ARIABridge:
|
|||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
|
||||
# Stimme auswaehlen
|
||||
voice_name = requested_voice or self.voice_engine.select_voice(text)
|
||||
|
||||
# Antwort an die App weiterleiten (als Chat-Nachricht)
|
||||
await self._send_to_rvs({
|
||||
"type": "chat",
|
||||
"payload": {
|
||||
"text": text,
|
||||
"sender": "aria",
|
||||
"voice": requested_voice or self.voice_engine.select_voice(text),
|
||||
"voice": voice_name,
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
|
||||
# Sprachausgabe lokal (wenn Modus es erlaubt)
|
||||
# TTS-Audio rendern und an die App senden (wenn Modus es erlaubt)
|
||||
if should_speak(self.current_mode, is_critical):
|
||||
self.voice_engine.speak(text, requested_voice)
|
||||
audio_data = self.voice_engine.synthesize(text, voice_name)
|
||||
if audio_data:
|
||||
audio_b64 = base64.b64encode(audio_data).decode("ascii")
|
||||
await self._send_to_rvs({
|
||||
"type": "audio",
|
||||
"payload": {
|
||||
"base64": audio_b64,
|
||||
"mimeType": "audio/wav",
|
||||
"voice": voice_name,
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
logger.info("[core] TTS-Audio gesendet: %d bytes (%s)", len(audio_data), voice_name)
|
||||
|
||||
# Lokal abspielen (nur wenn Soundkarte vorhanden)
|
||||
if self.audio_available:
|
||||
self.voice_engine.speak(text, requested_voice)
|
||||
else:
|
||||
logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name)
|
||||
|
||||
|
|
@ -557,19 +599,21 @@ class ARIABridge:
|
|||
async def connect_to_rvs(self) -> None:
|
||||
"""Persistente WebSocket-Verbindung zum RVS mit Auto-Reconnect.
|
||||
|
||||
Authentifiziert sich mit dem gleichen Token wie die App.
|
||||
Nachrichten von der App werden an aria-core weitergeleitet.
|
||||
Bei TLS-Fehler wird automatisch auf ws:// gefallbackt
|
||||
(wenn RVS_TLS_FALLBACK=true).
|
||||
"""
|
||||
if not self.rvs_url or not self.rvs_token:
|
||||
logger.info("[rvs] Nicht konfiguriert — ueberspringe")
|
||||
return
|
||||
|
||||
retry_delay = 2
|
||||
url = f"{self.rvs_url}?token={self.rvs_token}"
|
||||
current_url = self.rvs_url
|
||||
using_fallback = False
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
logger.info("[rvs] Verbinde: %s", self.rvs_url)
|
||||
url = f"{current_url}?token={self.rvs_token}"
|
||||
logger.info("[rvs] Verbinde: %s", current_url)
|
||||
async with websockets.connect(url) as ws:
|
||||
self.ws_rvs = ws
|
||||
retry_delay = 2
|
||||
|
|
@ -588,6 +632,16 @@ class ARIABridge:
|
|||
logger.warning("[rvs] Verbindung verloren")
|
||||
except ConnectionRefusedError:
|
||||
logger.warning("[rvs] Nicht erreichbar")
|
||||
except (ssl.SSLError, OSError) as e:
|
||||
# TLS-Fehler — Fallback auf ws:// versuchen
|
||||
if not using_fallback and self.rvs_url_fallback:
|
||||
logger.warning("[rvs] TLS-Fehler: %s", e)
|
||||
logger.warning("[rvs] TLS gewollt aber nicht verfuegbar — Fallback auf ws://")
|
||||
current_url = self.rvs_url_fallback
|
||||
using_fallback = True
|
||||
retry_delay = 1 # Sofort versuchen
|
||||
else:
|
||||
logger.error("[rvs] SSL-Fehler (kein Fallback): %s", e)
|
||||
except Exception:
|
||||
logger.exception("[rvs] WebSocket-Fehler")
|
||||
finally:
|
||||
|
|
@ -762,15 +816,22 @@ class ARIABridge:
|
|||
# ── Run & Shutdown ───────────────────────────────────────
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Startet die Bridge mit allen drei Verbindungen parallel."""
|
||||
"""Startet die Bridge mit allen Verbindungen parallel.
|
||||
|
||||
Ohne Audio-Geraet laeuft nur core + rvs (reiner Relay-Modus).
|
||||
"""
|
||||
self.running = True
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(self.connect_to_core()),
|
||||
asyncio.create_task(self.connect_to_rvs()),
|
||||
asyncio.create_task(self.audio_loop()),
|
||||
]
|
||||
|
||||
if self.audio_available:
|
||||
tasks.append(asyncio.create_task(self.audio_loop()))
|
||||
else:
|
||||
logger.info("Audio-Loop deaktiviert — kein Audio-Geraet")
|
||||
|
||||
try:
|
||||
await asyncio.gather(*tasks)
|
||||
except asyncio.CancelledError:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ services:
|
|||
- RVS_HOST=${RVS_HOST:-}
|
||||
- RVS_PORT=${RVS_PORT:-443}
|
||||
- RVS_TLS=${RVS_TLS:-true}
|
||||
- RVS_TLS_FALLBACK=${RVS_TLS_FALLBACK:-true}
|
||||
- RVS_TOKEN=${RVS_TOKEN:-}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
|
|
|
|||
Loading…
Reference in New Issue