Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5f1bf6d2c | |||
| afcd45d32f |
@@ -43,6 +43,24 @@ 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
|
||||||
|
|
||||||
|
**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
|
||||||
|
|||||||
@@ -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) ─────────────────────────────
|
# ─── 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:
|
||||||
@@ -405,7 +405,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 +426,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 +689,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 +703,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.
Binary file not shown.
Binary file not shown.
@@ -21,7 +21,8 @@
|
|||||||
"@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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 \
|
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/*
|
||||||
|
|||||||
+35
-12
@@ -296,22 +296,45 @@ 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."""
|
||||||
logger.info("Lade Wake-Word-Modell...")
|
logger.info("Lade Wake-Word-Modell...")
|
||||||
self.model = WakeWordModel(
|
|
||||||
wakeword_models=[self.WAKE_WORD],
|
custom_path = Path(self.CUSTOM_MODEL_PATH)
|
||||||
inference_framework="onnx",
|
if custom_path.exists():
|
||||||
)
|
# Custom "aria" Modell vorhanden
|
||||||
logger.info("Wake-Word-Modell geladen (Trigger: '%s')", self.WAKE_WORD)
|
self.model = WakeWordModel(
|
||||||
|
wakeword_models=[str(custom_path)],
|
||||||
|
)
|
||||||
|
self.wake_word_key = custom_path.stem
|
||||||
|
logger.info("Custom Wake-Word-Modell geladen: %s", custom_path)
|
||||||
|
else:
|
||||||
|
# Fallback auf eingebautes Modell
|
||||||
|
self.model = WakeWordModel()
|
||||||
|
self.wake_word_key = self.FALLBACK_MODEL
|
||||||
|
logger.warning(
|
||||||
|
"Kein Custom-Modell (%s) — nutze Fallback '%s'",
|
||||||
|
self.CUSTOM_MODEL_PATH,
|
||||||
|
self.FALLBACK_MODEL,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Tipp: Custom Wake-Word trainieren → "
|
||||||
|
"https://github.com/dscripka/openWakeWord#training-new-models"
|
||||||
|
)
|
||||||
|
|
||||||
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.
|
||||||
@@ -328,10 +351,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
|
||||||
for key, score in prediction.items():
|
score = prediction.get(self.wake_word_key, 0)
|
||||||
if score > self.THRESHOLD:
|
if score > self.THRESHOLD:
|
||||||
logger.info("Wake-Word erkannt! (Score: %.2f)", score)
|
logger.info("Wake-Word erkannt! (Score: %.2f)", score)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -689,7 +712,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()
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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:
|
||||||
|
|||||||
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