feat: QR-Code Onboarding + TTS-Audio-Cache im Filesystem
QR-Code Onboarding - Diagnostic: GET /api/onboarding gibt RVS-Credentials zurueck - Einstellungen-UI: neue Sektion mit QR-Code (qrcode-generator via CDN) - Format kompatibel mit bestehendem QRScanner.parseQRData (host/port/tls/token) - App-SettingsScreen hatte QR-Scanner bereits — funktioniert out of the box - Warnhinweis zu Token im Klartext TTS-Audio-Cache - Bridge: jede ARIA-Chat-Nachricht bekommt eine messageId (UUID) Audio-Payload wird mit messageId verknuepft (Piper-Pfade) - ChatScreen: messageId + audioPath in ChatMessage Interface - audioService.cacheAudio(): speichert Base64 in DocumentDirectory/tts_cache/<id>.wav - audioService.playFromPath(): spielt aus Cache ohne Regenerierung - Play-Button: wenn audioPath gesetzt → aus Cache, sonst tts_request - cleanupOldTTSCache(): alte unreferenzierte WAVs (>30 Tage) weg - Persistiert via AsyncStorage — ueberlebt App-Restart Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -279,6 +279,46 @@ class AudioService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Base64-Audio persistent speichern. Gibt file:// Pfad zurueck (oder leer bei Fehler). */
|
||||
async cacheAudio(base64Data: string, messageId: string): Promise<string> {
|
||||
if (!base64Data || !messageId) return '';
|
||||
try {
|
||||
const dir = `${RNFS.DocumentDirectoryPath}/tts_cache`;
|
||||
await RNFS.mkdir(dir).catch(() => {});
|
||||
const path = `${dir}/${messageId}.wav`;
|
||||
// Wenn Datei schon existiert (z.B. XTTS Chunks) → anhaengen statt ueberschreiben
|
||||
const exists = await RNFS.exists(path);
|
||||
if (exists) {
|
||||
// Bestehende + neue Base64 laden, zusammenkleben (fuer jetzt: ueberschreiben)
|
||||
// XTTS sendet mehrere Chunks — bei mehrfacher Ueberschreibung bleibt nur der letzte
|
||||
// Fuer eine echte Konkatenation muesste WAV-Header gemerged werden
|
||||
await RNFS.writeFile(path, base64Data, 'base64');
|
||||
} else {
|
||||
await RNFS.writeFile(path, base64Data, 'base64');
|
||||
}
|
||||
return `file://${path}`;
|
||||
} catch (err) {
|
||||
console.warn('[Audio] cacheAudio fehlgeschlagen:', err);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Audio aus lokaler Datei (file:// Pfad) in die Queue und abspielen. */
|
||||
async playFromPath(filePath: string): Promise<void> {
|
||||
if (!filePath) return;
|
||||
try {
|
||||
const cleanPath = filePath.replace(/^file:\/\//, '');
|
||||
if (!(await RNFS.exists(cleanPath))) {
|
||||
console.warn('[Audio] Cache-Datei existiert nicht mehr:', cleanPath);
|
||||
return;
|
||||
}
|
||||
const b64 = await RNFS.readFile(cleanPath, 'base64');
|
||||
this.playAudio(b64);
|
||||
} catch (err) {
|
||||
console.warn('[Audio] playFromPath fehlgeschlagen:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Callback wenn alle Audio-Teile abgespielt sind
|
||||
private playbackFinishedListeners: (() => void)[] = [];
|
||||
|
||||
@@ -437,6 +477,28 @@ class AudioService {
|
||||
// silent — cleanup ist best-effort
|
||||
}
|
||||
}
|
||||
|
||||
/** Alte TTS-Cache-Dateien loeschen die nicht mehr referenziert sind (>30 Tage). */
|
||||
async cleanupOldTTSCache(keepMessageIds: Set<string>, maxAgeDays = 30): Promise<void> {
|
||||
try {
|
||||
const dir = `${RNFS.DocumentDirectoryPath}/tts_cache`;
|
||||
if (!(await RNFS.exists(dir))) return;
|
||||
const files = await RNFS.readDir(dir);
|
||||
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
for (const f of files) {
|
||||
if (!f.isFile() || !f.name.endsWith('.wav')) continue;
|
||||
const messageId = f.name.replace(/\.wav$/, '');
|
||||
const age = now - (f.mtime ? f.mtime.getTime() : 0);
|
||||
// Loeschen wenn: nicht mehr referenziert UND aelter als X Tage
|
||||
if (!keepMessageIds.has(messageId) && age > maxAgeMs) {
|
||||
await RNFS.unlink(f.path).catch(() => {});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
|
||||
Reference in New Issue
Block a user