Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a28b46a809 | |||
| 59c8d36a3d | |||
| 79ba7b8487 | |||
| ba62cec78c | |||
| f15b3f583f | |||
| 402bddc18a | |||
| 350069d371 |
@@ -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 402
|
versionCode 404
|
||||||
versionName "0.0.4.2"
|
versionName "0.0.4.4"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
val channelConfig = if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO
|
val channelConfig = if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO
|
||||||
val encoding = AudioFormat.ENCODING_PCM_16BIT
|
val encoding = AudioFormat.ENCODING_PCM_16BIT
|
||||||
val minBuf = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding)
|
val minBuf = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding)
|
||||||
// Etwas grosszuegiger Buffer: 8x MinSize (ca. 200-400ms bei 24kHz) — glatt auch bei kleinen Netzwerk-Aussetzern
|
// Grosszuegiger Buffer: 32x MinSize — tolerant gegen Netzwerk-Jitter und
|
||||||
val bufferSize = (minBuf * 8).coerceAtLeast(32 * 1024)
|
// bursty XTTS-Delivery (Render dauert 1-3s, dann kommen alle Samples
|
||||||
|
// auf einmal). Bei 24kHz mono s16 entspricht 128KB ca. 2.7 Sekunden.
|
||||||
|
val bufferSize = (minBuf * 32).coerceAtLeast(128 * 1024)
|
||||||
|
|
||||||
val newTrack = AudioTrack.Builder()
|
val newTrack = AudioTrack.Builder()
|
||||||
.setAudioAttributes(
|
.setAudioAttributes(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.0.4.2",
|
"version": "0.0.4.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
+6
-19
@@ -1100,25 +1100,12 @@ class ARIABridge:
|
|||||||
return
|
return
|
||||||
|
|
||||||
elif msg_type == "audio_pcm":
|
elif msg_type == "audio_pcm":
|
||||||
# XTTS-PCM-Stream vom Gaming-PC empfangen → durchleiten zur App.
|
# Audio-PCM geht direkt von XTTS-Bridge an die App.
|
||||||
# Wenn in payload kein messageId (alte XTTS-Bridge), aus requestId auflösen.
|
# Die aria-bridge darf es NICHT rebroadcasten — sonst bekommt die App
|
||||||
error = payload.get("error", "")
|
# jeden Chunk doppelt (einmal direkt von XTTS-Bridge via RVS-Broadcast,
|
||||||
if error:
|
# einmal indirekt via uns).
|
||||||
logger.warning("[rvs] XTTS PCM-Fehler: %s", error)
|
# Wir ignorieren diese Message hier einfach — messageId wird von
|
||||||
return
|
# XTTS-Bridge selbst im Payload mitgeliefert.
|
||||||
linked_message_id = payload.get("messageId", "")
|
|
||||||
if not linked_message_id:
|
|
||||||
req_id_full = payload.get("requestId", "")
|
|
||||||
req_id_base = req_id_full.rsplit("_", 1)[0] if "_" in req_id_full else req_id_full
|
|
||||||
linked_message_id = self._xtts_request_to_message.get(req_id_base, "")
|
|
||||||
# Einfach 1:1 weiterleiten mit eingefuellter messageId
|
|
||||||
forwarded = dict(payload)
|
|
||||||
forwarded["messageId"] = linked_message_id
|
|
||||||
await self._send_to_rvs({
|
|
||||||
"type": "audio_pcm",
|
|
||||||
"payload": forwarded,
|
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
elif msg_type == "xtts_response":
|
elif msg_type == "xtts_response":
|
||||||
|
|||||||
@@ -127,6 +127,33 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Disk-Space Warnung (dynamisch gesetzt) -->
|
||||||
|
<div id="disk-banner" style="display:none;position:sticky;top:0;z-index:500;padding:10px 14px;border-radius:0;margin:-16px -16px 12px -16px;font-size:13px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
||||||
|
<span id="disk-banner-icon" style="font-size:18px;">⚠️</span>
|
||||||
|
<span id="disk-banner-text" style="flex:1;min-width:200px;font-weight:600;"></span>
|
||||||
|
<button onclick="copyDiskCmd('safe')" class="btn secondary" style="padding:4px 10px;font-size:11px;" title="docker builder prune -a -f && docker image prune -a -f">
|
||||||
|
Sicher aufraeumen
|
||||||
|
</button>
|
||||||
|
<button onclick="document.getElementById('disk-banner-aggressive').style.display=(document.getElementById('disk-banner-aggressive').style.display==='none'?'flex':'none')"
|
||||||
|
class="btn secondary" style="padding:4px 10px;font-size:11px;">
|
||||||
|
Mehr ▾
|
||||||
|
</button>
|
||||||
|
<button onclick="document.getElementById('disk-banner').style.display='none'" class="btn secondary" style="padding:4px 10px;font-size:11px;">Schliessen</button>
|
||||||
|
</div>
|
||||||
|
<!-- Aggressive Variante (erst nach Klick sichtbar) -->
|
||||||
|
<div id="disk-banner-aggressive" style="display:none;margin-top:10px;padding:8px;background:rgba(0,0,0,0.25);border-radius:4px;flex-direction:column;gap:6px;font-size:12px;">
|
||||||
|
<div>
|
||||||
|
<b>Sicher</b> (empfohlen) — Build-Cache + ungenutzte Images, keine Volumes:<br>
|
||||||
|
<code style="font-family:monospace;">docker builder prune -a -f && docker image prune -a -f</code>
|
||||||
|
</div>
|
||||||
|
<div style="color:#FFAA55;">
|
||||||
|
<b>Aggressiv</b> — zusaetzlich ungenutzte Volumes. <b>Nur wenn alle ARIA-Container laufen</b>, sonst riskierst du Daten-Verlust (Sessions, SSH-Keys, Shared):<br>
|
||||||
|
<code style="font-family:monospace;">docker system prune -a --volumes -f</code>
|
||||||
|
<button onclick="copyDiskCmd('aggressive')" class="btn secondary" style="padding:2px 8px;font-size:10px;margin-left:6px;">Kopieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h1>ARIA Diagnostic</h1>
|
<h1>ARIA Diagnostic</h1>
|
||||||
|
|
||||||
<!-- Haupt-Navigation -->
|
<!-- Haupt-Navigation -->
|
||||||
@@ -753,6 +780,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'disk_status') {
|
||||||
|
updateDiskBanner(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === 'mode' && msg.payload) {
|
if (msg.type === 'mode' && msg.payload) {
|
||||||
// Bridge hat den Modus geaendert (evtl. von anderer App/Diagnostic) — UI syncen
|
// Bridge hat den Modus geaendert (evtl. von anderer App/Diagnostic) — UI syncen
|
||||||
const mode = (msg.payload.mode || '').toLowerCase();
|
const mode = (msg.payload.mode || '').toLowerCase();
|
||||||
@@ -2155,6 +2187,53 @@
|
|||||||
const ttsToggleEl = document.getElementById('tts-debug-toggle');
|
const ttsToggleEl = document.getElementById('tts-debug-toggle');
|
||||||
if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug;
|
if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug;
|
||||||
|
|
||||||
|
// Disk-Space Banner aktualisieren (wird vom Server via disk_status gepusht)
|
||||||
|
function updateDiskBanner(status) {
|
||||||
|
const banner = document.getElementById('disk-banner');
|
||||||
|
const icon = document.getElementById('disk-banner-icon');
|
||||||
|
const text = document.getElementById('disk-banner-text');
|
||||||
|
if (!banner) return;
|
||||||
|
if (!status || status.level === 'ok') {
|
||||||
|
banner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gb = (n) => (n / 1024 / 1024 / 1024).toFixed(1);
|
||||||
|
const pct = status.percent;
|
||||||
|
const used = gb(status.usedBytes);
|
||||||
|
const total = gb(status.totalBytes);
|
||||||
|
const avail = gb(status.availBytes);
|
||||||
|
let bg, col, msg;
|
||||||
|
if (status.level === 'critical') {
|
||||||
|
bg = '#5C1A1A'; col = '#FF6B6B'; icon.innerHTML = '🚨'; // 🚨
|
||||||
|
msg = `KRITISCH: Platte ${pct}% voll (${used}GB von ${total}GB, nur noch ${avail}GB frei). aria-core kann bald nicht mehr schreiben — sofort aufraeumen!`;
|
||||||
|
} else if (status.level === 'warn') {
|
||||||
|
bg = '#5C3A1A'; col = '#FFAA55'; icon.innerHTML = '⚠️'; // ⚠️
|
||||||
|
msg = `Warnung: Platte ${pct}% voll (${avail}GB frei). Bald aufraeumen.`;
|
||||||
|
} else {
|
||||||
|
bg = '#4A3A1A'; col = '#FFD60A'; icon.innerHTML = 'ℹ️'; // ℹ️
|
||||||
|
msg = `Hinweis: Platte ${pct}% voll (${avail}GB frei).`;
|
||||||
|
}
|
||||||
|
banner.style.background = bg;
|
||||||
|
banner.style.color = col;
|
||||||
|
banner.style.borderBottom = `2px solid ${col}`;
|
||||||
|
text.textContent = msg;
|
||||||
|
banner.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDiskCmd(variant) {
|
||||||
|
const cmd = variant === 'aggressive'
|
||||||
|
? 'docker system prune -a --volumes -f'
|
||||||
|
: 'docker builder prune -a -f && docker image prune -a -f';
|
||||||
|
navigator.clipboard.writeText(cmd).then(() => {
|
||||||
|
const btn = event.target;
|
||||||
|
const old = btn.textContent;
|
||||||
|
btn.textContent = 'Kopiert!';
|
||||||
|
setTimeout(() => { btn.textContent = old; }, 1500);
|
||||||
|
}).catch(() => {
|
||||||
|
alert('Kopieren fehlgeschlagen — Befehl: ' + cmd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
connectWS();
|
connectWS();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1148,6 +1148,53 @@ function updateAgentActivity() {
|
|||||||
watchdogWarned = false;
|
watchdogWarned = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Disk-Space Monitor ───────────────────────────────
|
||||||
|
// Prueft regelmaessig die Host-Disk (via gemountetem /shared) und
|
||||||
|
// broadcastet bei kritischen Schwellwerten ein disk_status Event.
|
||||||
|
let lastDiskStatus = null;
|
||||||
|
let currentDiskStatus = null; // Vollstaendig fuer neu verbundene Clients
|
||||||
|
function checkDiskSpace() {
|
||||||
|
const { exec } = require("child_process");
|
||||||
|
exec("df -B1 /shared", (err, stdout) => {
|
||||||
|
if (err) return;
|
||||||
|
const lines = stdout.trim().split("\n");
|
||||||
|
if (lines.length < 2) return;
|
||||||
|
const cols = lines[1].split(/\s+/);
|
||||||
|
// Filesystem Size Used Avail Use% MountedOn
|
||||||
|
const total = parseInt(cols[1], 10);
|
||||||
|
const used = parseInt(cols[2], 10);
|
||||||
|
const avail = parseInt(cols[3], 10);
|
||||||
|
if (!total) return;
|
||||||
|
const pct = Math.round((used / total) * 100);
|
||||||
|
let level = "ok";
|
||||||
|
if (pct >= 95) level = "critical";
|
||||||
|
else if (pct >= 85) level = "warn";
|
||||||
|
else if (pct >= 70) level = "info";
|
||||||
|
const status = {
|
||||||
|
type: "disk_status",
|
||||||
|
level,
|
||||||
|
percent: pct,
|
||||||
|
usedBytes: used,
|
||||||
|
totalBytes: total,
|
||||||
|
availBytes: avail,
|
||||||
|
};
|
||||||
|
currentDiskStatus = status;
|
||||||
|
// Nur broadcasten wenn sich was geaendert hat (oder alle 60s Refresh)
|
||||||
|
const key = `${level}-${pct}`;
|
||||||
|
if (lastDiskStatus !== key) {
|
||||||
|
lastDiskStatus = key;
|
||||||
|
broadcast(status);
|
||||||
|
if (level !== "ok") {
|
||||||
|
log(level === "critical" ? "error" : "warn", "server",
|
||||||
|
`Disk ${pct}% belegt (${(used/1024/1024/1024).toFixed(1)}GB von ${(total/1024/1024/1024).toFixed(1)}GB)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Beim Start + alle 30s
|
||||||
|
setTimeout(checkDiskSpace, 2000);
|
||||||
|
setInterval(checkDiskSpace, 30000);
|
||||||
|
|
||||||
// Watchdog prüft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert
|
// Watchdog prüft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
if (pendingMessageTime === 0) return; // Keine Nachricht gesendet
|
if (pendingMessageTime === 0) return; // Keine Nachricht gesendet
|
||||||
@@ -1281,6 +1328,8 @@ wss.on("connection", (ws) => {
|
|||||||
browserClients.add(ws);
|
browserClients.add(ws);
|
||||||
// Initialen State + letzte Logs senden
|
// Initialen State + letzte Logs senden
|
||||||
ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) }));
|
ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) }));
|
||||||
|
// Letzten Disk-Status mitgeben damit der Client sofort weiss wie's um Platz steht
|
||||||
|
if (currentDiskStatus) ws.send(JSON.stringify(currentDiskStatus));
|
||||||
|
|
||||||
ws.on("message", (raw) => {
|
ws.on("message", (raw) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+63
-82
@@ -95,7 +95,20 @@ function connectRVS(forcePlain) {
|
|||||||
|
|
||||||
// ── TTS Request Handler ─────────────────────────────
|
// ── TTS Request Handler ─────────────────────────────
|
||||||
|
|
||||||
async function handleTTSRequest(payload) {
|
// ── TTS-Queue ──────────────────────────────────────
|
||||||
|
// XTTS verarbeitet Requests sequenziell, damit Streams sich nicht ueberlappen.
|
||||||
|
// Ohne Queue wuerden parallele Requests parallel streamen → App bekommt
|
||||||
|
// interleaved PCM-Chunks aus zwei Rendern → klingt wie Chaos.
|
||||||
|
let ttsQueue = Promise.resolve();
|
||||||
|
|
||||||
|
function handleTTSRequest(payload) {
|
||||||
|
ttsQueue = ttsQueue.then(() => _runTTSRequest(payload)).catch(err => {
|
||||||
|
log(`TTS-Queue Fehler: ${err.message}`);
|
||||||
|
});
|
||||||
|
return ttsQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _runTTSRequest(payload) {
|
||||||
const { text, voice, requestId, language, messageId } = payload;
|
const { text, voice, requestId, language, messageId } = payload;
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
@@ -116,87 +129,58 @@ async function handleTTSRequest(payload) {
|
|||||||
.replace(/\(\)/g, "")
|
.replace(/\(\)/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Satzweise Chunks (XTTS Modell laedt Context pro Call — Saetze gruppieren)
|
log(`TTS-Request (streaming): "${cleanText.slice(0, 80)}..." (${cleanText.length} chars, voice: ${voice || "default"})`);
|
||||||
const sentences = cleanText.split(/(?<=[.!?])\s+/)
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(s => s.length > 0)
|
|
||||||
.map(s => s.replace(/[.]+$/, ''));
|
|
||||||
|
|
||||||
const MAX_CHUNK_CHARS = 150;
|
|
||||||
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 (streaming): "${cleanText.slice(0, 60)}..." (${chunks.length} Chunks, voice: ${voice || "default"})`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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);
|
||||||
|
|
||||||
let chunkIndex = 0;
|
let chunkIndex = 0;
|
||||||
// Audio-Format (aus WAV-Header extrahiert, einmal pro Request)
|
|
||||||
let pcmMeta = null;
|
let pcmMeta = null;
|
||||||
|
|
||||||
for (let i = 0; i < chunks.length; i++) {
|
// EIN Request fuer den GANZEN Text — kein Gap zwischen Saetzen.
|
||||||
const chunk = chunks[i];
|
// XTTS rendert und wir streamen PCM sobald es reinkommt.
|
||||||
const isLastChunk = i === chunks.length - 1;
|
await streamXTTSAsPCM(
|
||||||
try {
|
cleanText,
|
||||||
// Streaming: PCM-Frames werden nacheinander an RVS gepusht,
|
language || "de",
|
||||||
// sobald sie vom XTTS-Server reinkommen
|
hasCustomVoice ? voiceSample : null,
|
||||||
await streamXTTSAsPCM(
|
(pcmBase64, meta) => {
|
||||||
chunk,
|
if (!pcmMeta) pcmMeta = meta;
|
||||||
language || "de",
|
sendToRVS({
|
||||||
hasCustomVoice ? voiceSample : null,
|
type: "audio_pcm",
|
||||||
(pcmBase64, meta) => {
|
payload: {
|
||||||
if (!pcmMeta) pcmMeta = meta;
|
requestId: requestId || "",
|
||||||
sendToRVS({
|
messageId: messageId || "",
|
||||||
type: "audio_pcm",
|
base64: pcmBase64,
|
||||||
payload: {
|
format: "pcm_s16le",
|
||||||
requestId: requestId || "",
|
sampleRate: meta.sampleRate,
|
||||||
messageId: messageId || "",
|
channels: meta.channels,
|
||||||
base64: pcmBase64,
|
voice: voice || "default",
|
||||||
format: "pcm_s16le",
|
chunk: chunkIndex++,
|
||||||
sampleRate: meta.sampleRate,
|
final: false,
|
||||||
channels: meta.channels,
|
|
||||||
voice: voice || "default",
|
|
||||||
chunk: chunkIndex++,
|
|
||||||
final: false,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Nach letztem Text-Chunk: final-Flag senden damit App weiss "fertig"
|
// Am Ende: final-Flag damit App weiss "fertig" und Cache geschrieben werden kann
|
||||||
if (isLastChunk && pcmMeta) {
|
if (pcmMeta) {
|
||||||
sendToRVS({
|
sendToRVS({
|
||||||
type: "audio_pcm",
|
type: "audio_pcm",
|
||||||
payload: {
|
payload: {
|
||||||
requestId: requestId || "",
|
requestId: requestId || "",
|
||||||
messageId: messageId || "",
|
messageId: messageId || "",
|
||||||
base64: "",
|
base64: "",
|
||||||
format: "pcm_s16le",
|
format: "pcm_s16le",
|
||||||
sampleRate: pcmMeta.sampleRate,
|
sampleRate: pcmMeta.sampleRate,
|
||||||
channels: pcmMeta.channels,
|
channels: pcmMeta.channels,
|
||||||
voice: voice || "default",
|
voice: voice || "default",
|
||||||
chunk: chunkIndex++,
|
chunk: chunkIndex++,
|
||||||
final: true,
|
final: true,
|
||||||
},
|
},
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} catch (chunkErr) {
|
|
||||||
log(`TTS [${i + 1}/${chunks.length}] Fehler: ${chunkErr.message} — ueberspringe`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`TTS komplett: ${chunkIndex} PCM-Frames gestreamt (${cleanText.length} chars)`);
|
log(`TTS komplett: ${chunkIndex} PCM-Frames gestreamt (${cleanText.length} chars)`);
|
||||||
@@ -211,16 +195,12 @@ async function handleTTSRequest(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ruft /tts_to_audio/ auf und streamt das resultierende WAV bereits waehrend
|
* Ruft /tts_stream auf — echter Streaming-Endpoint von daswer123.
|
||||||
* des Empfangs in PCM-Frames an den Callback. Der WAV-Header wird einmal
|
* Chunked Transfer-Encoding: Samples kommen WAEHREND XTTS rendert,
|
||||||
* geparst, danach werden nur noch raw PCM-Samples weitergeleitet.
|
* nicht erst am Ende. Time-to-first-audio ~300-500ms statt 2-3s.
|
||||||
*
|
*
|
||||||
* Warum nicht echtes /tts_stream/? daswer123 hat den Endpoint, aber die
|
* stream_chunk_size = Characters pro Render-Iteration.
|
||||||
* Audio-Quality ist dort niedriger und er produziert beim ersten Chunk
|
* 40 = guter Kompromiss zwischen Schnellstart und GPU-Effizienz.
|
||||||
* oft Artefakte. Pragmatischer Weg: /tts_to_audio/ + Response-Stream
|
|
||||||
* chunkweise auslesen. Das ist zwar kein echtes Server-Streaming, aber
|
|
||||||
* gibt uns deutlich kleinere Netzwerk-Haeppchen und die App kann via
|
|
||||||
* AudioTrack MODE_STREAM sofort nahtlos abspielen.
|
|
||||||
*/
|
*/
|
||||||
function streamXTTSAsPCM(text, language, speakerWav, onPcmChunk) {
|
function streamXTTSAsPCM(text, language, speakerWav, onPcmChunk) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -228,9 +208,10 @@ function streamXTTSAsPCM(text, language, speakerWav, onPcmChunk) {
|
|||||||
text,
|
text,
|
||||||
language,
|
language,
|
||||||
speaker_wav: speakerWav || "",
|
speaker_wav: speakerWav || "",
|
||||||
|
stream_chunk_size: 40,
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = new URL(`${XTTS_API_URL}/tts_to_audio/`);
|
const url = new URL(`${XTTS_API_URL}/tts_stream`);
|
||||||
const options = {
|
const options = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port,
|
port: url.port,
|
||||||
|
|||||||
Reference in New Issue
Block a user