Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89e3a195a3 | |||
| f023ba0ac5 | |||
| a0570ef8f7 | |||
| facde1fef7 | |||
| 38106a2096 | |||
| a476afb311 | |||
| db4c7b9b72 | |||
| 3bc490b485 | |||
| dd6d70c46e | |||
| b1eaf42fef | |||
| fb9e5dcd10 | |||
| f95e71463f | |||
| 1088bff43d | |||
| cad68db2a2 | |||
| 50b10c8ac0 | |||
| a8b586ec92 | |||
| 632e1e4fa1 |
@@ -13,6 +13,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import ChatScreen from './src/screens/ChatScreen';
|
||||
import SettingsScreen from './src/screens/SettingsScreen';
|
||||
import rvs from './src/services/rvs';
|
||||
import { initLogger } from './src/services/logger';
|
||||
|
||||
// --- Navigation ---
|
||||
|
||||
@@ -44,6 +45,10 @@ const TAB_ICONS: Record<string, { active: string; inactive: string }> = {
|
||||
const App: React.FC = () => {
|
||||
// Beim Start: gespeicherte RVS-Konfiguration laden und verbinden
|
||||
useEffect(() => {
|
||||
// Verbose-Logging-Setting laden BEVOR andere Module loslegen.
|
||||
// initLogger ist async aber blockt nichts — solange er noch laueft,
|
||||
// loggen wir normal (Default an), danach respektiert console.log das Setting.
|
||||
initLogger().catch(() => {});
|
||||
const initConnection = async () => {
|
||||
const config = await rvs.loadConfig();
|
||||
if (config) {
|
||||
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10000
|
||||
versionName "0.1.0.0"
|
||||
versionCode 10008
|
||||
versionName "0.1.0.8"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -131,6 +131,58 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
||||
promise.resolve(true)
|
||||
}
|
||||
|
||||
/** Den USAGE_MEDIA-Focus-Stack im System aufmischen, damit Spotify/YouTube
|
||||
* resumen wenn ein anderer Player (z.B. react-native-sound) seinen Focus
|
||||
* nicht ordnungsgemaess released hat. Strategie: kurz selbst USAGE_MEDIA
|
||||
* GAIN beanspruchen — das System invalidiert dabei den haengenden Stack-
|
||||
* Eintrag des anderen Players — und sofort wieder abandonen. Spotify
|
||||
* bekommt den Focus-Gain und resumed.
|
||||
*
|
||||
* Workaround fuer das react-native-sound-Bug: Sound.stop()/release()
|
||||
* laesst den AudioFocusRequest haengen.
|
||||
*/
|
||||
@ReactMethod
|
||||
fun kickReleaseMedia(promise: Promise) {
|
||||
val am = audioManager()
|
||||
if (am == null) {
|
||||
promise.resolve(false)
|
||||
return
|
||||
}
|
||||
// Async laufen lassen — wir wollen einen request, Pause, dann abandon.
|
||||
// Ohne Pause merkt das System (und damit Spotify) die kurze Owner-
|
||||
// Wechsel oft gar nicht. 250ms reicht erfahrungsgemaess fuer den
|
||||
// Focus-Stack-Refresh.
|
||||
Thread {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val attrs = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
val kickListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||
val kickReq = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAudioAttributes(attrs)
|
||||
.setOnAudioFocusChangeListener(kickListener)
|
||||
.build()
|
||||
am.requestAudioFocus(kickReq)
|
||||
Thread.sleep(250)
|
||||
am.abandonAudioFocusRequest(kickReq)
|
||||
} else {
|
||||
val kickListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||
@Suppress("DEPRECATION")
|
||||
am.requestAudioFocus(kickListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
|
||||
Thread.sleep(250)
|
||||
@Suppress("DEPRECATION")
|
||||
am.abandonAudioFocus(kickListener)
|
||||
}
|
||||
Log.i(TAG, "kickReleaseMedia: USAGE_MEDIA-Stack aufgemischt (250ms Pause)")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "kickReleaseMedia failed: ${e.message}")
|
||||
}
|
||||
}.start()
|
||||
promise.resolve(true)
|
||||
}
|
||||
|
||||
private fun release() {
|
||||
val am = audioManager() ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.0.0",
|
||||
"version": "0.1.0.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -1038,19 +1038,24 @@ const ChatScreen: React.FC = () => {
|
||||
{!isUser && item.text.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.playButton}
|
||||
onPress={() => {
|
||||
if (item.audioPath) {
|
||||
audioService.playFromPath(item.audioPath);
|
||||
} else {
|
||||
// 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 || '',
|
||||
});
|
||||
onPress={async () => {
|
||||
// Erst lokalen Cache pruefen — audioPath kann auf eine geloeschte
|
||||
// Datei zeigen (TTS-Cache geleert oder Auto-Cleanup). In dem Fall
|
||||
// ueber RVS neu rendern lassen statt stumm zu bleiben.
|
||||
const cachePath = item.audioPath?.replace(/^file:\/\//, '') || '';
|
||||
const cached = cachePath ? await RNFS.exists(cachePath).catch(() => false) : false;
|
||||
if (cached) {
|
||||
audioService.playFromPath(item.audioPath!);
|
||||
return;
|
||||
}
|
||||
// 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>
|
||||
|
||||
@@ -50,6 +50,8 @@ import {
|
||||
TTS_SPEED_MAX,
|
||||
TTS_SPEED_STORAGE_KEY,
|
||||
} from '../services/audio';
|
||||
import audioService from '../services/audio';
|
||||
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
|
||||
import {
|
||||
isWakeReadySoundEnabled,
|
||||
setWakeReadySoundEnabled,
|
||||
@@ -135,6 +137,8 @@ const SettingsScreen: React.FC = () => {
|
||||
const [vadSilenceDb, setVadSilenceDb] = useState<number | null>(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 [verboseLogging, setVerboseLoggingState] = useState<boolean>(isVerboseLogging());
|
||||
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
||||
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
||||
const [wakeStatus, setWakeStatus] = useState<string>('');
|
||||
@@ -224,6 +228,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,11 +1256,65 @@ const SettingsScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</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 === */}
|
||||
{currentSection === 'protocol' && (<>
|
||||
<Text style={styles.sectionTitle}>Protokoll</Text>
|
||||
|
||||
{/* Verbose-Logging-Toggle */}
|
||||
<View style={styles.card}>
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>Verbose Logging</Text>
|
||||
<Switch
|
||||
value={verboseLogging}
|
||||
onValueChange={(v) => {
|
||||
setVerboseLogging(v);
|
||||
setVerboseLoggingState(v);
|
||||
}}
|
||||
trackColor={{ false: '#3A3A52', true: '#0096FF' }}
|
||||
thumbColor={verboseLogging ? '#FFFFFF' : '#666680'}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.toggleHint}>
|
||||
Wenn aus: console.log wird global stummgeschaltet (Speicher schonen).
|
||||
Warnungen und Fehler bleiben immer aktiv. Bei Bedarf einschalten zum
|
||||
Debuggen via adb logcat.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
{/* Tab-Umschalter */}
|
||||
<View style={styles.tabRow}>
|
||||
|
||||
@@ -41,6 +41,8 @@ const { AudioFocus, PcmStreamPlayer } = NativeModules as {
|
||||
requestDuck: () => Promise<boolean>;
|
||||
requestExclusive: () => Promise<boolean>;
|
||||
release: () => Promise<boolean>;
|
||||
kickReleaseMedia: () => Promise<boolean>;
|
||||
getMode?: () => Promise<number>;
|
||||
};
|
||||
PcmStreamPlayer?: {
|
||||
start: (sampleRate: number, channels: number, prerollSeconds: number) => Promise<boolean>;
|
||||
@@ -301,6 +303,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
|
||||
@@ -310,13 +318,19 @@ class AudioService {
|
||||
* unterdrueckt — der Focus bleibt fuer die ganze Konversation gehalten. */
|
||||
private _releaseFocusDeferred(): void {
|
||||
if (this._conversationFocusActive) {
|
||||
console.log('[Audio] _releaseFocusDeferred: Conversation aktiv → kein Release');
|
||||
this._cancelDeferredFocusRelease();
|
||||
return;
|
||||
}
|
||||
this._cancelDeferredFocusRelease();
|
||||
console.log('[Audio] _releaseFocusDeferred: in %dms', this.FOCUS_RELEASE_DELAY_MS);
|
||||
this.focusReleaseTimer = setTimeout(() => {
|
||||
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(() => {});
|
||||
}, this.FOCUS_RELEASE_DELAY_MS);
|
||||
}
|
||||
@@ -363,6 +377,10 @@ class AudioService {
|
||||
console.log('[Audio] pauseForCall: %s', reason || '(no reason)');
|
||||
this._conversationFocusActive = false;
|
||||
this._pausedForCall = true;
|
||||
// Queue + isPlaying ruecksetzen — sonst klemmt der naechste Play-Button
|
||||
// (playAudio sieht isPlaying=true und ruft _playNext nicht mehr auf).
|
||||
this.audioQueue = [];
|
||||
this.isPlaying = false;
|
||||
// Foreground-Service stoppen — Notification waere sonst irrefuehrend
|
||||
stopBackgroundAudio().catch(() => {});
|
||||
// SoundPool/RNSound (Resume-Sound, Play-Button) stoppen — nicht relevant fuer Auto-Resume
|
||||
@@ -778,8 +796,13 @@ class AudioService {
|
||||
if (!base64Data) return;
|
||||
// Mute-Flag respektieren — robust gegen Race-Conditions zwischen User-
|
||||
// Klick auf Mute und einem TTS-Chunk der im selben Tick eintrifft.
|
||||
if (this._muted) return;
|
||||
if (this._muted) {
|
||||
console.log('[Audio] playAudio: muted=true → skip');
|
||||
return;
|
||||
}
|
||||
this.audioQueue.push(base64Data);
|
||||
console.log('[Audio] playAudio: queued (queue=%d isPlaying=%s pausedForCall=%s)',
|
||||
this.audioQueue.length, this.isPlaying, this._pausedForCall);
|
||||
if (!this.isPlaying) {
|
||||
this._playNext();
|
||||
}
|
||||
@@ -845,11 +868,16 @@ class AudioService {
|
||||
final?: boolean;
|
||||
silent?: boolean;
|
||||
}): Promise<string> {
|
||||
// _stoppedMessageId: User hat diese Antwort mid-Wiedergabe gestoppt
|
||||
// (Mute geklickt). Auch wenn Mute jetzt wieder aus ist, soll diese
|
||||
// Antwort nicht weiterspielen. Erst eine neue messageId resetted das.
|
||||
const incomingMsgId = payload.messageId || '';
|
||||
const stoppedByUser = !!this._stoppedMessageId && incomingMsgId === this._stoppedMessageId;
|
||||
// Globaler Mute-Flag uebersteuert das per-Call silent — verhindert
|
||||
// Race-Conditions wenn der User zwischen Chunks den Mute-Knopf drueckt.
|
||||
// _pausedForCall: AudioTrack ist gestoppt waehrend Anruf — Chunks weiter
|
||||
// sammeln (fuer WAV-Cache), aber NICHT in den Player schicken.
|
||||
const silent = !!payload.silent || this._muted || this._pausedForCall;
|
||||
const silent = !!payload.silent || this._muted || this._pausedForCall || stoppedByUser;
|
||||
if (!silent && !PcmStreamPlayer) {
|
||||
console.warn('[Audio] PcmStreamPlayer Native Module nicht verfuegbar');
|
||||
return '';
|
||||
@@ -890,6 +918,13 @@ class AudioService {
|
||||
this.pausedMessageId = '';
|
||||
this.pausedPosition = 0;
|
||||
}
|
||||
// Stop-Marker zuruecksetzen wenn neue messageId — neue Antwort darf
|
||||
// wieder normal abspielen, egal ob Mute zwischendurch aktiv war.
|
||||
if (this._stoppedMessageId && this._stoppedMessageId !== messageId) {
|
||||
console.log('[Audio] Neue Antwort (msgId=%s) — Stop-Marker fuer %s zurueckgesetzt',
|
||||
messageId, this._stoppedMessageId);
|
||||
this._stoppedMessageId = '';
|
||||
}
|
||||
this.pcmStreamActive = true;
|
||||
this.pcmMessageId = messageId;
|
||||
this.pcmSampleRate = sampleRate;
|
||||
@@ -1116,11 +1151,13 @@ class AudioService {
|
||||
}
|
||||
|
||||
this.currentSound = sound;
|
||||
console.log('[Audio] Sound.play startet (path=%s)', soundPath);
|
||||
|
||||
// Naechstes Audio schon vorbereiten waehrend dieses abspielt
|
||||
this._preloadNext();
|
||||
|
||||
sound.play((success) => {
|
||||
console.log('[Audio] Sound.play callback: success=%s queue=%d', success, this.audioQueue.length);
|
||||
if (!success) console.warn('[Audio] Wiedergabe fehlgeschlagen');
|
||||
sound.release();
|
||||
this.currentSound = null;
|
||||
@@ -1156,16 +1193,37 @@ class AudioService {
|
||||
* abgespielt. Wird in pauseForCall gesetzt, in endCallPause/resumeFrom-
|
||||
* Interruption zurueckgenommen. */
|
||||
private _pausedForCall: boolean = false;
|
||||
/** Wenn der User mid-Wiedergabe Mute drueckt: messageId der ABGEBROCHENEN
|
||||
* Antwort merken. Folge-Chunks dieser msgId werden silent ignoriert, auch
|
||||
* wenn der User Mute wieder ausschaltet — kein "Resume mid-Antwort". Eine
|
||||
* NEUE messageId resetted das, dann spielt's wieder normal. */
|
||||
private _stoppedMessageId: string = '';
|
||||
setMuted(muted: boolean): void {
|
||||
console.log('[Audio] setMuted: %s (currentSound=%s pcmStreamActive=%s)',
|
||||
muted, this.currentSound ? 'aktiv' : 'null', this.pcmStreamActive);
|
||||
this._muted = muted;
|
||||
if (muted) this.stopPlayback();
|
||||
if (muted) {
|
||||
// Aktuell laufende Antwort als "verworfen" markieren — nachfolgende
|
||||
// chunks dieser msgId werden silent gehalten auch wenn der User Mute
|
||||
// gleich wieder ausschaltet. Erst eine NEUE Antwort darf wieder reden.
|
||||
const activeMsgId = this.pcmMessageId || this.currentPlaybackMsgId;
|
||||
if (activeMsgId) {
|
||||
this._stoppedMessageId = activeMsgId;
|
||||
console.log('[Audio] Antwort %s als gestoppt markiert', activeMsgId);
|
||||
}
|
||||
this.stopPlayback();
|
||||
}
|
||||
}
|
||||
isMuted(): boolean { return this._muted; }
|
||||
|
||||
/** Laufende Wiedergabe stoppen + Queue leeren */
|
||||
stopPlayback(): void {
|
||||
// Idempotent: wenn nichts mehr aktiv ist, NICHT noch einen Focus-Release/
|
||||
// Kick-Cycle anstossen — Re-Renders triggern setMuted oft mehrfach hinter-
|
||||
// einander, und jeder weitere Kick lässt Spotify nochmal kurz pausieren.
|
||||
const hasAnything = !!(this.currentSound || this.resumeSound || this.preloadedSound
|
||||
|| this.pcmStreamActive || this.audioQueue.length || this.isPlaying);
|
||||
if (!hasAnything) return;
|
||||
console.log('[Audio] stopPlayback: currentSound=%s queue=%d pcm=%s',
|
||||
this.currentSound ? 'aktiv' : 'null', this.audioQueue.length, this.pcmStreamActive);
|
||||
// Foreground-Service auch stoppen — sonst bleibt die Notification haengen
|
||||
@@ -1201,6 +1259,10 @@ class AudioService {
|
||||
// Audio-Focus sofort freigeben — User hat explizit abgebrochen
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.release().catch(() => {});
|
||||
// Focus-Stack immer aufmischen — bei aelteren Nachrichten die ueber
|
||||
// tts_request (PCM-Stream) re-rendert wurden, bleibt Spotify ohne den
|
||||
// Kick auch pausiert. Kostet nichts, deckt beide Pfade ab.
|
||||
AudioFocus?.kickReleaseMedia?.().catch(() => {});
|
||||
}
|
||||
|
||||
// --- Status & Callbacks ---
|
||||
@@ -1240,19 +1302,29 @@ class AudioService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Alte Aufnahme- und TTS-Files aus dem Cache loeschen (>30s alt). */
|
||||
private async _cleanupStaleCacheFiles(): Promise<void> {
|
||||
/** 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<void> {
|
||||
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
|
||||
}
|
||||
@@ -1279,6 +1351,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
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Verbose-Logging-Toggle: console.log laesst sich global stummschalten.
|
||||
* console.warn/console.error bleiben immer an — Fehler will man immer sehen.
|
||||
*
|
||||
* Default: an (true). Toggle ueber Settings → Protokoll → Verbose Logging.
|
||||
* Beim Start wird der gespeicherte Wert geladen, vorher loggen wir normal.
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
|
||||
|
||||
// Original-console.log retten, damit wir die Wrapper jederzeit wieder
|
||||
// "scharf" stellen koennen (sonst waere ein Toggle-an nach -aus tot).
|
||||
const originalLog = console.log.bind(console);
|
||||
const noop = () => {};
|
||||
|
||||
let _verbose = true;
|
||||
|
||||
function applyState(): void {
|
||||
console.log = _verbose ? originalLog : noop;
|
||||
}
|
||||
|
||||
/** Wert aus AsyncStorage laden und anwenden. Beim App-Start aufrufen. */
|
||||
export async function initLogger(): Promise<void> {
|
||||
try {
|
||||
const v = await AsyncStorage.getItem(VERBOSE_LOGGING_KEY);
|
||||
_verbose = v !== 'false'; // default: true
|
||||
} catch {}
|
||||
applyState();
|
||||
}
|
||||
|
||||
export function isVerboseLogging(): boolean {
|
||||
return _verbose;
|
||||
}
|
||||
|
||||
export function setVerboseLogging(verbose: boolean): void {
|
||||
_verbose = verbose;
|
||||
applyState();
|
||||
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
|
||||
}
|
||||
Reference in New Issue
Block a user