From 50b10c8ac0e6c0286188c3bacf6c0203ada46979 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 10 May 2026 16:36:01 +0200 Subject: [PATCH] feat(audio): Cache-Cleanup beim App-Start + TTS-Cache-Settings-Button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App-Start raeumt orphane aria_tts_*.wav (>5min) aus dem Cache — Wiedergaben die durch Anruf/Mute/Barge-In abgebrochen wurden hinterliessen sonst Files, weil der completion-Callback nicht feuert. - Neuer Settings-Button "TTS-Cache leeren" mit Live-Groessenanzeige — parallel zum bestehenden "Update-Cache leeren". Co-Authored-By: Claude Opus 4.7 (1M context) --- android/src/screens/SettingsScreen.tsx | 35 +++++++++++++++ android/src/services/audio.ts | 59 ++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 6060c41..b22da8d 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -50,6 +50,7 @@ import { TTS_SPEED_MAX, TTS_SPEED_STORAGE_KEY, } from '../services/audio'; +import audioService from '../services/audio'; import { isWakeReadySoundEnabled, setWakeReadySoundEnabled, @@ -135,6 +136,7 @@ const SettingsScreen: React.FC = () => { const [vadSilenceDb, setVadSilenceDb] = useState(null); const [showVadInfo, setShowVadInfo] = useState(false); const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null); + const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null); const [ttsSpeed, setTtsSpeed] = useState(TTS_SPEED_DEFAULT); const [wakeKeyword, setWakeKeyword] = useState(DEFAULT_KEYWORD); const [wakeStatus, setWakeStatus] = useState(''); @@ -224,6 +226,7 @@ const SettingsScreen: React.FC = () => { }); isWakeReadySoundEnabled().then(setWakeReadySound); updateService.getApkCacheSize().then(setApkCacheInfo).catch(() => {}); + audioService.getTtsCacheSize().then(setTtsCacheInfo).catch(() => {}); AsyncStorage.getItem('aria_xtts_voice').then(saved => { if (saved) setXttsVoice(saved); }); @@ -1251,6 +1254,38 @@ const SettingsScreen: React.FC = () => { + {/* === TTS-Cache === */} + TTS-Cache + + + Gespeicherte Sprachausgaben (WAV pro Antwort) — werden fuer den + Play-Button und Auto-Resume nach Anrufen genutzt. Loeschen + unterbricht keine laufende Wiedergabe, alte Antworten lassen sich + danach nur nicht mehr abspielen. + + + {ttsCacheInfo === null ? '...' : + ttsCacheInfo.count === 0 ? 'leer' : + `${ttsCacheInfo.count} WAV${ttsCacheInfo.count === 1 ? '' : 's'} · ${ttsCacheInfo.totalMB.toFixed(1)}MB`} + + { + const res = await audioService.clearTtsCache(); + ToastAndroid.show( + res.removed === 0 + ? 'TTS-Cache war schon leer' + : `${res.removed} WAV${res.removed === 1 ? '' : 's'} geloescht (${res.freedMB.toFixed(1)}MB frei)`, + ToastAndroid.SHORT, + ); + const info = await audioService.getTtsCacheSize(); + setTtsCacheInfo(info); + }} + > + TTS-Cache leeren + + + )} {/* === Logs === */} diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index 5cee4e2..83863ef 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -301,6 +301,12 @@ class AudioService { console.warn('[Audio] PcmPlaybackFinished-Subscription fehlgeschlagen:', err); } } + // App-Start: orphaned aria_tts_*.wav / aria_recording_*.mp4 aus dem Cache + // wegraeumen. Sammeln sich an wenn Sound mid-playback gestoppt wird (Anruf, + // Mute, Barge-In) — der completion-callback feuert dann nicht und die Datei + // bleibt liegen. 5min-Threshold damit gerade aktiv geschriebene Files sicher + // sind. cleanupOnStartup ist async, blockt den Constructor nicht. + this._cleanupStaleCacheFiles(5 * 60 * 1000).catch(() => {}); } /** AudioFocus mit kleiner Verzoegerung freigeben — Spotify/YouTube @@ -1249,19 +1255,29 @@ class AudioService { } } - /** Alte Aufnahme- und TTS-Files aus dem Cache loeschen (>30s alt). */ - private async _cleanupStaleCacheFiles(): Promise { + /** Alte Aufnahme- und TTS-Files aus dem Cache loeschen. + * Default 30s — verwendet beim Mikro-Start (kurze Lebensdauer reicht). + * App-Start nutzt 5min damit gerade aktive Files nicht erwischt werden. */ + private async _cleanupStaleCacheFiles(maxAgeMs: number = 30000): Promise { try { const files = await RNFS.readDir(RNFS.CachesDirectoryPath); const now = Date.now(); + let removed = 0; + let freedBytes = 0; for (const f of files) { if (!f.isFile()) continue; if (!f.name.startsWith('aria_recording_') && !f.name.startsWith('aria_tts_')) continue; const age = now - (f.mtime ? f.mtime.getTime() : 0); - if (age > 30000) { + if (age > maxAgeMs) { + freedBytes += parseInt(f.size as any, 10) || 0; await RNFS.unlink(f.path).catch(() => {}); + removed += 1; } } + if (removed > 0) { + console.log('[Audio] Cache-Cleanup: %d Files entfernt, %.1fMB freigegeben', + removed, freedBytes / 1024 / 1024); + } } catch { // silent — cleanup ist best-effort } @@ -1288,6 +1304,43 @@ class AudioService { // silent } } + + /** Aktuelle Groesse des TTS-Caches. */ + async getTtsCacheSize(): Promise<{ count: number; totalMB: number }> { + let count = 0; + let total = 0; + try { + const dir = `${RNFS.DocumentDirectoryPath}/tts_cache`; + if (await RNFS.exists(dir)) { + const files = await RNFS.readDir(dir); + for (const f of files) { + if (!f.isFile() || !f.name.endsWith('.wav')) continue; + count += 1; + total += parseInt(f.size as any, 10) || 0; + } + } + } catch {} + return { count, totalMB: total / 1024 / 1024 }; + } + + /** TTS-Cache komplett leeren (Settings-Button). */ + async clearTtsCache(): Promise<{ removed: number; freedMB: number }> { + let removed = 0; + let freed = 0; + try { + const dir = `${RNFS.DocumentDirectoryPath}/tts_cache`; + if (!(await RNFS.exists(dir))) return { removed: 0, freedMB: 0 }; + const files = await RNFS.readDir(dir); + for (const f of files) { + if (!f.isFile() || !f.name.endsWith('.wav')) continue; + const size = parseInt(f.size as any, 10) || 0; + await RNFS.unlink(f.path).catch(() => {}); + removed += 1; + freed += size; + } + } catch {} + return { removed, freedMB: freed / 1024 / 1024 }; + } } // Singleton