Compare commits

..

4 Commits

Author SHA1 Message Date
duffyduck f95e71463f release: bump version to 0.1.0.3 2026-05-10 16:43:37 +02:00
duffyduck 1088bff43d fix(chat): Play-Button rendert neu wenn Cache-Datei weg
Vorher: Button checkte nur ob audioPath gesetzt ist — auf eine geloeschte
Cache-Datei hat aber nichts geprueft. playFromPath warntete nur und
returnte stumm. Jetzt wird VOR playFromPath die Existenz geprueft, sonst
geht's ueber tts_request an die Bridge zum Neu-Rendern.

Plus: Logs in Sound.play-Callback und _releaseFocusDeferred fuer den
"Spotify resumed nicht nach Replay"-Bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:42:38 +02:00
duffyduck cad68db2a2 release: bump version to 0.1.0.2 2026-05-10 16:38:00 +02:00
duffyduck 50b10c8ac0 feat(audio): Cache-Cleanup beim App-Start + TTS-Cache-Settings-Button
- 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) <noreply@anthropic.com>
2026-05-10 16:36:01 +02:00
5 changed files with 120 additions and 19 deletions
+2 -2
View File
@@ -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 10001 versionCode 10003
versionName "0.1.0.1" versionName "0.1.0.3"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.1.0.1", "version": "0.1.0.3",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+17 -12
View File
@@ -1038,19 +1038,24 @@ const ChatScreen: React.FC = () => {
{!isUser && item.text.length > 0 && ( {!isUser && item.text.length > 0 && (
<TouchableOpacity <TouchableOpacity
style={styles.playButton} style={styles.playButton}
onPress={() => { onPress={async () => {
if (item.audioPath) { // Erst lokalen Cache pruefen — audioPath kann auf eine geloeschte
audioService.playFromPath(item.audioPath); // Datei zeigen (TTS-Cache geleert oder Auto-Cleanup). In dem Fall
} else { // ueber RVS neu rendern lassen statt stumm zu bleiben.
// messageId mitschicken damit die Bridge das generierte Audio const cachePath = item.audioPath?.replace(/^file:\/\//, '') || '';
// wieder mit der Nachricht verknuepft (fuer den naechsten Replay aus Cache) const cached = cachePath ? await RNFS.exists(cachePath).catch(() => false) : false;
rvs.send('tts_request' as any, { if (cached) {
text: item.text, audioService.playFromPath(item.audioPath!);
voice: localXttsVoiceRef.current, return;
speed: ttsSpeedRef.current,
messageId: item.messageId || '',
});
} }
// messageId mitschicken damit die Bridge das generierte Audio
// wieder mit der Nachricht verknuepft (fuer den naechsten Replay aus Cache)
rvs.send('tts_request' as any, {
text: item.text,
voice: localXttsVoiceRef.current,
speed: ttsSpeedRef.current,
messageId: item.messageId || '',
});
}} }}
> >
<Text style={styles.playButtonText}>{'\uD83D\uDD0A'}</Text> <Text style={styles.playButtonText}>{'\uD83D\uDD0A'}</Text>
+35
View File
@@ -50,6 +50,7 @@ import {
TTS_SPEED_MAX, TTS_SPEED_MAX,
TTS_SPEED_STORAGE_KEY, TTS_SPEED_STORAGE_KEY,
} from '../services/audio'; } from '../services/audio';
import audioService from '../services/audio';
import { import {
isWakeReadySoundEnabled, isWakeReadySoundEnabled,
setWakeReadySoundEnabled, setWakeReadySoundEnabled,
@@ -135,6 +136,7 @@ const SettingsScreen: React.FC = () => {
const [vadSilenceDb, setVadSilenceDb] = useState<number | null>(null); const [vadSilenceDb, setVadSilenceDb] = useState<number | null>(null);
const [showVadInfo, setShowVadInfo] = useState(false); const [showVadInfo, setShowVadInfo] = useState(false);
const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null); const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT); const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD); const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
const [wakeStatus, setWakeStatus] = useState<string>(''); const [wakeStatus, setWakeStatus] = useState<string>('');
@@ -224,6 +226,7 @@ const SettingsScreen: React.FC = () => {
}); });
isWakeReadySoundEnabled().then(setWakeReadySound); isWakeReadySoundEnabled().then(setWakeReadySound);
updateService.getApkCacheSize().then(setApkCacheInfo).catch(() => {}); updateService.getApkCacheSize().then(setApkCacheInfo).catch(() => {});
audioService.getTtsCacheSize().then(setTtsCacheInfo).catch(() => {});
AsyncStorage.getItem('aria_xtts_voice').then(saved => { AsyncStorage.getItem('aria_xtts_voice').then(saved => {
if (saved) setXttsVoice(saved); if (saved) setXttsVoice(saved);
}); });
@@ -1251,6 +1254,38 @@ const SettingsScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* === TTS-Cache === */}
<Text style={[styles.sectionTitle, {marginTop: 16}]}>TTS-Cache</Text>
<View style={styles.card}>
<Text style={styles.toggleHint}>
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.
</Text>
<Text style={[styles.storageSizeText, {marginTop: 8}]}>
{ttsCacheInfo === null ? '...' :
ttsCacheInfo.count === 0 ? 'leer' :
`${ttsCacheInfo.count} WAV${ttsCacheInfo.count === 1 ? '' : 's'} · ${ttsCacheInfo.totalMB.toFixed(1)}MB`}
</Text>
<TouchableOpacity
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,59,48,0.15)'}]}
onPress={async () => {
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);
}}
>
<Text style={[styles.clearButtonText, {color: '#FF3B30'}]}>TTS-Cache leeren</Text>
</TouchableOpacity>
</View>
</>)} </>)}
{/* === Logs === */} {/* === Logs === */}
+65 -4
View File
@@ -301,6 +301,12 @@ class AudioService {
console.warn('[Audio] PcmPlaybackFinished-Subscription fehlgeschlagen:', err); 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 /** AudioFocus mit kleiner Verzoegerung freigeben — Spotify/YouTube
@@ -310,13 +316,19 @@ class AudioService {
* unterdrueckt — der Focus bleibt fuer die ganze Konversation gehalten. */ * unterdrueckt — der Focus bleibt fuer die ganze Konversation gehalten. */
private _releaseFocusDeferred(): void { private _releaseFocusDeferred(): void {
if (this._conversationFocusActive) { if (this._conversationFocusActive) {
console.log('[Audio] _releaseFocusDeferred: Conversation aktiv → kein Release');
this._cancelDeferredFocusRelease(); this._cancelDeferredFocusRelease();
return; return;
} }
this._cancelDeferredFocusRelease(); this._cancelDeferredFocusRelease();
console.log('[Audio] _releaseFocusDeferred: in %dms', this.FOCUS_RELEASE_DELAY_MS);
this.focusReleaseTimer = setTimeout(() => { this.focusReleaseTimer = setTimeout(() => {
this.focusReleaseTimer = null; this.focusReleaseTimer = null;
if (this._conversationFocusActive) return; if (this._conversationFocusActive) {
console.log('[Audio] Focus-Release abgebrochen (Conversation jetzt aktiv)');
return;
}
console.log('[Audio] AudioFocus jetzt released');
AudioFocus?.release().catch(() => {}); AudioFocus?.release().catch(() => {});
}, this.FOCUS_RELEASE_DELAY_MS); }, this.FOCUS_RELEASE_DELAY_MS);
} }
@@ -1125,11 +1137,13 @@ class AudioService {
} }
this.currentSound = sound; this.currentSound = sound;
console.log('[Audio] Sound.play startet (path=%s)', soundPath);
// Naechstes Audio schon vorbereiten waehrend dieses abspielt // Naechstes Audio schon vorbereiten waehrend dieses abspielt
this._preloadNext(); this._preloadNext();
sound.play((success) => { sound.play((success) => {
console.log('[Audio] Sound.play callback: success=%s queue=%d', success, this.audioQueue.length);
if (!success) console.warn('[Audio] Wiedergabe fehlgeschlagen'); if (!success) console.warn('[Audio] Wiedergabe fehlgeschlagen');
sound.release(); sound.release();
this.currentSound = null; this.currentSound = null;
@@ -1249,19 +1263,29 @@ class AudioService {
} }
} }
/** Alte Aufnahme- und TTS-Files aus dem Cache loeschen (>30s alt). */ /** Alte Aufnahme- und TTS-Files aus dem Cache loeschen.
private async _cleanupStaleCacheFiles(): Promise<void> { * 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<void> {
try { try {
const files = await RNFS.readDir(RNFS.CachesDirectoryPath); const files = await RNFS.readDir(RNFS.CachesDirectoryPath);
const now = Date.now(); const now = Date.now();
let removed = 0;
let freedBytes = 0;
for (const f of files) { for (const f of files) {
if (!f.isFile()) continue; if (!f.isFile()) continue;
if (!f.name.startsWith('aria_recording_') && !f.name.startsWith('aria_tts_')) continue; if (!f.name.startsWith('aria_recording_') && !f.name.startsWith('aria_tts_')) continue;
const age = now - (f.mtime ? f.mtime.getTime() : 0); 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(() => {}); 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 { } catch {
// silent — cleanup ist best-effort // silent — cleanup ist best-effort
} }
@@ -1288,6 +1312,43 @@ class AudioService {
// silent // 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 // Singleton