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_HOST=rvs.example.de
|
||||||
RVS_PORT=443
|
RVS_PORT=443
|
||||||
RVS_TLS=true
|
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=
|
RVS_TOKEN=
|
||||||
|
|
||||||
# Gitea (for release.sh — Kennwort wird interaktiv abgefragt)
|
# 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`
|
- `WakeWordDetector` umgebaut — sucht Custom-Modell `/voices/wake_aria.onnx`, Fallback auf eingebautes `hey_jarvis`
|
||||||
- Alter Code crashte: `wakeword_models=["aria"]` erwartet Dateipfad, kein Keyword
|
- 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`**
|
**Neues Script: `get-voices.sh`**
|
||||||
- Lädt Piper Stimmen (Ramona + Thorsten) von HuggingFace herunter
|
- Lädt Piper Stimmen (Ramona + Thorsten) von HuggingFace herunter
|
||||||
- Neuer Installationsschritt in README
|
- Neuer Installationsschritt in README
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ services:
|
||||||
- RVS_HOST=${RVS_HOST:-}
|
- RVS_HOST=${RVS_HOST:-}
|
||||||
- RVS_PORT=${RVS_PORT:-443}
|
- RVS_PORT=${RVS_PORT:-443}
|
||||||
- RVS_TLS=${RVS_TLS:-true}
|
- RVS_TLS=${RVS_TLS:-true}
|
||||||
|
- RVS_TLS_FALLBACK=${RVS_TLS_FALLBACK:-true}
|
||||||
- RVS_TOKEN=${RVS_TOKEN:-}
|
- RVS_TOKEN=${RVS_TOKEN:-}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
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;
|
import com.zoontek.rnpermissions.RNPermissionsPackage;
|
||||||
// react-native-camera-kit
|
// react-native-camera-kit
|
||||||
import com.rncamerakit.RNCameraKitPackage;
|
import com.rncamerakit.RNCameraKitPackage;
|
||||||
|
// @react-native-async-storage/async-storage
|
||||||
|
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
|
||||||
|
|
||||||
public class PackageList {
|
public class PackageList {
|
||||||
private Application application;
|
private Application application;
|
||||||
|
|
@ -79,7 +81,8 @@ public class PackageList {
|
||||||
new GeolocationPackage(),
|
new GeolocationPackage(),
|
||||||
new ImagePickerPackage(),
|
new ImagePickerPackage(),
|
||||||
new RNPermissionsPackage(),
|
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"
|
symbolFile="R.txt"
|
||||||
externalAnnotations="annotations.zip"
|
externalAnnotations="annotations.zip"
|
||||||
proguardRules="proguard.txt"/>
|
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
|
<library
|
||||||
name="org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21@jar"
|
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"
|
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"
|
symbolFile="R.txt"
|
||||||
externalAnnotations="annotations.zip"
|
externalAnnotations="annotations.zip"
|
||||||
proguardRules="proguard.txt"/>
|
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
|
<library
|
||||||
name="org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21@jar"
|
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"
|
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
|
base.2=/home/duffy/Dokumente/programmierung/ARIA-AGENT/android/android/app/build/intermediates/dex/release/mergeDexRelease/classes2.dex
|
||||||
path.2=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
|
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-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-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-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.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: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
|
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-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-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.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
|
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",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-async-storage/async-storage": "^1.21.0",
|
||||||
"@react-native-community/geolocation": "^3.2.1",
|
"@react-native-community/geolocation": "^3.2.1",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.11",
|
"@react-navigation/bottom-tabs": "^6.5.11",
|
||||||
"@react-navigation/native": "^6.1.9",
|
"@react-navigation/native": "^6.1.9",
|
||||||
|
|
@ -2848,6 +2849,18 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@react-native-community/cli": {
|
||||||
"version": "12.3.2",
|
"version": "12.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.2.tgz",
|
||||||
|
|
@ -7682,6 +7695,15 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/is-plain-object": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
"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==",
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"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-image-picker": "^7.1.0",
|
||||||
"react-native-permissions": "^4.1.4",
|
"react-native-permissions": "^4.1.4",
|
||||||
"react-native-camera-kit": "^13.0.0",
|
"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": {
|
"devDependencies": {
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Modal,
|
Modal,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
import VoiceButton from '../components/VoiceButton';
|
import VoiceButton from '../components/VoiceButton';
|
||||||
|
|
@ -41,6 +42,11 @@ interface ChatMessage {
|
||||||
attachments?: Attachment[];
|
attachments?: Attachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Konstanten ---
|
||||||
|
|
||||||
|
const CHAT_STORAGE_KEY = 'aria_chat_messages';
|
||||||
|
const MAX_STORED_MESSAGES = 500;
|
||||||
|
|
||||||
// --- Komponente ---
|
// --- Komponente ---
|
||||||
|
|
||||||
const ChatScreen: React.FC = () => {
|
const ChatScreen: React.FC = () => {
|
||||||
|
|
@ -60,10 +66,26 @@ const ChatScreen: React.FC = () => {
|
||||||
return `msg_${Date.now()}_${messageIdCounter.current}`;
|
return `msg_${Date.now()}_${messageIdCounter.current}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// GPS-Einstellung aus Settings laden (vereinfacht)
|
// Chat-Verlauf aus AsyncStorage laden
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// In Produktion: AsyncStorage oder Context verwenden
|
const loadMessages = async () => {
|
||||||
// Hier Platzhalter - GPS Toggle kommt aus SettingsScreen
|
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
|
// 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
|
// Auto-Scroll bei neuen Nachrichten
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { Platform, PermissionsAndroid } from 'react-native';
|
import { Platform, PermissionsAndroid } from 'react-native';
|
||||||
import Sound from 'react-native-sound';
|
import Sound from 'react-native-sound';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
|
||||||
// --- Typen ---
|
// --- Typen ---
|
||||||
|
|
||||||
|
|
@ -127,18 +128,20 @@ class AudioService {
|
||||||
|
|
||||||
/** Base64-kodiertes Audio abspielen (z.B. TTS-Antwort von ARIA) */
|
/** Base64-kodiertes Audio abspielen (z.B. TTS-Antwort von ARIA) */
|
||||||
async playAudio(base64Data: string): Promise<void> {
|
async playAudio(base64Data: string): Promise<void> {
|
||||||
|
if (!base64Data) return;
|
||||||
|
|
||||||
// Laufende Wiedergabe stoppen
|
// Laufende Wiedergabe stoppen
|
||||||
this.stopPlayback();
|
this.stopPlayback();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Base64-Daten in temporaere Datei schreiben und abspielen
|
// Base64 → temporaere WAV-Datei → Sound abspielen
|
||||||
// In Produktion: react-native-fs + Sound kombinieren
|
const tmpPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`;
|
||||||
const tmpPath = `${Platform.OS === 'android' ? '/data/user/0/' : ''}aria_tts_temp.wav`;
|
await RNFS.writeFile(tmpPath, base64Data, 'base64');
|
||||||
|
|
||||||
// Platzhalter: Sound aus Datei laden
|
|
||||||
this.currentSound = new Sound(tmpPath, '', (error) => {
|
this.currentSound = new Sound(tmpPath, '', (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('[Audio] Fehler beim Laden:', error);
|
console.error('[Audio] Fehler beim Laden:', error);
|
||||||
|
RNFS.unlink(tmpPath).catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentSound?.play((success) => {
|
this.currentSound?.play((success) => {
|
||||||
|
|
@ -149,6 +152,8 @@ class AudioService {
|
||||||
}
|
}
|
||||||
this.currentSound?.release();
|
this.currentSound?.release();
|
||||||
this.currentSound = null;
|
this.currentSound = null;
|
||||||
|
// Temp-Datei aufraeumen
|
||||||
|
RNFS.unlink(tmpPath).catch(() => {});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ Stimmen:
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import wave
|
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_HOST = os.getenv("RVS_HOST", "") # z.B. rvs.hackersoft.de
|
||||||
RVS_PORT = os.getenv("RVS_PORT", "443") # Port des RVS
|
RVS_PORT = os.getenv("RVS_PORT", "443") # Port des RVS
|
||||||
RVS_TLS = os.getenv("RVS_TLS", "true") # true = wss://, false = ws://
|
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)
|
RVS_TOKEN = os.getenv("RVS_TOKEN", "") # Pairing-Token (gleich wie in der App)
|
||||||
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "small")
|
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "small")
|
||||||
WHISPER_LANGUAGE = os.getenv("WHISPER_LANGUAGE", "de")
|
WHISPER_LANGUAGE = os.getenv("WHISPER_LANGUAGE", "de")
|
||||||
|
|
@ -311,30 +314,34 @@ class WakeWordDetector:
|
||||||
self.wake_word_key: str = ""
|
self.wake_word_key: str = ""
|
||||||
|
|
||||||
def initialize(self) -> None:
|
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...")
|
logger.info("Lade Wake-Word-Modell...")
|
||||||
|
|
||||||
custom_path = Path(self.CUSTOM_MODEL_PATH)
|
# Alle eingebauten Modelle laden (ohne kwargs — Kompatibilitaet!)
|
||||||
if custom_path.exists():
|
self.model = WakeWordModel()
|
||||||
# Custom "aria" Modell vorhanden
|
|
||||||
self.model = WakeWordModel(
|
# Verfuegbare Modelle ermitteln
|
||||||
wakeword_models=[str(custom_path)],
|
available = list(self.model.models.keys()) if hasattr(self.model, 'models') else []
|
||||||
)
|
logger.info("Verfuegbare Wake-Words: %s", ", ".join(available) if available else "(keine)")
|
||||||
self.wake_word_key = custom_path.stem
|
|
||||||
logger.info("Custom Wake-Word-Modell geladen: %s", custom_path)
|
# Bestes Modell auswaehlen
|
||||||
else:
|
if self.FALLBACK_MODEL in available:
|
||||||
# Fallback auf eingebautes Modell
|
|
||||||
self.model = WakeWordModel()
|
|
||||||
self.wake_word_key = self.FALLBACK_MODEL
|
self.wake_word_key = self.FALLBACK_MODEL
|
||||||
logger.warning(
|
elif available:
|
||||||
"Kein Custom-Modell (%s) — nutze Fallback '%s'",
|
self.wake_word_key = available[0]
|
||||||
self.CUSTOM_MODEL_PATH,
|
else:
|
||||||
self.FALLBACK_MODEL,
|
self.wake_word_key = self.FALLBACK_MODEL
|
||||||
)
|
|
||||||
logger.info(
|
logger.info("Wake-Word aktiv: '%s'", self.wake_word_key)
|
||||||
"Tipp: Custom Wake-Word trainieren → "
|
logger.info(
|
||||||
"https://github.com/dscripka/openWakeWord#training-new-models"
|
"Tipp: Custom 'aria' Wake-Word trainieren → "
|
||||||
)
|
"https://github.com/dscripka/openWakeWord#training-new-models"
|
||||||
|
)
|
||||||
|
|
||||||
def detect(self, audio_chunk: np.ndarray) -> bool:
|
def detect(self, audio_chunk: np.ndarray) -> bool:
|
||||||
"""Prueft ob das Wake-Word im Audio-Chunk enthalten ist.
|
"""Prueft ob das Wake-Word im Audio-Chunk enthalten ist.
|
||||||
|
|
@ -351,10 +358,10 @@ class WakeWordDetector:
|
||||||
prediction = self.model.predict(audio_chunk)
|
prediction = self.model.predict(audio_chunk)
|
||||||
|
|
||||||
# openwakeword gibt Scores pro Modell zurueck
|
# openwakeword gibt Scores pro Modell zurueck
|
||||||
score = prediction.get(self.wake_word_key, 0)
|
for key, score in prediction.items():
|
||||||
if score > self.THRESHOLD:
|
if score > self.THRESHOLD:
|
||||||
logger.info("Wake-Word erkannt! (Score: %.2f)", score)
|
logger.info("Wake-Word '%s' erkannt! (Score: %.2f)", key, score)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -402,13 +409,20 @@ class ARIABridge:
|
||||||
rvs_host = self.config.get("RVS_HOST", RVS_HOST)
|
rvs_host = self.config.get("RVS_HOST", RVS_HOST)
|
||||||
rvs_port = self.config.get("RVS_PORT", RVS_PORT)
|
rvs_port = self.config.get("RVS_PORT", RVS_PORT)
|
||||||
rvs_tls = self.config.get("RVS_TLS", RVS_TLS).lower() == "true"
|
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)
|
self.rvs_token = self.config.get("RVS_TOKEN", RVS_TOKEN)
|
||||||
# URL zusammenbauen
|
# URLs zusammenbauen (primaer + fallback)
|
||||||
if rvs_host:
|
if rvs_host:
|
||||||
proto = "wss" if rvs_tls else "ws"
|
proto = "wss" if rvs_tls else "ws"
|
||||||
self.rvs_url = f"{proto}://{rvs_host}:{rvs_port}"
|
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:
|
else:
|
||||||
self.rvs_url = ""
|
self.rvs_url = ""
|
||||||
|
self.rvs_url_fallback = ""
|
||||||
self.current_mode = Mode.NORMAL
|
self.current_mode = Mode.NORMAL
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
|
@ -425,21 +439,30 @@ class ARIABridge:
|
||||||
self.ws_rvs: Optional[websockets.WebSocketClientProtocol] = None
|
self.ws_rvs: Optional[websockets.WebSocketClientProtocol] = None
|
||||||
|
|
||||||
def initialize(self) -> 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("=" * 50)
|
||||||
logger.info("ARIA Voice Bridge startet...")
|
logger.info("ARIA Voice Bridge startet...")
|
||||||
logger.info("=" * 50)
|
logger.info("=" * 50)
|
||||||
|
|
||||||
# PulseAudio-Server pruefen
|
# Voice-Engine IMMER laden — rendert Audio fuer die App (auch ohne Soundkarte)
|
||||||
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")
|
|
||||||
|
|
||||||
self.voice_engine.initialize()
|
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("Alle Komponenten initialisiert")
|
||||||
logger.info("aria-core: %s", self.ws_url)
|
logger.info("aria-core: %s", self.ws_url)
|
||||||
|
|
@ -516,20 +539,39 @@ class ARIABridge:
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
"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)
|
# Antwort an die App weiterleiten (als Chat-Nachricht)
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
"type": "chat",
|
"type": "chat",
|
||||||
"payload": {
|
"payload": {
|
||||||
"text": text,
|
"text": text,
|
||||||
"sender": "aria",
|
"sender": "aria",
|
||||||
"voice": requested_voice or self.voice_engine.select_voice(text),
|
"voice": voice_name,
|
||||||
},
|
},
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
"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):
|
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:
|
else:
|
||||||
logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name)
|
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:
|
async def connect_to_rvs(self) -> None:
|
||||||
"""Persistente WebSocket-Verbindung zum RVS mit Auto-Reconnect.
|
"""Persistente WebSocket-Verbindung zum RVS mit Auto-Reconnect.
|
||||||
|
|
||||||
Authentifiziert sich mit dem gleichen Token wie die App.
|
Bei TLS-Fehler wird automatisch auf ws:// gefallbackt
|
||||||
Nachrichten von der App werden an aria-core weitergeleitet.
|
(wenn RVS_TLS_FALLBACK=true).
|
||||||
"""
|
"""
|
||||||
if not self.rvs_url or not self.rvs_token:
|
if not self.rvs_url or not self.rvs_token:
|
||||||
logger.info("[rvs] Nicht konfiguriert — ueberspringe")
|
logger.info("[rvs] Nicht konfiguriert — ueberspringe")
|
||||||
return
|
return
|
||||||
|
|
||||||
retry_delay = 2
|
retry_delay = 2
|
||||||
url = f"{self.rvs_url}?token={self.rvs_token}"
|
current_url = self.rvs_url
|
||||||
|
using_fallback = False
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
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:
|
async with websockets.connect(url) as ws:
|
||||||
self.ws_rvs = ws
|
self.ws_rvs = ws
|
||||||
retry_delay = 2
|
retry_delay = 2
|
||||||
|
|
@ -588,6 +632,16 @@ class ARIABridge:
|
||||||
logger.warning("[rvs] Verbindung verloren")
|
logger.warning("[rvs] Verbindung verloren")
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
logger.warning("[rvs] Nicht erreichbar")
|
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:
|
except Exception:
|
||||||
logger.exception("[rvs] WebSocket-Fehler")
|
logger.exception("[rvs] WebSocket-Fehler")
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -762,15 +816,22 @@ class ARIABridge:
|
||||||
# ── Run & Shutdown ───────────────────────────────────────
|
# ── Run & Shutdown ───────────────────────────────────────
|
||||||
|
|
||||||
async def run(self) -> None:
|
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
|
self.running = True
|
||||||
|
|
||||||
tasks = [
|
tasks = [
|
||||||
asyncio.create_task(self.connect_to_core()),
|
asyncio.create_task(self.connect_to_core()),
|
||||||
asyncio.create_task(self.connect_to_rvs()),
|
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:
|
try:
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ services:
|
||||||
- RVS_HOST=${RVS_HOST:-}
|
- RVS_HOST=${RVS_HOST:-}
|
||||||
- RVS_PORT=${RVS_PORT:-443}
|
- RVS_PORT=${RVS_PORT:-443}
|
||||||
- RVS_TLS=${RVS_TLS:-true}
|
- RVS_TLS=${RVS_TLS:-true}
|
||||||
|
- RVS_TLS_FALLBACK=${RVS_TLS_FALLBACK:-true}
|
||||||
- RVS_TOKEN=${RVS_TOKEN:-}
|
- RVS_TOKEN=${RVS_TOKEN:-}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue