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:
+112
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user