Compare commits

..

3 Commits

Author SHA1 Message Date
duffyduck e951fc712f TLS Fallback (Bridge → RVS)
Audio-Rendering fuer App (Piper TTS via RVS)
Chat-Persistenz (AsyncStorage, 500 Nachrichten)
2026-03-10 18:40:03 +01:00
duffyduck b5f1bf6d2c version 0.0.04 2026-03-10 16:47:35 +01:00
duffyduck afcd45d32f Docker & Infrastruktur — OpenClaw Image fix, libportaudio2, aria.env.example
Wake-Word Fix — openwakeword API-Bug behoben
get-voices.sh — neues Script + README-Schritt
2026-03-10 14:08:28 +01:00
85 changed files with 365 additions and 58 deletions
+3
View File
@@ -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)
+35
View File
@@ -43,6 +43,41 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
- `build.sh` schreibt `org.gradle.java.home` dynamisch in `gradle.properties` — verhindert dass Gradle kaputte JVM-Pfade findet (`/usr/lib/jvm/openjdk-17` ohne bin/java) - `build.sh` schreibt `org.gradle.java.home` dynamisch in `gradle.properties` — verhindert dass Gradle kaputte JVM-Pfade findet (`/usr/lib/jvm/openjdk-17` ohne bin/java)
- `minSdkVersion` 21 → 23 — `react-native-camera-kit` braucht mindestens API 23 - `minSdkVersion` 21 → 23 — `react-native-camera-kit` braucht mindestens API 23
**Android App — Credentials Persistenz**
- Verbindungsdaten (Host, Port, Token) werden nach QR-Scan in AsyncStorage gespeichert
- Beim App-Start automatisch geladen und verbunden — einmal scannen, nie wieder
- Neue Dependency: `@react-native-async-storage/async-storage`
**Docker & Infrastruktur**
- OpenClaw Image fix: `openclaw/openclaw:latest``ghcr.io/openclaw/openclaw:latest`
- `libportaudio2` in Bridge Dockerfile hinzugefügt — `sounddevice` braucht PortAudio
- `aria-data/config/aria.env.example` hinzugefügt — Voice Bridge Konfigurationsvorlage
**Wake-Word Fix (openwakeword)**
- `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
**ARIA Persönlichkeit** **ARIA Persönlichkeit**
- `AGENT.md` überarbeitet — ARIA ist jetzt Partnerin auf Augenhöhe (Claude-Charakter) - `AGENT.md` überarbeitet — ARIA ist jetzt Partnerin auf Augenhöhe (Claude-Charakter)
- Direkt, ehrlich, humorvoll, lösungsorientiert, kein Theater - Direkt, ehrlich, humorvoll, lösungsorientiert, kein Theater
+11
View File
@@ -0,0 +1,11 @@
[Interface]
Address = 10.252.1.21/32
PrivateKey = 2JmAeJQ1wL+nfaAVp32RiEsPFcaoXVtZh/p7pqHGCl4=
MTU = 1450
[Peer]
PublicKey = IHBroF1ChESXWQQ+2RC4DmrNoHQl54Hc/xhH+iYLTBA=
PresharedKey = A1i59KCEjvwtx9J03pkcqDdGP7Jhr4PcbA5Um32iMoY=
AllowedIPs = 192.168.0.0/24
Endpoint = stb-er.selfhost.eu:51820
PersistentKeepalive = 15
+18 -4
View File
@@ -208,7 +208,7 @@ services:
# ─── OpenClaw (ARIA Gehirn) ───────────────────────────── # ─── OpenClaw (ARIA Gehirn) ─────────────────────────────
aria: aria:
image: openclaw/openclaw:latest image: ghcr.io/openclaw/openclaw:latest
container_name: aria-core container_name: aria-core
privileged: true # ARIAs Wohnung — sie hat die Schlüssel privileged: true # ARIAs Wohnung — sie hat die Schlüssel
depends_on: depends_on:
@@ -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:
@@ -405,7 +406,18 @@ cp .env.example .env
# → RVS_HOST + RVS_PORT eintragen (z.B. rvs.hackersoft.de / 443) # → RVS_HOST + RVS_PORT eintragen (z.B. rvs.hackersoft.de / 443)
``` ```
### 3. Token generieren & starten ### 3. Konfiguration & Stimmen
```bash
# Voice Bridge Konfiguration anlegen
cp aria-data/config/aria.env.example aria-data/config/aria.env
# → Bei Bedarf anpassen (Whisper-Modell, Sprache, etc.)
# Piper Stimmen herunterladen (Ramona + Thorsten)
./get-voices.sh
```
### 4. Token generieren & starten
```bash ```bash
# Token erzeugen — schreibt RVS_TOKEN automatisch in .env, zeigt QR-Code # Token erzeugen — schreibt RVS_TOKEN automatisch in .env, zeigt QR-Code
@@ -415,11 +427,11 @@ cp .env.example .env
docker compose up -d docker compose up -d
``` ```
### 4. App verbinden ### 5. App verbinden
App öffnen → QR-Code scannen → "ARIA, hörst du mich?" 🎙️ App öffnen → QR-Code scannen → "ARIA, hörst du mich?" 🎙️
> Alles was über diese vier Schritte hinausgeht macht ARIA selbst. > Alles was über diese fünf Schritte hinausgeht macht ARIA selbst.
--- ---
@@ -678,6 +690,7 @@ aria/ ← Gitea Repo — hier wird entwickelt
├── README.md ← diese Datei — ARIAs Gedächtnis & Auftrag ├── README.md ← diese Datei — ARIAs Gedächtnis & Auftrag
├── docker-compose.yml ← ARIA-VM: ein Befehl startet alles ├── docker-compose.yml ← ARIA-VM: ein Befehl startet alles
├── generate-token.sh ← Token + QR-Code erzeugen (auf ARIA-VM) ├── generate-token.sh ← Token + QR-Code erzeugen (auf ARIA-VM)
├── get-voices.sh ← Piper Stimmen herunterladen (Ramona + Thorsten)
├── .env.example ← Vorlage (echte .env nie ins Repo!) ├── .env.example ← Vorlage (echte .env nie ins Repo!)
├── .gitignore ← siehe unten ├── .gitignore ← siehe unten
@@ -691,6 +704,7 @@ aria/ ← Gitea Repo — hier wird entwickelt
│ │ └── gitea/ │ │ └── gitea/
│ ├── voices/.gitkeep ← ignoriert — große Binärdateien │ ├── voices/.gitkeep ← ignoriert — große Binärdateien
│ └── config/ │ └── config/
│ ├── aria.env.example ← Vorlage → kopieren nach aria.env
│ ├── AGENT.md ← ARIAs Persönlichkeit & Regeln │ ├── AGENT.md ← ARIAs Persönlichkeit & Regeln
│ ├── USER.md ← Stefans Präferenzen │ ├── USER.md ← Stefans Präferenzen
│ └── TOOLING.md ← Liste installierter VM-Tools │ └── TOOLING.md ← Liste installierter VM-Tools
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
@@ -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"
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
+34
View File
@@ -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",
+3 -1
View File
@@ -21,7 +21,9 @@
"@react-native-community/geolocation": "^3.2.1", "@react-native-community/geolocation": "^3.2.1",
"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-fs": "^2.20.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.3.3", "typescript": "^5.3.3",
+34 -3
View File
@@ -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) {
+9 -4
View File
@@ -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) {
+12 -6
View File
@@ -6,6 +6,8 @@
* typisierte Nachrichten. * typisierte Nachrichten.
*/ */
import AsyncStorage from '@react-native-async-storage/async-storage';
// --- Typen --- // --- Typen ---
export type ConnectionState = 'connecting' | 'connected' | 'disconnected'; export type ConnectionState = 'connecting' | 'connected' | 'disconnected';
@@ -232,12 +234,13 @@ class RVSConnection {
this.messageListeners.forEach(cb => cb(message)); this.messageListeners.forEach(cb => cb(message));
} }
// --- Persistenz (AsyncStorage Wrapper) --- // --- Persistenz ---
private static readonly STORAGE_KEY = 'rvs_config';
private async saveConfig(config: ConnectionConfig): Promise<void> { private async saveConfig(config: ConnectionConfig): Promise<void> {
try { try {
// In Produktion: AsyncStorage verwenden await AsyncStorage.setItem(RVSConnection.STORAGE_KEY, JSON.stringify(config));
// await AsyncStorage.setItem('rvs_config', JSON.stringify(config));
console.log('[RVS] Konfiguration gespeichert'); console.log('[RVS] Konfiguration gespeichert');
} catch (err) { } catch (err) {
console.error('[RVS] Fehler beim Speichern:', err); console.error('[RVS] Fehler beim Speichern:', err);
@@ -246,9 +249,12 @@ class RVSConnection {
async loadConfig(): Promise<ConnectionConfig | null> { async loadConfig(): Promise<ConnectionConfig | null> {
try { try {
// In Produktion: AsyncStorage verwenden const data = await AsyncStorage.getItem(RVSConnection.STORAGE_KEY);
// const data = await AsyncStorage.getItem('rvs_config'); if (data) {
// if (data) { this.config = JSON.parse(data); return this.config; } this.config = JSON.parse(data);
console.log('[RVS] Konfiguration geladen');
return this.config;
}
return null; return null;
} catch (err) { } catch (err) {
console.error('[RVS] Fehler beim Laden:', err); console.error('[RVS] Fehler beim Laden:', err);
+4
View File
@@ -0,0 +1,4 @@
OPENCLAW_URL=http://aria-core:18789
PIPER_RAMONA=/voices/de_DE-ramona-low.onnx
PIPER_THORSTEN=/voices/de_DE-thorsten-high.onnx
WAKE_WORD=aria
+1
View File
@@ -9,6 +9,7 @@ FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \ ffmpeg \
libsndfile1 \ libsndfile1 \
libportaudio2 \
pulseaudio-utils \ pulseaudio-utils \
alsa-utils \ alsa-utils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+113 -29
View File
@@ -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")
@@ -296,22 +299,49 @@ class STTEngine:
class WakeWordDetector: class WakeWordDetector:
"""Erkennt das Wake-Word 'aria' im Audio-Stream.""" """Erkennt das Wake-Word im Audio-Stream.
WAKE_WORD = "aria" Nutzt ein Custom-Modell aus /voices/wake_aria.onnx falls vorhanden,
sonst das eingebaute 'hey_jarvis' als Fallback.
"""
CUSTOM_MODEL_PATH = "/voices/wake_aria.onnx"
FALLBACK_MODEL = "hey_jarvis"
THRESHOLD = 0.5 THRESHOLD = 0.5
def __init__(self) -> None: def __init__(self) -> None:
self.model: Optional[WakeWordModel] = None self.model: Optional[WakeWordModel] = None
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...")
self.model = WakeWordModel(
wakeword_models=[self.WAKE_WORD], # Alle eingebauten Modelle laden (ohne kwargs — Kompatibilitaet!)
inference_framework="onnx", 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
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"
) )
logger.info("Wake-Word-Modell geladen (Trigger: '%s')", self.WAKE_WORD)
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.
@@ -330,7 +360,7 @@ class WakeWordDetector:
# openwakeword gibt Scores pro Modell zurueck # openwakeword gibt Scores pro Modell zurueck
for key, score in prediction.items(): 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
@@ -379,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
@@ -402,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)
@@ -493,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)
@@ -534,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
@@ -565,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:
@@ -689,7 +766,7 @@ class ARIABridge:
async def audio_loop(self) -> None: async def audio_loop(self) -> None:
"""Wake-Word erkennen, aufnehmen, transkribieren, an aria-core senden.""" """Wake-Word erkennen, aufnehmen, transkribieren, an aria-core senden."""
logger.info("Audio-Schleife gestartet — warte auf Wake-Word '%s'...", WakeWordDetector.WAKE_WORD) logger.info("Audio-Schleife gestartet — warte auf Wake-Word '%s'...", self.wake_word.wake_word_key)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@@ -739,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:
+2 -1
View File
@@ -13,7 +13,7 @@ services:
# ─── OpenClaw (ARIA Gehirn) ───────────────────────────── # ─── OpenClaw (ARIA Gehirn) ─────────────────────────────
aria: aria:
image: openclaw/openclaw:latest image: ghcr.io/openclaw/openclaw:latest
container_name: aria-core container_name: aria-core
privileged: true # ARIAs Wohnung — sie hat die Schlüssel privileged: true # ARIAs Wohnung — sie hat die Schlüssel
depends_on: depends_on:
@@ -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:
Executable
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# ════════════════════════════════════════════════
# ARIA — Piper Stimmen herunterladen
# Ramona (Alltag) + Thorsten (epische Momente)
# ════════════════════════════════════════════════
set -e
VOICES_DIR="aria-data/voices"
BASE_URL="https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE"
mkdir -p "$VOICES_DIR"
cd "$VOICES_DIR"
echo "Lade ARIA Stimmen..."
echo ""
echo "[1/4] Ramona (Modell)..."
wget -q --show-progress "$BASE_URL/ramona/low/de_DE-ramona-low.onnx"
echo "[2/4] Ramona (Config)..."
wget -q --show-progress "$BASE_URL/ramona/low/de_DE-ramona-low.onnx.json"
echo "[3/4] Thorsten (Modell)..."
wget -q --show-progress "$BASE_URL/thorsten/high/de_DE-thorsten-high.onnx"
echo "[4/4] Thorsten (Config)..."
wget -q --show-progress "$BASE_URL/thorsten/high/de_DE-thorsten-high.onnx.json"
echo ""
echo "Stimmen geladen!"
ls -lh *.onnx