feat: XTTS v2 integration, auto-update system, TTS engine abstraction
- XTTS v2: Docker setup for Gaming-PC (GPU), bridge via RVS relay - XTTS: Voice cloning UI in Diagnostic (multi-file upload) - XTTS: Engine selectable (Piper local vs XTTS remote) with fallback - Auto-Update: RVS serves APK over WebSocket (no HTTP needed) - Auto-Update: App checks version on start, prompts install - Auto-Update: release.sh copies APK to RVS via scp - Bridge: TTS engine abstraction (piper/xtts), config persistent - Bridge: xtts_response handler, tts_request on-demand - Diagnostic: TTS engine dropdown, XTTS voice panel, voice cloning - App: Play button on ARIA messages, chat search, update service - Wake word: Disabled LiveAudioStream (crash fix), Phase 1 placeholder - Watchdog: Container restart after 8min stuck - Chat backup: on-the-fly to /shared/config/chat_backup.jsonl Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
81ca3cc7a7
commit
a242693751
|
|
@ -36,6 +36,7 @@ android/local.properties
|
|||
android/package-lock.json
|
||||
*.apk
|
||||
*.aab
|
||||
rvs/updates/*.apk
|
||||
|
||||
# ── Tauri / Desktop Build ───────────────────────
|
||||
desktop/src-tauri/target/
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import RNFS from 'react-native-fs';
|
|||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||
import audioService from '../services/audio';
|
||||
import wakeWordService from '../services/wakeword';
|
||||
import updateService from '../services/updater';
|
||||
import VoiceButton from '../components/VoiceButton';
|
||||
import FileUpload, { FileData } from '../components/FileUpload';
|
||||
import CameraUpload, { PhotoData } from '../components/CameraUpload';
|
||||
|
|
@ -262,6 +263,16 @@ const ChatScreen: React.FC = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Auto-Update: Bei App-Start pruefen
|
||||
useEffect(() => {
|
||||
const unsubUpdate = updateService.onUpdateAvailable((info) => {
|
||||
updateService.promptUpdate(info);
|
||||
});
|
||||
// Nach 5s pruefen (RVS muss erst verbunden sein)
|
||||
const timer = setTimeout(() => updateService.checkForUpdate(), 5000);
|
||||
return () => { unsubUpdate(); clearTimeout(timer); };
|
||||
}, []);
|
||||
|
||||
// Wake Word: "ARIA" Erkennung → Auto-Aufnahme starten
|
||||
useEffect(() => {
|
||||
const unsubWake = wakeWordService.onWakeWord(async () => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||
|
||||
export type ConnectionState = 'connecting' | 'connected' | 'disconnected';
|
||||
|
||||
export type MessageType = 'chat' | 'audio' | 'file' | 'location' | 'mode' | 'log' | 'event';
|
||||
export type MessageType = 'chat' | 'audio' | 'file' | 'location' | 'mode' | 'log' | 'event' | 'update_available' | string;
|
||||
|
||||
export interface RVSMessage {
|
||||
type: MessageType;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* Auto-Update Service — prueft und installiert App-Updates via RVS
|
||||
*
|
||||
* Flow:
|
||||
* 1. App sendet "update_check" mit aktueller Version an RVS
|
||||
* 2. RVS vergleicht → sendet "update_available" mit Download-URL
|
||||
* 3. App zeigt Benachrichtigung → User bestaetigt → Download + Install
|
||||
*/
|
||||
|
||||
import { Alert, Linking, Platform } from 'react-native';
|
||||
import RNFS from 'react-native-fs';
|
||||
import rvs, { RVSMessage } from './rvs';
|
||||
|
||||
// Aktuelle App-Version (aus package.json via Build)
|
||||
const APP_VERSION = '0.0.2.3'; // TODO: aus nativer Build-Config lesen
|
||||
|
||||
type UpdateCallback = (info: UpdateInfo) => void;
|
||||
|
||||
export interface UpdateInfo {
|
||||
version: string;
|
||||
downloadUrl: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
class UpdateService {
|
||||
private listeners: UpdateCallback[] = [];
|
||||
private checking = false;
|
||||
private downloading = false;
|
||||
|
||||
constructor() {
|
||||
// Auf update_available Nachrichten lauschen
|
||||
rvs.onMessage((msg: RVSMessage) => {
|
||||
if (msg.type === 'update_available' as any) {
|
||||
const info: UpdateInfo = {
|
||||
version: (msg.payload.version as string) || '',
|
||||
downloadUrl: (msg.payload.downloadUrl as string) || '',
|
||||
size: (msg.payload.size as number) || 0,
|
||||
};
|
||||
if (info.version && this.isNewer(info.version)) {
|
||||
console.log(`[Update] Neue Version verfuegbar: ${info.version} (aktuell: ${APP_VERSION})`);
|
||||
this.listeners.forEach(cb => cb(info));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Bei App-Start Update pruefen */
|
||||
checkForUpdate(): void {
|
||||
if (this.checking) return;
|
||||
this.checking = true;
|
||||
|
||||
console.log(`[Update] Pruefe auf Updates (aktuell: ${APP_VERSION})`);
|
||||
rvs.send('update_check' as any, { version: APP_VERSION });
|
||||
|
||||
setTimeout(() => { this.checking = false; }, 10000);
|
||||
}
|
||||
|
||||
/** Callback registrieren */
|
||||
onUpdateAvailable(callback: UpdateCallback): () => void {
|
||||
this.listeners.push(callback);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
/** Update-Dialog anzeigen */
|
||||
promptUpdate(info: UpdateInfo): void {
|
||||
const sizeMB = (info.size / 1024 / 1024).toFixed(1);
|
||||
Alert.alert(
|
||||
'ARIA Update verfuegbar',
|
||||
`Version ${info.version} (${sizeMB} MB)\n\nAktuell: ${APP_VERSION}\n\nJetzt herunterladen und installieren?`,
|
||||
[
|
||||
{ text: 'Spaeter', style: 'cancel' },
|
||||
{
|
||||
text: 'Installieren',
|
||||
onPress: () => this.downloadAndInstall(info),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** APK ueber WebSocket herunterladen und installieren */
|
||||
async downloadAndInstall(info: UpdateInfo): Promise<void> {
|
||||
if (this.downloading) return;
|
||||
this.downloading = true;
|
||||
|
||||
try {
|
||||
console.log(`[Update] Fordere APK v${info.version} an...`);
|
||||
Alert.alert('Download gestartet', `Version ${info.version} wird ueber RVS heruntergeladen...`);
|
||||
|
||||
// APK ueber WebSocket anfordern
|
||||
rvs.send('update_download' as any, {});
|
||||
|
||||
// Auf update_data warten (einmalig)
|
||||
const apkData = await new Promise<{base64: string, fileName: string}>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Download-Timeout (60s)')), 60000);
|
||||
const unsub = rvs.onMessage((msg: RVSMessage) => {
|
||||
if ((msg.type as string) === 'update_data') {
|
||||
clearTimeout(timeout);
|
||||
unsub();
|
||||
if (msg.payload.error) {
|
||||
reject(new Error(msg.payload.error as string));
|
||||
} else {
|
||||
resolve({
|
||||
base64: msg.payload.base64 as string,
|
||||
fileName: msg.payload.fileName as string || `ARIA-${info.version}.apk`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Base64 als APK-Datei speichern
|
||||
const destPath = `${RNFS.CachesDirectoryPath}/${apkData.fileName}`;
|
||||
await RNFS.writeFile(destPath, apkData.base64, 'base64');
|
||||
const fileSize = await RNFS.stat(destPath);
|
||||
console.log(`[Update] APK gespeichert: ${destPath} (${(parseInt(fileSize.size) / 1024 / 1024).toFixed(1)}MB)`);
|
||||
|
||||
// APK installieren (oeffnet Android-Installer)
|
||||
if (Platform.OS === 'android') {
|
||||
await Linking.openURL(`file://${destPath}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[Update] Fehler: ${err.message}`);
|
||||
Alert.alert('Update fehlgeschlagen', err.message);
|
||||
} finally {
|
||||
this.downloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Versionsvergleich */
|
||||
private isNewer(remote: string): boolean {
|
||||
const r = remote.split('.').map(Number);
|
||||
const l = APP_VERSION.split('.').map(Number);
|
||||
for (let i = 0; i < Math.max(r.length, l.length); i++) {
|
||||
const diff = (r[i] || 0) - (l[i] || 0);
|
||||
if (diff > 0) return true;
|
||||
if (diff < 0) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getCurrentVersion(): string {
|
||||
return APP_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
const updateService = new UpdateService();
|
||||
export default updateService;
|
||||
|
|
@ -503,6 +503,8 @@ class ARIABridge:
|
|||
"thorsten": vc.get("speedThorsten", 1.0),
|
||||
}
|
||||
self.tts_enabled = vc.get("ttsEnabled", True)
|
||||
self.tts_engine_type = vc.get("ttsEngine", "piper")
|
||||
self.xtts_voice = vc.get("xttsVoice", "")
|
||||
logger.info("Voice-Config geladen: %s", vc)
|
||||
except Exception as e:
|
||||
logger.warning("Voice-Config laden fehlgeschlagen: %s", e)
|
||||
|
|
@ -846,17 +848,47 @@ class ARIABridge:
|
|||
|
||||
# TTS-Audio rendern und an die App senden (wenn Modus es erlaubt)
|
||||
if getattr(self, 'tts_enabled', True) and should_speak(self.current_mode, is_critical):
|
||||
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),
|
||||
tts_engine = getattr(self, 'tts_engine_type', 'piper')
|
||||
|
||||
if tts_engine == "xtts":
|
||||
# XTTS: Request ueber RVS an Gaming-PC senden
|
||||
xtts_voice = getattr(self, 'xtts_voice', '')
|
||||
try:
|
||||
await self._send_to_rvs({
|
||||
"type": "xtts_request",
|
||||
"payload": {
|
||||
"text": text,
|
||||
"voice": xtts_voice,
|
||||
"language": "de",
|
||||
"requestId": str(uuid.uuid4()),
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
logger.info("[core] XTTS-Request gesendet (%s): '%s'", xtts_voice or "default", text[:60])
|
||||
except Exception as e:
|
||||
logger.warning("[core] XTTS-Request fehlgeschlagen: %s — Fallback auf Piper", e)
|
||||
# Fallback auf Piper
|
||||
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),
|
||||
})
|
||||
else:
|
||||
# Piper: Lokal rendern
|
||||
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)
|
||||
|
||||
|
|
@ -1014,6 +1046,26 @@ class ARIABridge:
|
|||
if sender in ("aria", "stt"):
|
||||
return
|
||||
|
||||
elif msg_type == "xtts_response":
|
||||
# XTTS-Audio vom Gaming-PC empfangen → an App weiterleiten
|
||||
audio_b64 = payload.get("base64", "")
|
||||
error = payload.get("error", "")
|
||||
if error:
|
||||
logger.warning("[rvs] XTTS Fehler: %s", error)
|
||||
return
|
||||
if audio_b64:
|
||||
logger.info("[rvs] XTTS-Audio empfangen: %dKB", len(audio_b64) // 1365)
|
||||
await self._send_to_rvs({
|
||||
"type": "audio",
|
||||
"payload": {
|
||||
"base64": audio_b64,
|
||||
"mimeType": payload.get("mimeType", "audio/wav"),
|
||||
"voice": payload.get("voice", "xtts"),
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
return
|
||||
|
||||
elif msg_type == "tts_request":
|
||||
# App fordert TTS-Audio fuer einen Text an (Play-Button)
|
||||
text = payload.get("text", "")
|
||||
|
|
@ -1057,6 +1109,14 @@ class ARIABridge:
|
|||
self.tts_enabled = bool(payload["ttsEnabled"])
|
||||
logger.info("[rvs] TTS %s", "aktiviert" if self.tts_enabled else "deaktiviert")
|
||||
changed = True
|
||||
if "ttsEngine" in payload:
|
||||
self.tts_engine_type = payload["ttsEngine"]
|
||||
logger.info("[rvs] TTS-Engine: %s", self.tts_engine_type)
|
||||
changed = True
|
||||
if "xttsVoice" in payload:
|
||||
self.xtts_voice = payload["xttsVoice"]
|
||||
logger.info("[rvs] XTTS-Stimme: %s", self.xtts_voice)
|
||||
changed = True
|
||||
if "speedRamona" in payload:
|
||||
self.voice_engine.speech_speed["ramona"] = max(0.3, min(2.0, float(payload["speedRamona"])))
|
||||
logger.info("[rvs] Speed Ramona: %.1f", self.voice_engine.speech_speed["ramona"])
|
||||
|
|
@ -1073,6 +1133,8 @@ class ARIABridge:
|
|||
"defaultVoice": self.voice_engine.default_voice,
|
||||
"highlightVoice": self.voice_engine.highlight_voice,
|
||||
"ttsEnabled": getattr(self, "tts_enabled", True),
|
||||
"ttsEngine": getattr(self, "tts_engine_type", "piper"),
|
||||
"xttsVoice": getattr(self, "xtts_voice", ""),
|
||||
"speedRamona": self.voice_engine.speech_speed.get("ramona", 1.0),
|
||||
"speedThorsten": self.voice_engine.speech_speed.get("thorsten", 1.0),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -401,6 +401,17 @@
|
|||
<div class="settings-section">
|
||||
<h2>Sprachausgabe</h2>
|
||||
<div class="card" style="max-width:500px;">
|
||||
<!-- TTS Engine Auswahl -->
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||
<label style="color:#8888AA;font-size:12px;">TTS Engine:</label>
|
||||
<select id="diag-tts-engine" onchange="sendVoiceConfig();toggleXTTSPanel()" style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||||
<option value="piper">Piper (lokal, CPU, schnell)</option>
|
||||
<option value="xtts">XTTS v2 (remote, GPU, natuerlich)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Piper Stimmen (nur bei Engine=piper) -->
|
||||
<div id="piper-panel">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||
<label style="color:#8888AA;font-size:12px;">Standard-Stimme:</label>
|
||||
<select id="diag-default-voice" onchange="sendVoiceConfig()" style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||||
|
|
@ -441,6 +452,42 @@
|
|||
style="flex:1;accent-color:#0096FF;">
|
||||
<span style="color:#555570;font-size:11px;">2.0x</span>
|
||||
</div>
|
||||
</div><!-- /piper-panel -->
|
||||
|
||||
<!-- XTTS Panel (nur bei Engine=xtts) -->
|
||||
<div id="xtts-panel" style="display:none;">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||||
<label style="color:#8888AA;font-size:12px;">XTTS Stimme:</label>
|
||||
<select id="diag-xtts-voice" onchange="sendVoiceConfig()" style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||||
<option value="">Standard (XTTS Default)</option>
|
||||
</select>
|
||||
<button class="btn secondary" onclick="loadXTTSVoices()" style="padding:4px 10px;font-size:11px;">Laden</button>
|
||||
</div>
|
||||
|
||||
<!-- Voice Cloning -->
|
||||
<div style="background:#1E1E2E;border-radius:8px;padding:12px;margin-top:8px;">
|
||||
<div style="color:#0096FF;font-size:13px;font-weight:600;margin-bottom:8px;">Stimme klonen</div>
|
||||
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
|
||||
Lade ein oder mehrere Audio-Samples hoch (WAV/MP3, min. 6-10 Sekunden).
|
||||
Mehrere Dateien werden automatisch zusammengefuegt.
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<input type="text" id="xtts-clone-name" placeholder="Name fuer die Stimme..." style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;color:#fff;font-size:13px;width:100%;box-sizing:border-box;">
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<input type="file" id="xtts-clone-files" accept="audio/*" multiple style="color:#8888AA;font-size:12px;">
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button class="btn" onclick="uploadVoiceSamples()" style="flex:1;">Stimme erstellen</button>
|
||||
</div>
|
||||
<div id="xtts-clone-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- XTTS Status -->
|
||||
<div style="margin-top:8px;font-size:11px;color:#555570;" id="xtts-status">
|
||||
XTTS-Server: Nicht verbunden (starte xtts/ auf dem Gaming-PC)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -665,6 +712,27 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'xtts_voices_list') {
|
||||
const select = document.getElementById('diag-xtts-voice');
|
||||
// Behalte erste Option (Default)
|
||||
while (select.options.length > 1) select.remove(1);
|
||||
for (const v of (msg.payload?.voices || [])) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = v.name;
|
||||
opt.textContent = `${v.name} (${(v.size / 1024).toFixed(0)}KB)`;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
document.getElementById('xtts-status').textContent = `XTTS: ${msg.payload?.voices?.length || 0} Stimme(n) verfuegbar`;
|
||||
document.getElementById('xtts-status').style.color = '#34C759';
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'xtts_voice_saved') {
|
||||
document.getElementById('xtts-clone-status').textContent = `Stimme "${msg.payload?.name}" gespeichert!`;
|
||||
document.getElementById('xtts-clone-status').style.color = '#34C759';
|
||||
loadXTTSVoices(); // Liste neu laden
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'voice_config') {
|
||||
document.getElementById('diag-default-voice').value = msg.defaultVoice || 'ramona';
|
||||
document.getElementById('diag-highlight-voice').value = msg.highlightVoice || 'thorsten';
|
||||
|
|
@ -675,6 +743,9 @@
|
|||
document.getElementById('speed-ramona-label').textContent = sr + 'x';
|
||||
document.getElementById('diag-speed-thorsten').value = st;
|
||||
document.getElementById('speed-thorsten-label').textContent = st + 'x';
|
||||
document.getElementById('diag-tts-engine').value = msg.ttsEngine || 'piper';
|
||||
document.getElementById('diag-xtts-voice').value = msg.xttsVoice || '';
|
||||
toggleXTTSPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1167,6 +1238,44 @@
|
|||
}, 120000);
|
||||
}
|
||||
|
||||
// ── XTTS Panel ─────────────────────────────
|
||||
function toggleXTTSPanel() {
|
||||
const engine = document.getElementById('diag-tts-engine').value;
|
||||
document.getElementById('piper-panel').style.display = engine === 'piper' ? 'block' : 'none';
|
||||
document.getElementById('xtts-panel').style.display = engine === 'xtts' ? 'block' : 'none';
|
||||
if (engine === 'xtts') loadXTTSVoices();
|
||||
}
|
||||
|
||||
function loadXTTSVoices() {
|
||||
sendToRVS_raw({ type: 'xtts_list_voices', payload: {}, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
async function uploadVoiceSamples() {
|
||||
const name = document.getElementById('xtts-clone-name').value.trim();
|
||||
const files = document.getElementById('xtts-clone-files').files;
|
||||
if (!name) { alert('Bitte einen Namen eingeben'); return; }
|
||||
if (!files || files.length === 0) { alert('Bitte Audio-Dateien auswaehlen'); return; }
|
||||
|
||||
document.getElementById('xtts-clone-status').textContent = `Lade ${files.length} Datei(en) hoch...`;
|
||||
|
||||
const samples = [];
|
||||
for (const file of files) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||
samples.push({ base64, name: file.name, size: file.size });
|
||||
}
|
||||
|
||||
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 ──────────────────────────────
|
||||
function cancelRequest() {
|
||||
send({ action: 'cancel_request' });
|
||||
|
|
@ -1181,7 +1290,9 @@
|
|||
const ttsEnabled = document.getElementById('diag-tts-enabled').checked;
|
||||
const speedRamona = parseFloat(document.getElementById('diag-speed-ramona').value);
|
||||
const speedThorsten = parseFloat(document.getElementById('diag-speed-thorsten').value);
|
||||
send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled, speedRamona, speedThorsten });
|
||||
const ttsEngine = document.getElementById('diag-tts-engine').value;
|
||||
const xttsVoice = document.getElementById('diag-xtts-voice').value;
|
||||
send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled, speedRamona, speedThorsten, ttsEngine, xttsVoice });
|
||||
}
|
||||
|
||||
// ── Highlight-Trigger ────────────────────────
|
||||
|
|
|
|||
|
|
@ -1173,6 +1173,8 @@ wss.on("connection", (ws) => {
|
|||
defaultVoice: msg.defaultVoice || "ramona",
|
||||
highlightVoice: msg.highlightVoice || "thorsten",
|
||||
ttsEnabled: msg.ttsEnabled !== false,
|
||||
ttsEngine: msg.ttsEngine || "piper",
|
||||
xttsVoice: msg.xttsVoice || "",
|
||||
speedRamona: msg.speedRamona || 1.0,
|
||||
speedThorsten: msg.speedThorsten || 1.0,
|
||||
};
|
||||
|
|
|
|||
17
release.sh
17
release.sh
|
|
@ -170,6 +170,22 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# ── Auto-Update: APK auf RVS-Server kopieren ─
|
||||
RVS_UPDATE_HOST="${RVS_UPDATE_HOST:-}"
|
||||
if [ -n "$RVS_UPDATE_HOST" ]; then
|
||||
echo -e "${GREEN}[6/6] APK auf RVS-Server kopieren (Auto-Update)...${NC}"
|
||||
scp "$APK_PATH" "${RVS_UPDATE_HOST}:~/ARIA-AGENT/rvs/updates/${APK_NAME}" 2>/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e " ${GREEN}✓${NC} APK auf RVS-Server kopiert — Apps werden benachrichtigt"
|
||||
else
|
||||
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}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Auto-Update uebersprungen (RVS_UPDATE_HOST nicht gesetzt)${NC}"
|
||||
echo -e "${YELLOW}Setze RVS_UPDATE_HOST in .env fuer automatische Verteilung${NC}"
|
||||
fi
|
||||
|
||||
# ── Fertig ────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════╗${NC}"
|
||||
|
|
@ -177,4 +193,5 @@ echo -e "${GREEN}║ Release $TAG ist live!$(printf '%*s' $((27 - ${#TAG})) ''
|
|||
echo -e "${GREEN}╠═══════════════════════════════════════════════════╣${NC}"
|
||||
echo -e "${GREEN}║${NC} $GITEA_URL/$GITEA_REPO/releases/tag/$TAG"
|
||||
echo -e "${GREEN}║${NC} APK: $APK_NAME ($APK_SIZE)"
|
||||
echo -e "${GREEN}║${NC} Auto-Update: ${RVS_UPDATE_HOST:-nicht konfiguriert}"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════╝${NC}"
|
||||
|
|
|
|||
|
|
@ -4,5 +4,7 @@ services:
|
|||
ports:
|
||||
- "${RVS_PORT:-443}:3000"
|
||||
restart: always
|
||||
volumes:
|
||||
- ./updates:/updates # APK-Dateien fuer Auto-Update
|
||||
environment:
|
||||
- MAX_SESSIONS=10
|
||||
|
|
|
|||
112
rvs/server.js
112
rvs/server.js
|
|
@ -1,15 +1,21 @@
|
|||
"use strict";
|
||||
|
||||
const { WebSocketServer } = require("ws");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// ── Konfiguration aus Umgebungsvariablen ────────────────────────────
|
||||
const PORT = parseInt(process.env.PORT || "3000", 10);
|
||||
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
|
||||
const UPDATES_DIR = process.env.UPDATES_DIR || "/updates";
|
||||
// Kein Polling — APK wird manuell per git pull bereitgestellt
|
||||
|
||||
// Erlaubte Nachrichtentypen — alles andere wird verworfen
|
||||
const ALLOWED_TYPES = new Set([
|
||||
"chat", "audio", "file", "location", "mode", "log", "event", "heartbeat",
|
||||
"file_request", "file_response", "file_saved", "stt_result", "config", "tts_request",
|
||||
"xtts_request", "xtts_response", "xtts_list_voices", "xtts_voices_list", "voice_upload", "xtts_voice_saved",
|
||||
"update_check", "update_available", "update_download", "update_data",
|
||||
]);
|
||||
|
||||
// Token-Raum: token -> { clients: Set<ws> }
|
||||
|
|
@ -46,6 +52,9 @@ const wss = new WebSocketServer({ port: PORT });
|
|||
|
||||
wss.on("listening", () => {
|
||||
log(`RVS läuft auf Port ${PORT} | Max Sessions: ${MAX_SESSIONS}`);
|
||||
// Beim Start pruefen ob eine APK da ist
|
||||
const apkInfo = getLatestAPK();
|
||||
if (apkInfo) log(`APK bereit: v${apkInfo.version} (${(fs.statSync(apkInfo.path).size / 1024 / 1024).toFixed(1)}MB)`);
|
||||
});
|
||||
|
||||
wss.on("connection", (ws, req) => {
|
||||
|
|
@ -107,6 +116,52 @@ function registerClient(ws, token) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update-Check: direkt an den anfragenden Client antworten (nicht relay'en)
|
||||
if (msg.type === "update_check") {
|
||||
const clientVersion = msg.payload?.version || "0.0.0.0";
|
||||
const apkInfo = getLatestAPK();
|
||||
if (apkInfo && compareVersions(apkInfo.version, clientVersion) > 0) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "update_available",
|
||||
payload: {
|
||||
version: apkInfo.version,
|
||||
downloadUrl: `/update/latest.apk`,
|
||||
size: fs.statSync(apkInfo.path).size,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update-Download: APK als Base64 ueber WebSocket senden
|
||||
if (msg.type === "update_download") {
|
||||
const apkInfo = getLatestAPK();
|
||||
if (!apkInfo) {
|
||||
ws.send(JSON.stringify({ type: "update_data", payload: { error: "Keine APK verfuegbar" }, timestamp: Date.now() }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = fs.readFileSync(apkInfo.path);
|
||||
const base64 = data.toString("base64");
|
||||
const sizeMB = (data.length / 1024 / 1024).toFixed(1);
|
||||
log(`APK sende: v${apkInfo.version} (${sizeMB}MB) an Client`);
|
||||
ws.send(JSON.stringify({
|
||||
type: "update_data",
|
||||
payload: {
|
||||
version: apkInfo.version,
|
||||
base64,
|
||||
size: data.length,
|
||||
fileName: `ARIA-v${apkInfo.version}.apk`,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: "update_data", payload: { error: err.message }, timestamp: Date.now() }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// An alle anderen Clients im Raum weiterleiten
|
||||
for (const client of room.clients) {
|
||||
if (client !== ws && client.readyState === 1) {
|
||||
|
|
@ -167,6 +222,63 @@ wss.on("close", () => {
|
|||
clearInterval(cleanup);
|
||||
});
|
||||
|
||||
// ── Auto-Update: APK-Erkennung + Push ──────────────────────────────
|
||||
|
||||
let latestVersion = null;
|
||||
|
||||
function getLatestAPK() {
|
||||
try {
|
||||
if (!fs.existsSync(UPDATES_DIR)) return null;
|
||||
const files = fs.readdirSync(UPDATES_DIR)
|
||||
.filter(f => f.endsWith(".apk"))
|
||||
.map(f => {
|
||||
// ARIA-v0.0.2.3.apk oder ARIA-Cockpit-release.apk
|
||||
const match = f.match(/(\d+\.\d+\.\d+[\.\d]*)/);
|
||||
return { file: f, path: path.join(UPDATES_DIR, f), version: match ? match[1] : null };
|
||||
})
|
||||
.filter(f => f.version)
|
||||
.sort((a, b) => compareVersions(b.version, a.version)); // Neueste zuerst
|
||||
|
||||
return files[0] || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const pa = a.split(".").map(Number);
|
||||
const pb = b.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const diff = (pa[i] || 0) - (pb[i] || 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function notifyClientsAboutUpdate(apkInfo) {
|
||||
const msg = JSON.stringify({
|
||||
type: "update_available",
|
||||
payload: {
|
||||
version: apkInfo.version,
|
||||
downloadUrl: `/update/latest.apk`,
|
||||
size: fs.statSync(apkInfo.path).size,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// An alle Clients in allen Rooms senden
|
||||
for (const [, room] of rooms) {
|
||||
for (const client of room.clients) {
|
||||
if (client.readyState === 1) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
log(`Update-Benachrichtigung gesendet: v${apkInfo.version} (${rooms.size} Raum/Raeume)`);
|
||||
}
|
||||
|
||||
// Kein Polling — Update-Check passiert on-demand (update_check Message von App)
|
||||
|
||||
// ── Sauberes Herunterfahren ─────────────────────────────────────────
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# ════════════════════════════════════════════════
|
||||
# ARIA XTTS v2 — Konfiguration
|
||||
# Kopieren nach .env und anpassen
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
# RVS Verbindung (gleiche Daten wie auf der ARIA-VM)
|
||||
RVS_HOST=mobil.hacker-net.de
|
||||
RVS_PORT=444
|
||||
RVS_TLS=true
|
||||
RVS_TLS_FALLBACK=true
|
||||
RVS_TOKEN=dein_token_hier
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY bridge.js package.json ./
|
||||
RUN npm install --production
|
||||
CMD ["node", "bridge.js"]
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
/**
|
||||
* ARIA XTTS Bridge — Verbindet XTTS v2 Server mit dem RVS
|
||||
*
|
||||
* Empfaengt tts_request ueber RVS → rendert Audio via XTTS API → sendet zurueck
|
||||
* Empfaengt voice_upload → speichert Voice-Sample fuer Cloning
|
||||
* Empfaengt xtts_list_voices → listet verfuegbare Stimmen
|
||||
*/
|
||||
|
||||
const WebSocket = require("ws");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const XTTS_API_URL = process.env.XTTS_API_URL || "http://xtts:8000";
|
||||
const RVS_HOST = process.env.RVS_HOST || "";
|
||||
const RVS_PORT = process.env.RVS_PORT || "443";
|
||||
const RVS_TLS = process.env.RVS_TLS || "true";
|
||||
const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true";
|
||||
const RVS_TOKEN = process.env.RVS_TOKEN || "";
|
||||
const VOICES_DIR = "/voices";
|
||||
|
||||
function log(msg) {
|
||||
console.log(`[${new Date().toISOString()}] ${msg}`);
|
||||
}
|
||||
|
||||
// ── RVS Verbindung ──────────────────────────────────
|
||||
|
||||
let rvsWs = null;
|
||||
let retryDelay = 2;
|
||||
|
||||
function connectRVS(forcePlain) {
|
||||
if (!RVS_HOST || !RVS_TOKEN) {
|
||||
log("RVS nicht konfiguriert — beende");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const useTls = RVS_TLS === "true" && !forcePlain;
|
||||
const proto = useTls ? "wss" : "ws";
|
||||
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
|
||||
|
||||
log(`Verbinde zu RVS: ${proto}://${RVS_HOST}:${RVS_PORT}`);
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.on("open", () => {
|
||||
log("RVS verbunden — warte auf TTS-Requests");
|
||||
rvsWs = ws;
|
||||
retryDelay = 2;
|
||||
|
||||
// Keepalive
|
||||
setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.ping();
|
||||
ws.send(JSON.stringify({ type: "heartbeat", timestamp: Date.now() }));
|
||||
}
|
||||
}, 25000);
|
||||
});
|
||||
|
||||
ws.on("message", async (raw) => {
|
||||
try {
|
||||
const msg = JSON.parse(raw.toString());
|
||||
|
||||
if (msg.type === "xtts_request") {
|
||||
await handleTTSRequest(msg.payload);
|
||||
} else if (msg.type === "voice_upload") {
|
||||
await handleVoiceUpload(msg.payload);
|
||||
} else if (msg.type === "xtts_list_voices") {
|
||||
await handleListVoices();
|
||||
}
|
||||
} catch (err) {
|
||||
log(`Fehler: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
log("RVS Verbindung geschlossen");
|
||||
rvsWs = null;
|
||||
setTimeout(() => connectRVS(), Math.min(retryDelay * 1000, 30000));
|
||||
retryDelay = Math.min(retryDelay * 2, 30);
|
||||
});
|
||||
|
||||
ws.on("error", (err) => {
|
||||
log(`RVS Fehler: ${err.message}`);
|
||||
if (useTls && RVS_TLS_FALLBACK === "true") {
|
||||
log("TLS fehlgeschlagen — Fallback auf ws://");
|
||||
ws.removeAllListeners();
|
||||
try { ws.close(); } catch (_) {}
|
||||
connectRVS(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── TTS Request Handler ─────────────────────────────
|
||||
|
||||
async function handleTTSRequest(payload) {
|
||||
const { text, voice, requestId, language } = payload;
|
||||
if (!text) return;
|
||||
|
||||
log(`TTS-Request: "${text.slice(0, 60)}..." (voice: ${voice || "default"}, lang: ${language || "de"})`);
|
||||
|
||||
try {
|
||||
// Voice-Sample Pfad bestimmen
|
||||
const voiceSample = voice ? path.join(VOICES_DIR, `${voice}.wav`) : null;
|
||||
const hasCustomVoice = voiceSample && fs.existsSync(voiceSample);
|
||||
|
||||
// XTTS API aufrufen
|
||||
const audioBuffer = await callXTTSAPI(text, language || "de", hasCustomVoice ? voiceSample : null);
|
||||
|
||||
if (audioBuffer && audioBuffer.length > 100) {
|
||||
const base64 = audioBuffer.toString("base64");
|
||||
log(`TTS fertig: ${audioBuffer.length} bytes (${(audioBuffer.length / 1024).toFixed(0)}KB)`);
|
||||
|
||||
sendToRVS({
|
||||
type: "xtts_response",
|
||||
payload: {
|
||||
requestId: requestId || "",
|
||||
base64,
|
||||
mimeType: "audio/wav",
|
||||
voice: voice || "default",
|
||||
engine: "xtts",
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
log("TTS: Leeres Audio erhalten");
|
||||
sendToRVS({
|
||||
type: "xtts_response",
|
||||
payload: { requestId, error: "Leeres Audio" },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
log(`TTS Fehler: ${err.message}`);
|
||||
sendToRVS({
|
||||
type: "xtts_response",
|
||||
payload: { requestId, error: err.message },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function callXTTSAPI(text, language, speakerWav) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = JSON.stringify({
|
||||
text,
|
||||
language,
|
||||
speaker_wav: speakerWav || "",
|
||||
});
|
||||
|
||||
const url = new URL(`${XTTS_API_URL}/tts_to_audio/`);
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: url.pathname,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
},
|
||||
timeout: 60000,
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
const chunks = [];
|
||||
res.on("data", (chunk) => chunks.push(chunk));
|
||||
res.on("end", () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(Buffer.concat(chunks));
|
||||
} else {
|
||||
reject(new Error(`XTTS API HTTP ${res.statusCode}: ${Buffer.concat(chunks).toString().slice(0, 200)}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => { req.destroy(); reject(new Error("XTTS API Timeout (60s)")); });
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Voice Upload Handler ────────────────────────────
|
||||
|
||||
async function handleVoiceUpload(payload) {
|
||||
const { name, samples } = payload;
|
||||
if (!name || !samples || !Array.isArray(samples) || samples.length === 0) {
|
||||
log("Voice Upload: Ungueltige Daten");
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Voice Upload: "${name}" (${samples.length} Samples)`);
|
||||
|
||||
try {
|
||||
// Alle Samples zusammenfuegen
|
||||
const buffers = samples.map(s => Buffer.from(s.base64, "base64"));
|
||||
const combined = Buffer.concat(buffers);
|
||||
|
||||
// Als WAV speichern
|
||||
fs.mkdirSync(VOICES_DIR, { recursive: true });
|
||||
const filePath = path.join(VOICES_DIR, `${name.replace(/[^a-zA-Z0-9_-]/g, "_")}.wav`);
|
||||
fs.writeFileSync(filePath, combined);
|
||||
|
||||
log(`Voice gespeichert: ${filePath} (${(combined.length / 1024).toFixed(0)}KB)`);
|
||||
|
||||
sendToRVS({
|
||||
type: "xtts_voice_saved",
|
||||
payload: { name, size: combined.length, path: filePath },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (err) {
|
||||
log(`Voice Upload Fehler: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Voice List Handler ──────────────────────────────
|
||||
|
||||
async function handleListVoices() {
|
||||
try {
|
||||
const files = fs.existsSync(VOICES_DIR)
|
||||
? fs.readdirSync(VOICES_DIR).filter(f => f.endsWith(".wav"))
|
||||
: [];
|
||||
|
||||
const voices = files.map(f => ({
|
||||
name: path.basename(f, ".wav"),
|
||||
file: f,
|
||||
size: fs.statSync(path.join(VOICES_DIR, f)).size,
|
||||
}));
|
||||
|
||||
log(`Stimmen: ${voices.length} verfuegbar`);
|
||||
|
||||
sendToRVS({
|
||||
type: "xtts_voices_list",
|
||||
payload: { voices },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (err) {
|
||||
log(`Stimmen-Liste Fehler: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── RVS senden ──────────────────────────────────────
|
||||
|
||||
function sendToRVS(msg) {
|
||||
if (rvsWs && rvsWs.readyState === WebSocket.OPEN) {
|
||||
rvsWs.send(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Start ───────────────────────────────────────────
|
||||
|
||||
log("ARIA XTTS Bridge startet...");
|
||||
log(`XTTS API: ${XTTS_API_URL}`);
|
||||
log(`RVS: ${RVS_HOST}:${RVS_PORT}`);
|
||||
|
||||
// Warten bis XTTS API erreichbar ist
|
||||
function waitForXTTS(callback, attempts) {
|
||||
if (attempts <= 0) { log("XTTS API nicht erreichbar — starte trotzdem"); callback(); return; }
|
||||
http.get(`${XTTS_API_URL}/docs`, (res) => {
|
||||
log("XTTS API erreichbar");
|
||||
callback();
|
||||
}).on("error", () => {
|
||||
log(`XTTS API noch nicht bereit — warte (${attempts} Versuche uebrig)...`);
|
||||
setTimeout(() => waitForXTTS(callback, attempts - 1), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
waitForXTTS(() => connectRVS(), 24); // Max 2min warten
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# ════════════════════════════════════════════════
|
||||
# ARIA XTTS v2 — GPU TTS Server
|
||||
# Laeuft auf dem Gaming-PC (RTX 3060)
|
||||
# Verbindet sich zum RVS fuer TTS-Requests
|
||||
# ════════════════════════════════════════════════
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# - Docker Desktop mit WSL2
|
||||
# - NVIDIA Container Toolkit
|
||||
# - .env mit RVS-Verbindungsdaten
|
||||
#
|
||||
# Start: docker compose up -d
|
||||
# Test: curl http://localhost:8000/docs
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
services:
|
||||
|
||||
# ─── XTTS v2 API Server (GPU) ─────────────────
|
||||
xtts:
|
||||
image: ghcr.io/daswer123/xtts-api-server:latest
|
||||
container_name: aria-xtts
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- xtts-models:/root/.local/share/tts # Model-Cache (~2GB)
|
||||
- ./voices:/voices # Custom Voice Samples
|
||||
environment:
|
||||
- COQUI_TOS_AGREED=1
|
||||
restart: unless-stopped
|
||||
|
||||
# ─── XTTS Bridge (verbindet zu RVS) ───────────
|
||||
xtts-bridge:
|
||||
build: .
|
||||
container_name: aria-xtts-bridge
|
||||
depends_on:
|
||||
- xtts
|
||||
environment:
|
||||
- XTTS_API_URL=http://xtts:8000
|
||||
- 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
|
||||
|
||||
volumes:
|
||||
xtts-models:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "aria-xtts-bridge",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue