Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e951fc712f | |||
| b5f1bf6d2c | |||
| afcd45d32f |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
- `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**
|
||||
- `AGENT.md` überarbeitet — ARIA ist jetzt Partnerin auf Augenhöhe (Claude-Charakter)
|
||||
- Direkt, ehrlich, humorvoll, lösungsorientiert, kein Theater
|
||||
|
||||
@@ -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
|
||||
@@ -208,7 +208,7 @@ services:
|
||||
|
||||
# ─── OpenClaw (ARIA Gehirn) ─────────────────────────────
|
||||
aria:
|
||||
image: openclaw/openclaw:latest
|
||||
image: ghcr.io/openclaw/openclaw:latest
|
||||
container_name: aria-core
|
||||
privileged: true # ARIAs Wohnung — sie hat die Schlüssel
|
||||
depends_on:
|
||||
@@ -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:
|
||||
@@ -405,7 +406,18 @@ cp .env.example .env
|
||||
# → 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
|
||||
# Token erzeugen — schreibt RVS_TOKEN automatisch in .env, zeigt QR-Code
|
||||
@@ -415,11 +427,11 @@ cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 4. App verbinden
|
||||
### 5. App verbinden
|
||||
|
||||
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
|
||||
├── docker-compose.yml ← ARIA-VM: ein Befehl startet alles
|
||||
├── 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!)
|
||||
├── .gitignore ← siehe unten
|
||||
│
|
||||
@@ -691,6 +704,7 @@ aria/ ← Gitea Repo — hier wird entwickelt
|
||||
│ │ └── gitea/
|
||||
│ ├── voices/.gitkeep ← ignoriert — große Binärdateien
|
||||
│ └── config/
|
||||
│ ├── aria.env.example ← Vorlage → kopieren nach aria.env
|
||||
│ ├── AGENT.md ← ARIAs Persönlichkeit & Regeln
|
||||
│ ├── USER.md ← Stefans Präferenzen
|
||||
│ └── TOOLING.md ← Liste installierter VM-Tools
|
||||
|
||||
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.
BIN
Binary file not shown.
+4
-1
@@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
-2
File diff suppressed because one or more lines are too long
+13
@@ -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"
|
||||
|
||||
+8
-2
File diff suppressed because one or more lines are too long
+13
@@ -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
-1
@@ -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
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
BIN
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
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
+34
@@ -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",
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"@react-native-community/geolocation": "^3.2.1",
|
||||
"react-native-image-picker": "^7.1.0",
|
||||
"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": {
|
||||
"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) {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* typisierte Nachrichten.
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
// --- Typen ---
|
||||
|
||||
export type ConnectionState = 'connecting' | 'connected' | 'disconnected';
|
||||
@@ -232,12 +234,13 @@ class RVSConnection {
|
||||
this.messageListeners.forEach(cb => cb(message));
|
||||
}
|
||||
|
||||
// --- Persistenz (AsyncStorage Wrapper) ---
|
||||
// --- Persistenz ---
|
||||
|
||||
private static readonly STORAGE_KEY = 'rvs_config';
|
||||
|
||||
private async saveConfig(config: ConnectionConfig): Promise<void> {
|
||||
try {
|
||||
// In Produktion: AsyncStorage verwenden
|
||||
// await AsyncStorage.setItem('rvs_config', JSON.stringify(config));
|
||||
await AsyncStorage.setItem(RVSConnection.STORAGE_KEY, JSON.stringify(config));
|
||||
console.log('[RVS] Konfiguration gespeichert');
|
||||
} catch (err) {
|
||||
console.error('[RVS] Fehler beim Speichern:', err);
|
||||
@@ -246,9 +249,12 @@ class RVSConnection {
|
||||
|
||||
async loadConfig(): Promise<ConnectionConfig | null> {
|
||||
try {
|
||||
// In Produktion: AsyncStorage verwenden
|
||||
// const data = await AsyncStorage.getItem('rvs_config');
|
||||
// if (data) { this.config = JSON.parse(data); return this.config; }
|
||||
const data = await AsyncStorage.getItem(RVSConnection.STORAGE_KEY);
|
||||
if (data) {
|
||||
this.config = JSON.parse(data);
|
||||
console.log('[RVS] Konfiguration geladen');
|
||||
return this.config;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('[RVS] Fehler beim Laden:', err);
|
||||
|
||||
@@ -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
|
||||
@@ -9,6 +9,7 @@ FROM python:3.12-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
libsndfile1 \
|
||||
libportaudio2 \
|
||||
pulseaudio-utils \
|
||||
alsa-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
+113
-29
@@ -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")
|
||||
@@ -296,22 +299,49 @@ class STTEngine:
|
||||
|
||||
|
||||
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
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.model: Optional[WakeWordModel] = None
|
||||
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...")
|
||||
self.model = WakeWordModel(
|
||||
wakeword_models=[self.WAKE_WORD],
|
||||
inference_framework="onnx",
|
||||
|
||||
# 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
|
||||
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:
|
||||
"""Prueft ob das Wake-Word im Audio-Chunk enthalten ist.
|
||||
@@ -330,7 +360,7 @@ class WakeWordDetector:
|
||||
# openwakeword gibt Scores pro Modell zurueck
|
||||
for key, score in prediction.items():
|
||||
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 False
|
||||
@@ -379,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
|
||||
|
||||
@@ -402,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)
|
||||
@@ -493,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)
|
||||
|
||||
@@ -534,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
|
||||
@@ -565,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:
|
||||
@@ -689,7 +766,7 @@ class ARIABridge:
|
||||
|
||||
async def audio_loop(self) -> None:
|
||||
"""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()
|
||||
|
||||
@@ -739,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:
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@ services:
|
||||
|
||||
# ─── OpenClaw (ARIA Gehirn) ─────────────────────────────
|
||||
aria:
|
||||
image: openclaw/openclaw:latest
|
||||
image: ghcr.io/openclaw/openclaw:latest
|
||||
container_name: aria-core
|
||||
privileged: true # ARIAs Wohnung — sie hat die Schlüssel
|
||||
depends_on:
|
||||
@@ -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:
|
||||
|
||||
Executable
+32
@@ -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
|
||||
Reference in New Issue
Block a user