Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 056b579c47 | |||
| 576e612cd0 | |||
| c2faa06a15 | |||
| d3ed3556eb | |||
| d960d125c0 | |||
| 89d5d7ec0a | |||
| ea0c13936b | |||
| 773c976822 | |||
| cd05ed2379 | |||
| 054e4057d8 | |||
| 3943e79bb1 | |||
| 87f4317c15 | |||
| 50aa793910 | |||
| 5efc9865a8 | |||
| 949c573c49 | |||
| f7f450a09d | |||
| 81f7c38383 | |||
| 2c785cb37a | |||
| 57e65b061c | |||
| aa54765b03 | |||
| 8929bc99bb | |||
| 0428c06612 | |||
| a7eb3cf433 | |||
| e4e0e793a8 | |||
| b3d3b8b6bc | |||
| 06bc456221 | |||
| 3461f45207 | |||
| a17d4acc13 | |||
| 62fd9193a1 | |||
| 2329645df4 | |||
| 8a435ddf6c | |||
| 25b754ba31 | |||
| b734593bf2 | |||
| 16847ce6f7 | |||
| 6300829317 | |||
| a1e1ee31bd | |||
| 7ed70b876d |
+37
-7
@@ -1,20 +1,50 @@
|
|||||||
# ARIA Environment Configuration
|
# ════════════════════════════════════════════════
|
||||||
# Copy to .env and fill in values
|
# ARIA — Umgebungsvariablen
|
||||||
|
# Kopieren nach .env und Werte eintragen
|
||||||
|
# ════════════════════════════════════════════════
|
||||||
|
|
||||||
# Auth token for ARIA Core (generate a long random string)
|
# ── ARIA Auth Token ──────────────────────────────
|
||||||
# openssl rand -hex 32
|
# Authentifizierung fuer den OpenClaw Gateway (aria-core).
|
||||||
|
# Wird von Diagnostic, Bridge und App genutzt um sich am Gateway anzumelden.
|
||||||
|
# Alle Services die mit aria-core kommunizieren brauchen diesen Token.
|
||||||
|
# Generieren: openssl rand -hex 32
|
||||||
ARIA_AUTH_TOKEN=change-me-to-a-long-random-string
|
ARIA_AUTH_TOKEN=change-me-to-a-long-random-string
|
||||||
|
|
||||||
# RVS — Rendezvous-Server (Bridge + App verbinden sich hierüber)
|
# ── RVS — Rendezvous-Server ─────────────────────
|
||||||
|
# Der RVS ist ein WebSocket-Relay im Rechenzentrum.
|
||||||
|
# App, Bridge, Diagnostic und XTTS-Bridge verbinden sich hierueber.
|
||||||
|
# Alle muessen den gleichen Host, Port und Token nutzen.
|
||||||
|
|
||||||
|
# Hostname des RVS-Servers (z.B. rvs.example.de oder mobil.hacker-net.de)
|
||||||
RVS_HOST=rvs.example.de
|
RVS_HOST=rvs.example.de
|
||||||
|
|
||||||
|
# Port auf dem der RVS laeuft (muss mit rvs/docker-compose.yml uebereinstimmen)
|
||||||
RVS_PORT=443
|
RVS_PORT=443
|
||||||
|
|
||||||
|
# TLS (wss://) verwenden? true = verschluesselt, false = unverschluesselt (ws://)
|
||||||
RVS_TLS=true
|
RVS_TLS=true
|
||||||
|
|
||||||
# Bei TLS-Fehler automatisch auf ws:// (ohne TLS) fallback?
|
# Bei TLS-Fehler automatisch auf ws:// (ohne TLS) fallback?
|
||||||
# true = Fallback erlaubt, false = nur mit TLS verbinden
|
# Nuetzlich wenn kein TLS-Zertifikat vorhanden (z.B. Entwicklung)
|
||||||
RVS_TLS_FALLBACK=true
|
RVS_TLS_FALLBACK=true
|
||||||
|
|
||||||
|
# Pairing-Token: Wer den gleichen Token hat, landet im gleichen RVS-Room.
|
||||||
|
# Wird von generate-token.sh automatisch generiert und hier eingetragen.
|
||||||
|
# Die Android App bekommt den Token per QR-Code beim Pairing.
|
||||||
|
# WICHTIG: Muss auf ARIA-VM, Gaming-PC (xtts/.env) und App identisch sein!
|
||||||
|
# Generieren: ./generate-token.sh (traegt den Token automatisch ein)
|
||||||
RVS_TOKEN=
|
RVS_TOKEN=
|
||||||
|
|
||||||
# Gitea (for release.sh — Kennwort wird interaktiv abgefragt)
|
# ── Gitea — Release-Verwaltung ───────────────────
|
||||||
|
# Wird von release.sh genutzt um APKs auf Gitea zu veroeffentlichen.
|
||||||
|
# Kennwort wird beim Release interaktiv abgefragt (nicht in .env!).
|
||||||
GITEA_URL=https://git.hacker-net.de
|
GITEA_URL=https://git.hacker-net.de
|
||||||
GITEA_REPO=Hacker-Software/ARIA-AGENT
|
GITEA_REPO=Hacker-Software/ARIA-AGENT
|
||||||
GITEA_USER=duffyduck
|
GITEA_USER=duffyduck
|
||||||
|
|
||||||
|
# ── Auto-Update — APK auf RVS-Server kopieren ───
|
||||||
|
# SSH-Ziel fuer scp: release.sh kopiert die APK dorthin.
|
||||||
|
# Der RVS-Server stellt sie dann per WebSocket an die App bereit.
|
||||||
|
# Format: user@host (z.B. root@aria-rvs oder root@rvs.example.de)
|
||||||
|
# Leer lassen = Auto-Update ueberspringen, APK manuell auf RVS kopieren.
|
||||||
|
RVS_UPDATE_HOST=
|
||||||
|
|||||||
@@ -103,16 +103,31 @@ cd ~/ARIA-AGENT
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
`.env` Datei editieren:
|
`.env` Datei editieren (Details siehe `.env.example`):
|
||||||
```bash
|
```bash
|
||||||
|
# Gateway-Auth: Alle Services die mit aria-core reden brauchen diesen Token
|
||||||
|
# Diagnostic, Bridge, App nutzen ihn fuer den WebSocket-Handshake
|
||||||
ARIA_AUTH_TOKEN= # openssl rand -hex 32
|
ARIA_AUTH_TOKEN= # openssl rand -hex 32
|
||||||
|
|
||||||
|
# RVS-Verbindung: Hostname + Port deines Rendezvous-Servers
|
||||||
RVS_HOST= # z.B. rvs.hackersoft.de
|
RVS_HOST= # z.B. rvs.hackersoft.de
|
||||||
RVS_PORT=443
|
RVS_PORT=443
|
||||||
RVS_TLS=true
|
RVS_TLS=true
|
||||||
RVS_TLS_FALLBACK=true
|
RVS_TLS_FALLBACK=true
|
||||||
RVS_TOKEN= # wird von generate-token.sh automatisch gesetzt
|
|
||||||
|
# Pairing-Token: Verbindet App, Bridge, Diagnostic und XTTS im gleichen RVS-Room
|
||||||
|
# MUSS auf allen Geraeten identisch sein (ARIA-VM, Gaming-PC, App)
|
||||||
|
# Wird von generate-token.sh automatisch generiert und eingetragen
|
||||||
|
RVS_TOKEN= # ./generate-token.sh
|
||||||
|
|
||||||
|
# Optional: SSH-Host des RVS-Servers fuer Auto-Update (z.B. root@aria-rvs)
|
||||||
|
RVS_UPDATE_HOST=
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Zwei Tokens, zwei Zwecke:**
|
||||||
|
- **ARIA_AUTH_TOKEN**: Authentifizierung am OpenClaw Gateway (aria-core). Wer diesen Token hat, kann ARIA Befehle geben.
|
||||||
|
- **RVS_TOKEN**: Pairing-Token fuer den Rendezvous-Server. Alle Geraete mit dem gleichen Token landen im gleichen "Room" und koennen kommunizieren. Die App bekommt diesen Token per QR-Code.
|
||||||
|
|
||||||
### 2. Claude CLI einloggen (Proxy-Auth)
|
### 2. Claude CLI einloggen (Proxy-Auth)
|
||||||
|
|
||||||
Der Proxy-Container nutzt deine Claude Max Subscription. Die Credentials muessen
|
Der Proxy-Container nutzt deine Claude Max Subscription. Die Credentials muessen
|
||||||
@@ -530,38 +545,68 @@ cp ARIA-v0.0.3.0.apk ~/ARIA-AGENT/rvs/updates/
|
|||||||
## XTTS v2 — GPU TTS Server (optional)
|
## XTTS v2 — GPU TTS Server (optional)
|
||||||
|
|
||||||
Laeuft auf einem separaten Rechner mit NVIDIA GPU (z.B. Gaming-PC mit RTX 3060).
|
Laeuft auf einem separaten Rechner mit NVIDIA GPU (z.B. Gaming-PC mit RTX 3060).
|
||||||
Verbindet sich ueber RVS mit der ARIA-Infrastruktur — kein VPN noetig.
|
Verbindet sich ueber RVS mit der ARIA-Infrastruktur — kein VPN noetig, funktioniert
|
||||||
|
ueber verschiedene Netze hinweg.
|
||||||
|
|
||||||
|
### Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
Gaming-PC (Windows, RTX 3060, Docker Desktop + WSL2)
|
||||||
|
├── aria-xtts XTTS v2 GPU Server (Port 8020 intern)
|
||||||
|
└── aria-xtts-bridge RVS-Relay (empfaengt Requests, sendet Audio)
|
||||||
|
└── Beide teilen ./voices/ Volume fuer Voice Cloning
|
||||||
|
|
||||||
|
↕ RVS (Rechenzentrum, WebSocket Relay)
|
||||||
|
|
||||||
|
ARIA-VM
|
||||||
|
└── aria-bridge: tts_engine="xtts" → xtts_request via RVS → wartet auf xtts_response
|
||||||
|
```
|
||||||
|
|
||||||
### Voraussetzungen
|
### Voraussetzungen
|
||||||
|
|
||||||
- Docker Desktop mit WSL2 (Windows) oder Docker mit NVIDIA Runtime (Linux)
|
- Docker Desktop mit WSL2 (Windows) oder Docker mit NVIDIA Runtime (Linux)
|
||||||
- NVIDIA Container Toolkit
|
- NVIDIA Container Toolkit
|
||||||
- GPU mit mindestens 4GB VRAM (6GB+ empfohlen)
|
- GPU mit mindestens 4GB VRAM (6GB+ empfohlen)
|
||||||
|
- **Gleicher RVS_TOKEN wie auf der ARIA-VM!**
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd xtts
|
cd xtts
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# .env mit RVS-Verbindungsdaten fuellen (gleiche wie auf der ARIA-VM)
|
# .env mit RVS-Verbindungsdaten fuellen (gleicher Token wie ARIA-VM!)
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
# Erster Start laedt ~2GB Model herunter
|
# Erster Start laedt ~2GB Model herunter (danach gecacht)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Wichtig:** Der XTTS-Server laeuft intern auf Port **8020** (nicht 8000).
|
||||||
|
Das Model wird im Volume `xtts-models` gecacht und muss nur einmal geladen werden.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **Natuerliche Stimmen**: Deutlich bessere Qualitaet als Piper
|
- **Natuerliche Stimmen**: Deutlich bessere Qualitaet als Piper
|
||||||
- **Voice Cloning**: Eigene Stimme mit 6-10s Audio-Sample
|
- **Voice Cloning**: Eigene Stimme mit 6-10s Audio-Sample (~2s Latenz auf RTX 3060)
|
||||||
- **16 Sprachen**: Deutsch, Englisch, Franzoesisch, etc.
|
- **16 Sprachen**: Deutsch, Englisch, Franzoesisch, etc.
|
||||||
- **RVS-Integration**: Bridge waehlt automatisch XTTS wenn verfuegbar
|
- **Fallback**: Wenn XTTS nicht erreichbar, nutzt die Bridge automatisch Piper
|
||||||
|
|
||||||
|
### TTS-Engine umschalten
|
||||||
|
|
||||||
|
In der Diagnostic unter Einstellungen → Sprachausgabe:
|
||||||
|
- **TTS aktiv**: Global An/Aus
|
||||||
|
- **TTS Engine**: Piper (lokal, CPU, schnell) oder XTTS v2 (remote, GPU, natuerlich)
|
||||||
|
- **Piper**: Standard-Stimme, Highlight-Stimme, Speed pro Stimme
|
||||||
|
- **XTTS**: Stimmen-Auswahl, Voice Cloning
|
||||||
|
|
||||||
### Stimme klonen
|
### Stimme klonen
|
||||||
|
|
||||||
In der Diagnostic unter Einstellungen → Sprachausgabe → XTTS:
|
|
||||||
1. TTS Engine auf "XTTS v2" stellen
|
1. TTS Engine auf "XTTS v2" stellen
|
||||||
2. "Stimme klonen" → Audio-Dateien hochladen (WAV/MP3, min. 6-10s)
|
2. "Stimme klonen" → Audio-Dateien hochladen (WAV/MP3, 1-10 Dateien, min. 6-10s gesamt)
|
||||||
3. Name vergeben → "Stimme erstellen"
|
3. Name vergeben → "Stimme erstellen"
|
||||||
4. Neue Stimme in der Auswahl verfuegbar
|
4. "Laden" klicken → neue Stimme in der Auswahl
|
||||||
|
5. Stimme auswaehlen → Config wird automatisch gespeichert
|
||||||
|
|
||||||
|
> **Tipp:** Fuer beste Ergebnisse: saubere Aufnahme, eine Stimme, kein Hintergrund,
|
||||||
|
> 10-30 Sekunden Gesamtlaenge. Mehrere kurze Dateien werden zusammengefuegt.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -633,6 +678,8 @@ docker exec aria-core ssh aria-wohnung hostname
|
|||||||
- **Wake Word nur auf VM**: Die Bridge hoert auf "ARIA" ueber das lokale Mikrofon der VM.
|
- **Wake Word nur auf VM**: Die Bridge hoert auf "ARIA" ueber das lokale Mikrofon der VM.
|
||||||
In der App gibt es Energy-basierte Erkennung (Phase 1). On-device "ARIA"-Keyword (Porcupine) ist Phase 2.
|
In der App gibt es Energy-basierte Erkennung (Phase 1). On-device "ARIA"-Keyword (Porcupine) ist Phase 2.
|
||||||
- **Audio-Format**: App nimmt AAC/MP4 auf, Bridge konvertiert via FFmpeg zu 16kHz PCM.
|
- **Audio-Format**: App nimmt AAC/MP4 auf, Bridge konvertiert via FFmpeg zu 16kHz PCM.
|
||||||
|
- **RVS Zombie-Connections**: WebSocket-Verbindungen sterben gelegentlich ohne Fehlermeldung.
|
||||||
|
Bridge hat Ping-Check (5s), Diagnostic nutzt frische Verbindungen pro Request.
|
||||||
- **Bildanalyse eingeschraenkt**: Bilder werden in `/shared/uploads/` gespeichert. ARIA kann
|
- **Bildanalyse eingeschraenkt**: Bilder werden in `/shared/uploads/` gespeichert. ARIA kann
|
||||||
sie per Bash/Read-Tool oeffnen, aber Claude Vision (direkte Bildanalyse) ist ueber den
|
sie per Bash/Read-Tool oeffnen, aber Claude Vision (direkte Bildanalyse) ist ueber den
|
||||||
Proxy-Pfad (`claude --print`) noch nicht moeglich. ARIA sieht den Dateipfad, nicht das Bild.
|
Proxy-Pfad (`claude --print`) noch nicht moeglich. ARIA sieht den Dateipfad, nicht das Bild.
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 205
|
versionCode 302
|
||||||
versionName "0.0.2.5"
|
versionName "0.0.3.2"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
@@ -24,5 +25,15 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ariacockpit
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ApkInstallerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||||
|
override fun getName() = "ApkInstaller"
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun install(filePath: String, promise: Promise) {
|
||||||
|
try {
|
||||||
|
val file = File(filePath)
|
||||||
|
if (!file.exists()) {
|
||||||
|
promise.reject("FILE_NOT_FOUND", "APK nicht gefunden: $filePath")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = reactApplicationContext
|
||||||
|
val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||||
|
} else {
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
promise.resolve(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
promise.reject("INSTALL_ERROR", e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ariacockpit
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage
|
||||||
|
import com.facebook.react.bridge.NativeModule
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.uimanager.ViewManager
|
||||||
|
|
||||||
|
class ApkInstallerPackage : ReactPackage {
|
||||||
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||||
|
return listOf(ApkInstallerModule(reactContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,7 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
object : DefaultReactNativeHost(this) {
|
object : DefaultReactNativeHost(this) {
|
||||||
override fun getPackages(): List<ReactPackage> =
|
override fun getPackages(): List<ReactPackage> =
|
||||||
PackageList(this).packages.apply {
|
PackageList(this).packages.apply {
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
add(ApkInstallerPackage())
|
||||||
// add(MyReactNativePackage())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJSMainModuleName(): String = "index"
|
override fun getJSMainModuleName(): String = "index"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="cache" path="." />
|
||||||
|
</paths>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.0.2.5",
|
"version": "0.0.3.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -748,11 +748,21 @@ const SettingsScreen: React.FC = () => {
|
|||||||
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
|
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
||||||
<Text style={styles.aboutVersion}>Version 0.0.2.5 </Text>
|
<Text style={styles.aboutVersion}>Version {require('../../package.json').version}</Text>
|
||||||
<Text style={styles.aboutInfo}>
|
<Text style={styles.aboutInfo}>
|
||||||
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
|
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
|
||||||
Gebaut mit React Native + TypeScript.
|
Gebaut mit React Native + TypeScript.
|
||||||
</Text>
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.connectButton, {marginTop: 12}]}
|
||||||
|
onPress={() => {
|
||||||
|
const updateService = require('../services/updater').default;
|
||||||
|
updateService.checkForUpdate();
|
||||||
|
Alert.alert('Update-Check', 'Pruefe auf neue Version...');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.connectButtonText}>Auf Updates pr{'\u00FC'}fen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Platz am Ende */}
|
{/* Platz am Ende */}
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ class AudioService {
|
|||||||
private recorder: AudioRecorderPlayer;
|
private recorder: AudioRecorderPlayer;
|
||||||
private recordingPath: string = '';
|
private recordingPath: string = '';
|
||||||
|
|
||||||
|
// Audio-Queue fuer sequentielle TTS-Wiedergabe
|
||||||
|
private audioQueue: string[] = [];
|
||||||
|
private isPlaying: boolean = false;
|
||||||
|
private preloadedSound: Sound | null = null;
|
||||||
|
private preloadedPath: string = '';
|
||||||
|
|
||||||
// VAD State
|
// VAD State
|
||||||
private vadEnabled: boolean = false;
|
private vadEnabled: boolean = false;
|
||||||
private lastSpeechTime: number = 0;
|
private lastSpeechTime: number = 0;
|
||||||
@@ -198,47 +204,98 @@ class AudioService {
|
|||||||
|
|
||||||
// --- Wiedergabe ---
|
// --- Wiedergabe ---
|
||||||
|
|
||||||
/** Base64-kodiertes Audio abspielen (z.B. TTS-Antwort von ARIA) */
|
/** Base64-kodiertes Audio in die Queue stellen und abspielen */
|
||||||
async playAudio(base64Data: string): Promise<void> {
|
async playAudio(base64Data: string): Promise<void> {
|
||||||
if (!base64Data) return;
|
if (!base64Data) return;
|
||||||
|
|
||||||
// Laufende Wiedergabe stoppen
|
this.audioQueue.push(base64Data);
|
||||||
this.stopPlayback();
|
if (!this.isPlaying) {
|
||||||
|
this._playNext();
|
||||||
try {
|
|
||||||
// Base64 -> temporaere WAV-Datei -> Sound abspielen
|
|
||||||
const tmpPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`;
|
|
||||||
await RNFS.writeFile(tmpPath, base64Data, 'base64');
|
|
||||||
|
|
||||||
this.currentSound = new Sound(tmpPath, '', (error) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('[Audio] Fehler beim Laden:', error);
|
|
||||||
RNFS.unlink(tmpPath).catch(() => {});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.currentSound?.play((success) => {
|
|
||||||
if (success) {
|
|
||||||
console.log('[Audio] Wiedergabe abgeschlossen');
|
|
||||||
} else {
|
|
||||||
console.warn('[Audio] Wiedergabe fehlgeschlagen');
|
|
||||||
}
|
|
||||||
this.currentSound?.release();
|
|
||||||
this.currentSound = null;
|
|
||||||
RNFS.unlink(tmpPath).catch(() => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Audio] Wiedergabefehler:', err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Laufende Wiedergabe stoppen */
|
/** Naechstes Audio aus der Queue abspielen */
|
||||||
|
private async _playNext(): Promise<void> {
|
||||||
|
if (this.audioQueue.length === 0) {
|
||||||
|
this.isPlaying = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isPlaying = true;
|
||||||
|
|
||||||
|
// Preloaded Sound verwenden wenn verfuegbar, sonst neu laden
|
||||||
|
let sound: Sound;
|
||||||
|
let soundPath: string;
|
||||||
|
|
||||||
|
if (this.preloadedSound) {
|
||||||
|
sound = this.preloadedSound;
|
||||||
|
soundPath = this.preloadedPath;
|
||||||
|
this.preloadedSound = null;
|
||||||
|
this.preloadedPath = '';
|
||||||
|
// Daten aus Queue entfernen (wurde schon preloaded)
|
||||||
|
this.audioQueue.shift();
|
||||||
|
} else {
|
||||||
|
const base64Data = this.audioQueue.shift()!;
|
||||||
|
try {
|
||||||
|
soundPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`;
|
||||||
|
await RNFS.writeFile(soundPath, base64Data, 'base64');
|
||||||
|
sound = await new Promise<Sound>((resolve, reject) => {
|
||||||
|
const s = new Sound(soundPath, '', (err) => err ? reject(err) : resolve(s));
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Audio] Laden fehlgeschlagen:', err);
|
||||||
|
this._playNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSound = sound;
|
||||||
|
|
||||||
|
// Naechstes Audio schon vorbereiten waehrend dieses abspielt
|
||||||
|
this._preloadNext();
|
||||||
|
|
||||||
|
sound.play((success) => {
|
||||||
|
if (!success) console.warn('[Audio] Wiedergabe fehlgeschlagen');
|
||||||
|
sound.release();
|
||||||
|
this.currentSound = null;
|
||||||
|
RNFS.unlink(soundPath).catch(() => {});
|
||||||
|
this._playNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Naechstes Audio im Hintergrund vorladen (verhindert Stottern) */
|
||||||
|
private async _preloadNext(): Promise<void> {
|
||||||
|
if (this.audioQueue.length === 0 || this.preloadedSound) return;
|
||||||
|
|
||||||
|
const base64Data = this.audioQueue[0]; // Nicht shift — bleibt in Queue
|
||||||
|
try {
|
||||||
|
const tmpPath = `${RNFS.CachesDirectoryPath}/aria_tts_pre_${Date.now()}.wav`;
|
||||||
|
await RNFS.writeFile(tmpPath, base64Data, 'base64');
|
||||||
|
this.preloadedSound = await new Promise<Sound>((resolve, reject) => {
|
||||||
|
const s = new Sound(tmpPath, '', (err) => err ? reject(err) : resolve(s));
|
||||||
|
});
|
||||||
|
this.preloadedPath = tmpPath;
|
||||||
|
} catch {
|
||||||
|
this.preloadedSound = null;
|
||||||
|
this.preloadedPath = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Laufende Wiedergabe stoppen + Queue leeren */
|
||||||
stopPlayback(): void {
|
stopPlayback(): void {
|
||||||
|
this.audioQueue = [];
|
||||||
|
this.isPlaying = false;
|
||||||
if (this.currentSound) {
|
if (this.currentSound) {
|
||||||
this.currentSound.stop();
|
this.currentSound.stop();
|
||||||
this.currentSound.release();
|
this.currentSound.release();
|
||||||
this.currentSound = null;
|
this.currentSound = null;
|
||||||
}
|
}
|
||||||
|
if (this.preloadedSound) {
|
||||||
|
this.preloadedSound.release();
|
||||||
|
this.preloadedSound = null;
|
||||||
|
if (this.preloadedPath) RNFS.unlink(this.preloadedPath).catch(() => {});
|
||||||
|
this.preloadedPath = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Status & Callbacks ---
|
// --- Status & Callbacks ---
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
* 3. App zeigt Benachrichtigung → User bestaetigt → Download + Install
|
* 3. App zeigt Benachrichtigung → User bestaetigt → Download + Install
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Alert, Linking, Platform } from 'react-native';
|
import { Alert, Linking, Platform, NativeModules } from 'react-native';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import rvs, { RVSMessage } from './rvs';
|
import rvs, { RVSMessage } from './rvs';
|
||||||
|
|
||||||
// Aktuelle App-Version (aus package.json via Build)
|
// Version aus package.json (wird beim Build eingebettet)
|
||||||
const APP_VERSION = '0.0.2.3'; // TODO: aus nativer Build-Config lesen
|
const packageJson = require('../../package.json');
|
||||||
|
const APP_VERSION = packageJson.version || '0.0.0.0';
|
||||||
|
|
||||||
type UpdateCallback = (info: UpdateInfo) => void;
|
type UpdateCallback = (info: UpdateInfo) => void;
|
||||||
|
|
||||||
@@ -116,9 +117,17 @@ class UpdateService {
|
|||||||
const fileSize = await RNFS.stat(destPath);
|
const fileSize = await RNFS.stat(destPath);
|
||||||
console.log(`[Update] APK gespeichert: ${destPath} (${(parseInt(fileSize.size) / 1024 / 1024).toFixed(1)}MB)`);
|
console.log(`[Update] APK gespeichert: ${destPath} (${(parseInt(fileSize.size) / 1024 / 1024).toFixed(1)}MB)`);
|
||||||
|
|
||||||
// APK installieren (oeffnet Android-Installer)
|
// APK installieren via natives ApkInstaller Module (FileProvider + Intent)
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
await Linking.openURL(`file://${destPath}`);
|
try {
|
||||||
|
const { ApkInstaller } = NativeModules;
|
||||||
|
await ApkInstaller.install(destPath);
|
||||||
|
} catch (installErr: any) {
|
||||||
|
Alert.alert(
|
||||||
|
'APK heruntergeladen',
|
||||||
|
`Version ${info.version} gespeichert.\n\nBitte manuell installieren:\nDateimanager → ${apkData.fileName} antippen.\n\n(${installErr.message})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`[Update] Fehler: ${err.message}`);
|
console.error(`[Update] Fehler: ${err.message}`);
|
||||||
|
|||||||
+21
-2
@@ -851,7 +851,7 @@ class ARIABridge:
|
|||||||
tts_engine = getattr(self, 'tts_engine_type', 'piper')
|
tts_engine = getattr(self, 'tts_engine_type', 'piper')
|
||||||
|
|
||||||
if tts_engine == "xtts":
|
if tts_engine == "xtts":
|
||||||
# XTTS: Request ueber RVS an Gaming-PC senden
|
# XTTS: Ganzen Text senden, XTTS-Bridge teilt satzweise auf
|
||||||
xtts_voice = getattr(self, 'xtts_voice', '')
|
xtts_voice = getattr(self, 'xtts_voice', '')
|
||||||
try:
|
try:
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
@@ -1045,6 +1045,11 @@ class ARIABridge:
|
|||||||
sender = payload.get("sender", "")
|
sender = payload.get("sender", "")
|
||||||
if sender in ("aria", "stt"):
|
if sender in ("aria", "stt"):
|
||||||
return
|
return
|
||||||
|
text = payload.get("text", "")
|
||||||
|
if text:
|
||||||
|
logger.info("[rvs] App-Chat: '%s'", text[:80])
|
||||||
|
await self.send_to_core(text, source="app")
|
||||||
|
return
|
||||||
|
|
||||||
elif msg_type == "xtts_response":
|
elif msg_type == "xtts_response":
|
||||||
# XTTS-Audio vom Gaming-PC empfangen → an App weiterleiten
|
# XTTS-Audio vom Gaming-PC empfangen → an App weiterleiten
|
||||||
@@ -1354,10 +1359,24 @@ class ARIABridge:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def _send_to_rvs(self, message: dict) -> None:
|
async def _send_to_rvs(self, message: dict) -> None:
|
||||||
"""Sendet eine Nachricht an die App (via RVS)."""
|
"""Sendet eine Nachricht an die App (via RVS) mit Verbindungs-Check."""
|
||||||
if self.ws_rvs is None:
|
if self.ws_rvs is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Ping-Check: Verbindung wirklich aktiv?
|
||||||
|
try:
|
||||||
|
pong = await self.ws_rvs.ping()
|
||||||
|
await asyncio.wait_for(pong, timeout=5)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("[rvs] Ping fehlgeschlagen — Verbindung tot, erzwinge Reconnect")
|
||||||
|
try:
|
||||||
|
await self.ws_rvs.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.ws_rvs = None
|
||||||
|
# Reconnect wird vom connect_to_rvs Loop uebernommen
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.ws_rvs.send(json.dumps(message))
|
await self.ws_rvs.send(json.dumps(message))
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
+48
-22
@@ -401,6 +401,12 @@
|
|||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h2>Sprachausgabe</h2>
|
<h2>Sprachausgabe</h2>
|
||||||
<div class="card" style="max-width:500px;">
|
<div class="card" style="max-width:500px;">
|
||||||
|
<!-- TTS aktiv (global fuer alle Engines) -->
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||||
|
<label style="color:#8888AA;font-size:12px;">TTS aktiv:</label>
|
||||||
|
<label class="toggle"><input type="checkbox" id="diag-tts-enabled" checked onchange="sendVoiceConfig()"><span class="slider"></span></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- TTS Engine Auswahl -->
|
<!-- TTS Engine Auswahl -->
|
||||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||||
<label style="color:#8888AA;font-size:12px;">TTS Engine:</label>
|
<label style="color:#8888AA;font-size:12px;">TTS Engine:</label>
|
||||||
@@ -426,10 +432,6 @@
|
|||||||
<option value="ramona">Ramona (weiblich)</option>
|
<option value="ramona">Ramona (weiblich)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
||||||
<label style="color:#8888AA;font-size:12px;">TTS aktiv:</label>
|
|
||||||
<label class="toggle"><input type="checkbox" id="diag-tts-enabled" checked onchange="sendVoiceConfig()"><span class="slider"></span></label>
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:4px;">
|
<div style="margin-bottom:4px;">
|
||||||
<label style="color:#8888AA;font-size:12px;">Ramona Speed: <span id="speed-ramona-label">1.0x</span></label>
|
<label style="color:#8888AA;font-size:12px;">Ramona Speed: <span id="speed-ramona-label">1.0x</span></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -744,7 +746,16 @@
|
|||||||
document.getElementById('diag-speed-thorsten').value = st;
|
document.getElementById('diag-speed-thorsten').value = st;
|
||||||
document.getElementById('speed-thorsten-label').textContent = st + 'x';
|
document.getElementById('speed-thorsten-label').textContent = st + 'x';
|
||||||
document.getElementById('diag-tts-engine').value = msg.ttsEngine || 'piper';
|
document.getElementById('diag-tts-engine').value = msg.ttsEngine || 'piper';
|
||||||
document.getElementById('diag-xtts-voice').value = msg.xttsVoice || '';
|
// XTTS-Voice setzen — Option hinzufuegen falls nicht vorhanden
|
||||||
|
const xttsSelect = document.getElementById('diag-xtts-voice');
|
||||||
|
const xttsVoice = msg.xttsVoice || '';
|
||||||
|
if (xttsVoice && !Array.from(xttsSelect.options).some(o => o.value === xttsVoice)) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = xttsVoice;
|
||||||
|
opt.textContent = xttsVoice;
|
||||||
|
xttsSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
xttsSelect.value = xttsVoice;
|
||||||
toggleXTTSPanel();
|
toggleXTTSPanel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1247,7 +1258,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadXTTSVoices() {
|
function loadXTTSVoices() {
|
||||||
sendToRVS_raw({ type: 'xtts_list_voices', payload: {}, timestamp: Date.now() });
|
send({ action: 'xtts_list_voices' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64(buffer) {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i += 8192) {
|
||||||
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadVoiceSamples() {
|
async function uploadVoiceSamples() {
|
||||||
@@ -1255,25 +1275,31 @@
|
|||||||
const files = document.getElementById('xtts-clone-files').files;
|
const files = document.getElementById('xtts-clone-files').files;
|
||||||
if (!name) { alert('Bitte einen Namen eingeben'); return; }
|
if (!name) { alert('Bitte einen Namen eingeben'); return; }
|
||||||
if (!files || files.length === 0) { alert('Bitte Audio-Dateien auswaehlen'); return; }
|
if (!files || files.length === 0) { alert('Bitte Audio-Dateien auswaehlen'); return; }
|
||||||
|
if (files.length > 10) { alert('Maximal 10 Dateien'); return; }
|
||||||
|
|
||||||
document.getElementById('xtts-clone-status').textContent = `Lade ${files.length} Datei(en) hoch...`;
|
const status = document.getElementById('xtts-clone-status');
|
||||||
|
status.textContent = `Lade ${files.length} Datei(en)...`;
|
||||||
|
status.style.color = '#FFD60A';
|
||||||
|
|
||||||
const samples = [];
|
try {
|
||||||
for (const file of files) {
|
const samples = [];
|
||||||
const buffer = await file.arrayBuffer();
|
for (let i = 0; i < files.length; i++) {
|
||||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
status.textContent = `Lese Datei ${i + 1}/${files.length}: ${files[i].name}...`;
|
||||||
samples.push({ base64, name: file.name, size: file.size });
|
const buffer = await files[i].arrayBuffer();
|
||||||
|
const base64 = arrayBufferToBase64(buffer);
|
||||||
|
samples.push({ base64, name: files[i].name, size: files[i].size });
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = samples.reduce((s, f) => s + f.size, 0);
|
||||||
|
status.textContent = `Sende ${samples.length} Sample(s) (${(totalSize / 1024).toFixed(0)}KB)...`;
|
||||||
|
|
||||||
|
send({ action: 'voice_upload', name, samples });
|
||||||
|
|
||||||
|
status.textContent = `Gesendet — warte auf Bestaetigung vom XTTS-Server...`;
|
||||||
|
} catch (err) {
|
||||||
|
status.textContent = `Fehler: ${err.message}`;
|
||||||
|
status.style.color = '#FF3B30';
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSize = samples.reduce((s, f) => s + f.size, 0);
|
|
||||||
document.getElementById('xtts-clone-status').textContent =
|
|
||||||
`Sende ${samples.length} Sample(s) (${(totalSize / 1024).toFixed(0)}KB) an XTTS-Server...`;
|
|
||||||
|
|
||||||
sendToRVS_raw({
|
|
||||||
type: 'voice_upload',
|
|
||||||
payload: { name, samples },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Abbrechen ──────────────────────────────
|
// ── Abbrechen ──────────────────────────────
|
||||||
|
|||||||
@@ -560,6 +560,31 @@ function connectRVS(forcePlain) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendToRVS_withResponse(sendType, sendPayload, expectType, clientWs) {
|
||||||
|
if (!RVS_HOST || !RVS_TOKEN) return;
|
||||||
|
const proto = RVS_TLS === "true" ? "wss" : "ws";
|
||||||
|
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
|
||||||
|
const freshWs = new WebSocket(url);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
try { freshWs.close(); } catch (_) {}
|
||||||
|
clientWs.send(JSON.stringify({ type: expectType, payload: { voices: [], error: "Timeout" }, timestamp: Date.now() }));
|
||||||
|
}, 15000);
|
||||||
|
freshWs.on("open", () => {
|
||||||
|
freshWs.send(JSON.stringify({ type: sendType, payload: sendPayload, timestamp: Date.now() }));
|
||||||
|
});
|
||||||
|
freshWs.on("message", (raw) => {
|
||||||
|
try {
|
||||||
|
const resp = JSON.parse(raw.toString());
|
||||||
|
if (resp.type === expectType) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
clientWs.send(JSON.stringify(resp));
|
||||||
|
setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 1000);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
freshWs.on("error", () => {});
|
||||||
|
}
|
||||||
|
|
||||||
function sendToRVS_raw(msgObj) {
|
function sendToRVS_raw(msgObj) {
|
||||||
if (!RVS_HOST || !RVS_TOKEN) return;
|
if (!RVS_HOST || !RVS_TOKEN) return;
|
||||||
const proto = RVS_TLS === "true" ? "wss" : "ws";
|
const proto = RVS_TLS === "true" ? "wss" : "ws";
|
||||||
@@ -1165,6 +1190,13 @@ wss.on("connection", (ws) => {
|
|||||||
if (pipelineActive) pipelineEnd(false, "Vom Benutzer abgebrochen");
|
if (pipelineActive) pipelineEnd(false, "Vom Benutzer abgebrochen");
|
||||||
broadcast({ type: "agent_activity", activity: "idle" });
|
broadcast({ type: "agent_activity", activity: "idle" });
|
||||||
dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
|
dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
|
||||||
|
} else if (msg.action === "voice_upload") {
|
||||||
|
// Voice-Samples an XTTS-Bridge via RVS weiterleiten, auf Bestätigung warten
|
||||||
|
log("info", "server", `Voice-Upload '${msg.name}' (${(msg.samples || []).length} Samples) sende an RVS...`);
|
||||||
|
sendToRVS_withResponse("voice_upload", { name: msg.name, samples: msg.samples }, "xtts_voice_saved", ws);
|
||||||
|
} else if (msg.action === "xtts_list_voices") {
|
||||||
|
// Frische Verbindung die auf Antwort wartet
|
||||||
|
sendToRVS_withResponse("xtts_list_voices", {}, "xtts_voices_list", ws);
|
||||||
} else if (msg.action === "get_voice_config") {
|
} else if (msg.action === "get_voice_config") {
|
||||||
handleGetVoiceConfig(ws);
|
handleGetVoiceConfig(ws);
|
||||||
} else if (msg.action === "send_voice_config") {
|
} else if (msg.action === "send_voice_config") {
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ services:
|
|||||||
claude-max-api"
|
claude-max-api"
|
||||||
volumes:
|
volumes:
|
||||||
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
||||||
- ./aria-data/ssh:/root/.ssh:ro # SSH Keys fuer VM-Zugriff (aria-wohnung)
|
- ./aria-data/ssh:/root/.ssh # SSH Keys fuer VM-Zugriff (aria-wohnung, rw fuer ARIA)
|
||||||
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
|
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
|
||||||
environment:
|
environment:
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
|
|||||||
@@ -18,19 +18,36 @@
|
|||||||
- [x] RVS Nachrichten vom Smartphone gehen durch
|
- [x] RVS Nachrichten vom Smartphone gehen durch
|
||||||
- [x] Stimmen-Einstellungen (Ramona/Thorsten, Speed pro Stimme)
|
- [x] Stimmen-Einstellungen (Ramona/Thorsten, Speed pro Stimme)
|
||||||
- [x] Highlight-Trigger konfigurierbar in Diagnostic
|
- [x] Highlight-Trigger konfigurierbar in Diagnostic
|
||||||
|
- [x] XTTS v2 Integration (Gaming-PC, GPU, Voice Cloning)
|
||||||
|
- [x] XTTS Voice Cloning (Audio-Samples hochladen, eigene Stimme)
|
||||||
|
- [x] TTS Engine waehlbar (Piper/XTTS) in Diagnostic + App
|
||||||
|
- [x] Auto-Update System (APK via RVS WebSocket)
|
||||||
|
- [x] Audio-Queue (sequentielle Wiedergabe, kein Ueberlappen)
|
||||||
|
|
||||||
## Offen
|
## Offen
|
||||||
|
|
||||||
### TTS / Stimmen
|
### Bugs (Prioritaet)
|
||||||
- [ ] TTS Engine waehlbar: Piper (CPU, schnell) oder Coqui XTTS v2 (GPU, natuerlicher)
|
- [ ] Session-Persistenz: Bei Container-Restart wird immer aria-bridge geladen statt die zuletzt gewaehlte Session. Wird nicht persistent gespeichert.
|
||||||
- [ ] Piper Voices Download ueber Diagnostic (neue Sprachen/Stimmen)
|
- [ ] App: Textnachrichten, Bilder und Anhaenge werden von ARIA nicht beantwortet — nur Sprachnachrichten funktionieren.
|
||||||
- [ ] Coqui XTTS v2 Integration (braucht GPU, bessere deutsche Stimme)
|
- [ ] App: Audioausgabe hoert ab und zu einfach auf (mitten im Satz oder zwischen Chunks)
|
||||||
|
- [ ] Auto-Update: APK-Installation schlaegt fehl (file:// URI exposed beyond app — braucht FileProvider fuer content:// URI)
|
||||||
|
- [ ] Auto-Update: "Auf Updates pruefen" Button in App-Einstellungen
|
||||||
|
- [ ] App: Kein Auto-Scroll zur letzten Nachricht beim App-Start (soll direkt springen, nicht animiert scrollen)
|
||||||
|
- [ ] App: Bei neuen Nachrichten soll automatisch zur letzten Nachricht gescrollt werden
|
||||||
|
|
||||||
### App
|
### App Features
|
||||||
|
- [ ] App: Zu Anhaengen noch Text/Sprache hinzufuegen koennen (z.B. Bild senden + "Was siehst du?")
|
||||||
- [ ] Wake Word on-device (Porcupine "ARIA" Keyword, Phase 2)
|
- [ ] Wake Word on-device (Porcupine "ARIA" Keyword, Phase 2)
|
||||||
- [ ] Chat-History zuverlaessiger laden (AsyncStorage Race Condition)
|
- [ ] Chat-History zuverlaessiger laden (AsyncStorage Race Condition)
|
||||||
|
- [ ] Background Audio Service (TTS auch bei minimierter App)
|
||||||
|
|
||||||
|
### TTS / Audio
|
||||||
|
- [ ] XTTS Audio-Streaming verbessern (minimales Stottern bei Chunk-Uebergaengen)
|
||||||
|
- [ ] Audio-Normalisierung (Lautstaerke zwischen Chunks angleichen)
|
||||||
|
- [ ] Piper Voices Download ueber Diagnostic (neue Sprachen/Stimmen)
|
||||||
|
|
||||||
### Architektur
|
### Architektur
|
||||||
- [ ] Bilder: Claude Vision direkt nutzen (aktuell nur Dateipfad an ARIA)
|
- [ ] Bilder: Claude Vision direkt nutzen (aktuell nur Dateipfad an ARIA)
|
||||||
- [ ] Auto-Compacting und Memory/Brain Verwaltung (SQLite?)
|
- [ ] Auto-Compacting und Memory/Brain Verwaltung (SQLite?)
|
||||||
- [ ] Diagnostic: System-Info Tab (Container-Status, Disk, RAM, CPU)
|
- [ ] Diagnostic: System-Info Tab (Container-Status, Disk, RAM, CPU)
|
||||||
|
- [ ] RVS Zombie-Connections endgueltig loesen (WebRTC statt WebSocket?)
|
||||||
|
|||||||
+7
-2
@@ -76,8 +76,11 @@ echo -e " ${GREEN}✓${NC} SettingsScreen → Version $VERSION"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── APK bauen ─────────────────────────────────
|
# ── APK bauen ─────────────────────────────────
|
||||||
echo -e "${GREEN}[2/5] APK bauen...${NC}"
|
echo -e "${GREEN}[2/5] APK bauen (Cache leeren + Build)...${NC}"
|
||||||
cd android
|
cd android
|
||||||
|
# Metro + Gradle Cache leeren damit neue Version sauber eingebettet wird
|
||||||
|
rm -rf node_modules/.cache 2>/dev/null
|
||||||
|
cd android && ./gradlew clean 2>/dev/null; cd ..
|
||||||
./build.sh release
|
./build.sh release
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
@@ -174,9 +177,11 @@ fi
|
|||||||
RVS_UPDATE_HOST="${RVS_UPDATE_HOST:-}"
|
RVS_UPDATE_HOST="${RVS_UPDATE_HOST:-}"
|
||||||
if [ -n "$RVS_UPDATE_HOST" ]; then
|
if [ -n "$RVS_UPDATE_HOST" ]; then
|
||||||
echo -e "${GREEN}[6/6] APK auf RVS-Server kopieren (Auto-Update)...${NC}"
|
echo -e "${GREEN}[6/6] APK auf RVS-Server kopieren (Auto-Update)...${NC}"
|
||||||
|
# Alte APKs auf dem RVS loeschen, dann neue hochladen
|
||||||
|
ssh "$RVS_UPDATE_HOST" "rm -f ~/ARIA-AGENT/rvs/updates/ARIA-*.apk" 2>/dev/null
|
||||||
scp "$APK_PATH" "${RVS_UPDATE_HOST}:~/ARIA-AGENT/rvs/updates/${APK_NAME}" 2>/dev/null
|
scp "$APK_PATH" "${RVS_UPDATE_HOST}:~/ARIA-AGENT/rvs/updates/${APK_NAME}" 2>/dev/null
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo -e " ${GREEN}✓${NC} APK auf RVS-Server kopiert — Apps werden benachrichtigt"
|
echo -e " ${GREEN}✓${NC} APK auf RVS-Server kopiert (alte Versionen geloescht)"
|
||||||
else
|
else
|
||||||
echo -e " ${YELLOW}APK konnte nicht auf RVS kopiert werden (RVS_UPDATE_HOST=$RVS_UPDATE_HOST)${NC}"
|
echo -e " ${YELLOW}APK konnte nicht auf RVS kopiert werden (RVS_UPDATE_HOST=$RVS_UPDATE_HOST)${NC}"
|
||||||
echo -e " ${YELLOW}Manuell: scp $APK_PATH $RVS_UPDATE_HOST:~/ARIA-AGENT/rvs/updates/${APK_NAME}${NC}"
|
echo -e " ${YELLOW}Manuell: scp $APK_PATH $RVS_UPDATE_HOST:~/ARIA-AGENT/rvs/updates/${APK_NAME}${NC}"
|
||||||
|
|||||||
+58
-28
@@ -97,39 +97,69 @@ async function handleTTSRequest(payload) {
|
|||||||
const { text, voice, requestId, language } = payload;
|
const { text, voice, requestId, language } = payload;
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
log(`TTS-Request: "${text.slice(0, 60)}..." (voice: ${voice || "default"}, lang: ${language || "de"})`);
|
// Markdown entfernen
|
||||||
|
const cleanText = text.replace(/\*\*([^*]+)\*\*/g, "$1").trim();
|
||||||
|
|
||||||
|
// Text in Saetze aufteilen, dann zu Chunks von 2-3 Saetzen zusammenfassen
|
||||||
|
// (mehr Kontext = konsistentere Stimme/Lautstaerke, aber nicht zu lang fuer WebSocket)
|
||||||
|
const sentences = cleanText.split(/(?<=[.!?])\s+/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
.map(s => s.replace(/[.]+$/, '')); // Punkt am Ende entfernen
|
||||||
|
|
||||||
|
const MAX_CHUNK_CHARS = 150; // Max ~150 Zeichen pro Chunk (schnelles Rendering, Preloading reicht)
|
||||||
|
const chunks = [];
|
||||||
|
let currentChunk = '';
|
||||||
|
for (const sentence of sentences) {
|
||||||
|
if (currentChunk && (currentChunk.length + sentence.length + 2) > MAX_CHUNK_CHARS) {
|
||||||
|
chunks.push(currentChunk);
|
||||||
|
currentChunk = sentence;
|
||||||
|
} else {
|
||||||
|
currentChunk = currentChunk ? currentChunk + ', ' + sentence : sentence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentChunk) chunks.push(currentChunk);
|
||||||
|
if (chunks.length === 0) return;
|
||||||
|
|
||||||
|
log(`TTS-Request: "${cleanText.slice(0, 60)}..." (${sentences.length} Saetze → ${chunks.length} Chunks, voice: ${voice || "default"}, lang: ${language || "de"})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Voice-Sample Pfad bestimmen
|
|
||||||
const voiceSample = voice ? path.join(VOICES_DIR, `${voice}.wav`) : null;
|
const voiceSample = voice ? path.join(VOICES_DIR, `${voice}.wav`) : null;
|
||||||
const hasCustomVoice = voiceSample && fs.existsSync(voiceSample);
|
const hasCustomVoice = voiceSample && fs.existsSync(voiceSample);
|
||||||
|
|
||||||
// XTTS API aufrufen
|
// Streaming: Chunk rendern → sofort senden → naechster Chunk
|
||||||
const audioBuffer = await callXTTSAPI(text, language || "de", hasCustomVoice ? voiceSample : null);
|
// App spielt mit Preloading-Queue nahtlos ab
|
||||||
|
let sentCount = 0;
|
||||||
|
|
||||||
if (audioBuffer && audioBuffer.length > 100) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
const base64 = audioBuffer.toString("base64");
|
const chunk = chunks[i];
|
||||||
log(`TTS fertig: ${audioBuffer.length} bytes (${(audioBuffer.length / 1024).toFixed(0)}KB)`);
|
try {
|
||||||
|
const audioBuffer = await callXTTSAPI(chunk, language || "de", hasCustomVoice ? voiceSample : null);
|
||||||
|
|
||||||
sendToRVS({
|
if (audioBuffer && audioBuffer.length > 100) {
|
||||||
type: "xtts_response",
|
log(`TTS [${i + 1}/${chunks.length}]: ${(audioBuffer.length / 1024).toFixed(0)}KB — "${chunk.slice(0, 50)}"`);
|
||||||
payload: {
|
|
||||||
requestId: requestId || "",
|
sendToRVS({
|
||||||
base64,
|
type: "xtts_response",
|
||||||
mimeType: "audio/wav",
|
payload: {
|
||||||
voice: voice || "default",
|
requestId: `${requestId || ""}_${i}`,
|
||||||
engine: "xtts",
|
base64: audioBuffer.toString("base64"),
|
||||||
},
|
mimeType: "audio/wav",
|
||||||
timestamp: Date.now(),
|
voice: voice || "default",
|
||||||
});
|
engine: "xtts",
|
||||||
} else {
|
part: i + 1,
|
||||||
log("TTS: Leeres Audio erhalten");
|
totalParts: chunks.length,
|
||||||
sendToRVS({
|
},
|
||||||
type: "xtts_response",
|
timestamp: Date.now(),
|
||||||
payload: { requestId, error: "Leeres Audio" },
|
});
|
||||||
timestamp: Date.now(),
|
sentCount++;
|
||||||
});
|
}
|
||||||
|
} catch (chunkErr) {
|
||||||
|
log(`TTS [${i + 1}/${chunks.length}] Fehler: ${chunkErr.message} — ueberspringe`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(`TTS komplett: ${sentCount}/${chunks.length} Chunks gestreamt`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(`TTS Fehler: ${err.message}`);
|
log(`TTS Fehler: ${err.message}`);
|
||||||
sendToRVS({
|
sendToRVS({
|
||||||
@@ -257,12 +287,12 @@ log(`RVS: ${RVS_HOST}:${RVS_PORT}`);
|
|||||||
function waitForXTTS(callback, attempts) {
|
function waitForXTTS(callback, attempts) {
|
||||||
if (attempts <= 0) { log("XTTS API nicht erreichbar — starte trotzdem"); callback(); return; }
|
if (attempts <= 0) { log("XTTS API nicht erreichbar — starte trotzdem"); callback(); return; }
|
||||||
http.get(`${XTTS_API_URL}/docs`, (res) => {
|
http.get(`${XTTS_API_URL}/docs`, (res) => {
|
||||||
log("XTTS API erreichbar");
|
log(`XTTS API erreichbar (HTTP ${res.statusCode})`);
|
||||||
callback();
|
callback();
|
||||||
}).on("error", () => {
|
}).on("error", () => {
|
||||||
log(`XTTS API noch nicht bereit — warte (${attempts} Versuche uebrig)...`);
|
log(`XTTS API noch nicht bereit — warte (${attempts} Versuche uebrig)...`);
|
||||||
setTimeout(() => waitForXTTS(callback, attempts - 1), 5000);
|
setTimeout(() => waitForXTTS(callback, attempts - 1), 10000); // 10s statt 5s (Model laden dauert)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForXTTS(() => connectRVS(), 24); // Max 2min warten
|
waitForXTTS(() => connectRVS(), 30); // Max 5min warten
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ services:
|
|||||||
|
|
||||||
# ─── XTTS v2 API Server (GPU) ─────────────────
|
# ─── XTTS v2 API Server (GPU) ─────────────────
|
||||||
xtts:
|
xtts:
|
||||||
image: ghcr.io/daswer123/xtts-api-server:latest
|
image: daswer123/xtts-api-server:latest
|
||||||
container_name: aria-xtts
|
container_name: aria-xtts
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -27,9 +27,9 @@ services:
|
|||||||
count: 1
|
count: 1
|
||||||
capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8020"
|
||||||
volumes:
|
volumes:
|
||||||
- xtts-models:/root/.local/share/tts # Model-Cache (~2GB)
|
- xtts-models:/app/xtts_models # Model-Cache (~2GB)
|
||||||
- ./voices:/voices # Custom Voice Samples
|
- ./voices:/voices # Custom Voice Samples
|
||||||
environment:
|
environment:
|
||||||
- COQUI_TOS_AGREED=1
|
- COQUI_TOS_AGREED=1
|
||||||
@@ -41,8 +41,10 @@ services:
|
|||||||
container_name: aria-xtts-bridge
|
container_name: aria-xtts-bridge
|
||||||
depends_on:
|
depends_on:
|
||||||
- xtts
|
- xtts
|
||||||
|
volumes:
|
||||||
|
- ./voices:/voices # Shared mit XTTS-Server
|
||||||
environment:
|
environment:
|
||||||
- XTTS_API_URL=http://xtts:8000
|
- XTTS_API_URL=http://xtts:8020
|
||||||
- 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}
|
||||||
|
|||||||
Reference in New Issue
Block a user