feat(audio): Auto-Resume nach Anruf ab der gemerkten Position
Stefans Idee: Position beim Halt merken (Date.now() - playbackStart - leadingSilence), nach dem Auflegen ab da weitermachen. Wenn der Cache noch nicht komplett ist (final-Marker kam waehrend Anruf), warten wir bis zu 30s auf das WAV — meistens ist's schon da weil das Telefonat laenger als die Antwort dauerte. audio.ts: - captureInterruption(): merkt position + messageId, returnt Sekunden - resumeFromInterruption(maxWaitMs): wartet auf WAV-Cache, lädt mit Sound, setCurrentTime(position), play - Tracking-Felder: playbackStartTime, currentPlaybackMsgId, pausedX phoneCall.ts: - _haltForCall ruft captureInterruption() VOR haltAllPlayback - _resumeAfterCall triggert resumeFromInterruption(30s) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -384,7 +384,7 @@ API-Endpoint fuer andere Services: `GET http://localhost:3001/api/session`
|
|||||||
- **VAD (Voice Activity Detection)**: Adaptive Schwelle (Baseline aus ersten 500ms Mic-Pegel + 6dB Offset). Konfigurierbare Stille-Toleranz (1.0–8.0s, Default 2.8s) bevor Auto-Stop greift. Max-Aufnahme einstellbar (1–30 min, Default 5 min)
|
- **VAD (Voice Activity Detection)**: Adaptive Schwelle (Baseline aus ersten 500ms Mic-Pegel + 6dB Offset). Konfigurierbare Stille-Toleranz (1.0–8.0s, Default 2.8s) bevor Auto-Stop greift. Max-Aufnahme einstellbar (1–30 min, Default 5 min)
|
||||||
- **Barge-In**: Wenn du waehrend ARIAs Antwort eine neue Sprach-/Text-Nachricht reinschickst, wird sie unterbrochen + bekommt den Hint "das ist eine Korrektur"
|
- **Barge-In**: Wenn du waehrend ARIAs Antwort eine neue Sprach-/Text-Nachricht reinschickst, wird sie unterbrochen + bekommt den Hint "das ist eine Korrektur"
|
||||||
- **Wake-Word waehrend TTS**: Du kannst "Computer" sagen waehrend ARIA noch redet — AcousticEchoCanceler verhindert dass ARIAs eigene Stimme das Wake-Word triggert
|
- **Wake-Word waehrend TTS**: Du kannst "Computer" sagen waehrend ARIA noch redet — AcousticEchoCanceler verhindert dass ARIAs eigene Stimme das Wake-Word triggert
|
||||||
- **Anruf-Pause**: TTS verstummt automatisch wenn das Telefon klingelt (READ_PHONE_STATE Permission)
|
- **Anruf-Pause + Auto-Resume**: TTS verstummt bei klassischem Anruf oder VoIP-Call (WhatsApp/Signal/Discord). Nach dem Auflegen geht ARIA von der **genauen Stelle** weiter wo sie unterbrochen wurde — die App misst die Position vom Wiedergabe-Anfang und nutzt den WAV-Cache der Antwort
|
||||||
- **Speech Gate**: Aufnahme wird verworfen wenn keine Sprache erkannt
|
- **Speech Gate**: Aufnahme wird verworfen wenn keine Sprache erkannt
|
||||||
- **STT (Speech-to-Text)**: 16kHz mono → Bridge → Gamebox-Whisper (CUDA) → Text im Chat. Fast in Echtzeit.
|
- **STT (Speech-to-Text)**: 16kHz mono → Bridge → Gamebox-Whisper (CUDA) → Text im Chat. Fast in Echtzeit.
|
||||||
- **"ARIA denkt..." Indicator**: Zeigt live den Status vom Core (Denken, Tool, Schreiben) + Abbrechen-Button
|
- **"ARIA denkt..." Indicator**: Zeigt live den Status vom Core (Denken, Tool, Schreiben) + Abbrechen-Button
|
||||||
@@ -864,7 +864,7 @@ docker exec aria-core ssh aria-wohnung hostname
|
|||||||
- [x] Audio-Pause statt Ducking (TRANSIENT statt MAY_DUCK) + release-Timing fix
|
- [x] Audio-Pause statt Ducking (TRANSIENT statt MAY_DUCK) + release-Timing fix
|
||||||
- [x] VAD-Stille-Toleranz einstellbar (1-8s) + adaptive Mikro-Baseline + Max-Aufnahme einstellbar (1-30 min)
|
- [x] VAD-Stille-Toleranz einstellbar (1-8s) + adaptive Mikro-Baseline + Max-Aufnahme einstellbar (1-30 min)
|
||||||
- [x] Barge-In: User kann ARIA waehrend Antwort unterbrechen, aria-core bekommt Kontext-Hint
|
- [x] Barge-In: User kann ARIA waehrend Antwort unterbrechen, aria-core bekommt Kontext-Hint
|
||||||
- [x] Anruf-Pause: TTS verstummt bei eingehendem Anruf (PhoneStateListener)
|
- [x] Anruf-Pause + Auto-Resume: TTS verstummt bei Anruf, faehrt nach Auflegen ab der gemerkten Position fort (Date.now()-Tracking + WAV-Cache der Antwort)
|
||||||
- [x] Settings-Sub-Screens: 8 Kategorien statt langer Liste
|
- [x] Settings-Sub-Screens: 8 Kategorien statt langer Liste
|
||||||
- [x] APK ABI-Split arm64-v8a: 35 MB statt 136 MB
|
- [x] APK ABI-Split arm64-v8a: 35 MB statt 136 MB
|
||||||
- [x] Sprachnachrichten-Bubble: audioRequestId statt Substring-Match — keine vertauschten Bubbles mehr bei parallelen Aufnahmen
|
- [x] Sprachnachrichten-Bubble: audioRequestId statt Substring-Match — keine vertauschten Bubbles mehr bei parallelen Aufnahmen
|
||||||
|
|||||||
@@ -269,6 +269,20 @@ class AudioService {
|
|||||||
private vadAdaptiveSilenceDb: number = VAD_SILENCE_FALLBACK_DB;
|
private vadAdaptiveSilenceDb: number = VAD_SILENCE_FALLBACK_DB;
|
||||||
private vadAdaptiveSpeechDb: number = VAD_SPEECH_FALLBACK_DB;
|
private vadAdaptiveSpeechDb: number = VAD_SPEECH_FALLBACK_DB;
|
||||||
|
|
||||||
|
// Interruption-Tracking fuer Auto-Resume nach Anruf:
|
||||||
|
// - playbackStartTime: ms-Timestamp wenn AudioTrack tatsaechlich anfing
|
||||||
|
// abzuspielen (= _firePlaybackStarted)
|
||||||
|
// - currentPlaybackMsgId: welche Antwort lief gerade
|
||||||
|
// - pausedPosition / pausedMessageId: bei captureInterruption gemerkt
|
||||||
|
private playbackStartTime: number = 0;
|
||||||
|
private currentPlaybackMsgId: string = '';
|
||||||
|
private pausedPosition: number = 0; // Sekunden in der Audio-Datei
|
||||||
|
private pausedMessageId: string = '';
|
||||||
|
private resumeSound: Sound | null = null; // halten damit GC nicht zuschlaegt
|
||||||
|
// Leading-Silence wird im Native vor den Chunks geschrieben — beim
|
||||||
|
// Position-Berechnen vom playbackStarted abziehen
|
||||||
|
private readonly LEADING_SILENCE_SEC = 0.3;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.recorder = new AudioRecorderPlayer();
|
this.recorder = new AudioRecorderPlayer();
|
||||||
this.recorder.setSubscriptionDuration(0.1); // 100ms Metering-Updates
|
this.recorder.setSubscriptionDuration(0.1); // 100ms Metering-Updates
|
||||||
@@ -341,6 +355,84 @@ class AudioService {
|
|||||||
this.stopPlayback();
|
this.stopPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bei Anruf: aktuelle Wiedergabe-Position merken damit wir nach dem
|
||||||
|
* Auflegen von dort weitermachen koennen. Returnt Position in Sekunden
|
||||||
|
* oder 0 wenn nichts spielte. */
|
||||||
|
captureInterruption(): number {
|
||||||
|
if (!this.playbackStartTime || !this.currentPlaybackMsgId) {
|
||||||
|
this.pausedPosition = 0;
|
||||||
|
this.pausedMessageId = '';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const elapsedMs = Date.now() - this.playbackStartTime;
|
||||||
|
const positionSec = Math.max(0, elapsedMs / 1000 - this.LEADING_SILENCE_SEC);
|
||||||
|
this.pausedPosition = positionSec;
|
||||||
|
this.pausedMessageId = this.currentPlaybackMsgId;
|
||||||
|
console.log('[Audio] captureInterruption: msgId=%s pos=%ss',
|
||||||
|
this.pausedMessageId, positionSec.toFixed(2));
|
||||||
|
return positionSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nach Anruf-Ende: ab gemerkter Position weiterspielen. Wenn Cache noch
|
||||||
|
* nicht geschrieben (final kam waehrend Anruf vielleicht doch nicht),
|
||||||
|
* warten bis maxWaitMs und dann probieren. Returnt true wenn gestartet. */
|
||||||
|
async resumeFromInterruption(maxWaitMs: number = 30000): Promise<boolean> {
|
||||||
|
const msgId = this.pausedMessageId;
|
||||||
|
const position = this.pausedPosition;
|
||||||
|
if (!msgId) return false;
|
||||||
|
this.pausedMessageId = ''; // konsumieren
|
||||||
|
const cachePath = `${RNFS.DocumentDirectoryPath}/tts_cache/${msgId}.wav`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
while (Date.now() - startTime < maxWaitMs) {
|
||||||
|
try {
|
||||||
|
if (await RNFS.exists(cachePath)) {
|
||||||
|
return await this._playFromPathAtPosition(cachePath, position);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
console.warn('[Audio] resumeFromInterruption: WAV %s nicht binnen %dms verfuegbar',
|
||||||
|
msgId, maxWaitMs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _playFromPathAtPosition(path: string, positionSec: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Bestehende laufende Wiedergabe abbrechen damit wir sauber starten
|
||||||
|
if (this.resumeSound) {
|
||||||
|
try { this.resumeSound.stop(); this.resumeSound.release(); } catch {}
|
||||||
|
this.resumeSound = null;
|
||||||
|
}
|
||||||
|
const sound = await new Promise<Sound>((resolve, reject) => {
|
||||||
|
const s = new Sound(path.replace(/^file:\/\//, ''), '', (err) =>
|
||||||
|
err ? reject(err) : resolve(s));
|
||||||
|
});
|
||||||
|
// Audio-Focus anfordern damit Spotify pausiert
|
||||||
|
this._cancelDeferredFocusRelease();
|
||||||
|
AudioFocus?.requestDuck().catch(() => {});
|
||||||
|
this._firePlaybackStarted();
|
||||||
|
this.isPlaying = true;
|
||||||
|
this.resumeSound = sound;
|
||||||
|
console.log('[Audio] Resume von Position %ss aus %s',
|
||||||
|
positionSec.toFixed(2), path);
|
||||||
|
sound.setCurrentTime(Math.max(0, positionSec));
|
||||||
|
sound.play((success) => {
|
||||||
|
if (!success) console.warn('[Audio] Resume-Wiedergabe fehlgeschlagen');
|
||||||
|
try { sound.release(); } catch {}
|
||||||
|
if (this.resumeSound === sound) this.resumeSound = null;
|
||||||
|
this.isPlaying = false;
|
||||||
|
this.playbackFinishedListeners.forEach(cb => {
|
||||||
|
try { cb(); } catch (e) { console.warn('[Audio] cb err:', e); }
|
||||||
|
});
|
||||||
|
this._releaseFocusDeferred();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.warn('[Audio] _playFromPathAtPosition fehlgeschlagen:', err?.message || err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** True wenn ARIA gerade was abspielt — egal ob WAV-Queue oder PCM-Stream.
|
/** True wenn ARIA gerade was abspielt — egal ob WAV-Queue oder PCM-Stream.
|
||||||
* Nuetzlich fuer "Barge-In": wenn der User spricht waehrend ARIA spricht,
|
* Nuetzlich fuer "Barge-In": wenn der User spricht waehrend ARIA spricht,
|
||||||
* soll die ARIA-Wiedergabe abgebrochen + die neue User-Message verarbeitet
|
* soll die ARIA-Wiedergabe abgebrochen + die neue User-Message verarbeitet
|
||||||
@@ -876,6 +968,9 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _firePlaybackStarted(): void {
|
private _firePlaybackStarted(): void {
|
||||||
|
// Tracking fuer Auto-Resume nach Anruf-Pause
|
||||||
|
this.playbackStartTime = Date.now();
|
||||||
|
this.currentPlaybackMsgId = this.pcmMessageId || '';
|
||||||
this.playbackStartedListeners.forEach(cb => {
|
this.playbackStartedListeners.forEach(cb => {
|
||||||
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
|
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ class PhoneCallService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _haltForCall(toast: string): void {
|
private _haltForCall(toast: string): void {
|
||||||
|
// Position merken bevor wir den Stream killen — fuer Auto-Resume.
|
||||||
|
audioService.captureInterruption();
|
||||||
audioService.haltAllPlayback(toast);
|
audioService.haltAllPlayback(toast);
|
||||||
wakeWordService.pauseForCall().catch(() => {});
|
wakeWordService.pauseForCall().catch(() => {});
|
||||||
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
||||||
@@ -184,6 +186,14 @@ class PhoneCallService {
|
|||||||
private _resumeAfterCall(toast: string): void {
|
private _resumeAfterCall(toast: string): void {
|
||||||
wakeWordService.resumeFromCall().catch(() => {});
|
wakeWordService.resumeFromCall().catch(() => {});
|
||||||
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
||||||
|
// Auto-Resume: ab gemerkter Position weiterspielen wenn ARIA vor dem
|
||||||
|
// Anruf gerade redete. Wartet bis zu 30s auf den WAV-Cache (falls
|
||||||
|
// final-Marker erst nach dem Anruf-Ende kam).
|
||||||
|
audioService.resumeFromInterruption(30000).then(ok => {
|
||||||
|
if (ok) {
|
||||||
|
console.log('[PhoneCall] Auto-Resume von gemerkter Position gestartet');
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Wenn was anders ist, ist's ein Bug.
|
|||||||
| TTS zu Ende | nach 800ms resumed | (Conversation-Window)| (tts released) |
|
| TTS zu Ende | nach 800ms resumed | (Conversation-Window)| (tts released) |
|
||||||
| Eingehender Anruf (auch VoIP)| — | Mikro pausiert | aus |
|
| Eingehender Anruf (auch VoIP)| — | Mikro pausiert | aus |
|
||||||
| Anruf vorbei | — | Mikro wieder armed | aktiv ('wake') |
|
| Anruf vorbei | — | Mikro wieder armed | aktiv ('wake') |
|
||||||
|
| Anruf vorbei (Auto-Resume) | nach 800ms resumed | aus | aktiv ('tts') |
|
||||||
|
|
||||||
Wichtige Mechanismen:
|
Wichtige Mechanismen:
|
||||||
- **Underrun-Schutz** im PcmStreamPlayer fuettert Stille rein wenn die
|
- **Underrun-Schutz** im PcmStreamPlayer fuettert Stille rein wenn die
|
||||||
@@ -32,6 +33,11 @@ Wichtige Mechanismen:
|
|||||||
zu/bereit").
|
zu/bereit").
|
||||||
- **Anruf-Erkennung** ueber TelephonyManager (klassisch) + AudioFocus-
|
- **Anruf-Erkennung** ueber TelephonyManager (klassisch) + AudioFocus-
|
||||||
Loss-Listener mit Polling-Fallback (VoIP wie WhatsApp/Signal/Discord).
|
Loss-Listener mit Polling-Fallback (VoIP wie WhatsApp/Signal/Discord).
|
||||||
|
- **Auto-Resume nach Anruf**: beim Halt wird die Wiedergabe-Position
|
||||||
|
gemerkt (Date.now() - playbackStart - leadingSilence). Nach Auflegen
|
||||||
|
wartet die App bis zu 30s auf den WAV-Cache und spielt dann ab der
|
||||||
|
gemerkten Position weiter. Wenn das Telefonat länger als die Antwort
|
||||||
|
dauerte, ist der Cache schon fertig — instant Resume.
|
||||||
|
|
||||||
## Erledigt
|
## Erledigt
|
||||||
|
|
||||||
@@ -165,7 +171,6 @@ Wichtige Mechanismen:
|
|||||||
### App Features
|
### App Features
|
||||||
- [ ] Chat-History zuverlaessiger laden (AsyncStorage Race Condition)
|
- [ ] Chat-History zuverlaessiger laden (AsyncStorage Race Condition)
|
||||||
- [ ] Custom-Wake-Word-Upload via Diagnostic (eigene .onnx-Files ohne App-Rebuild)
|
- [ ] Custom-Wake-Word-Upload via Diagnostic (eigene .onnx-Files ohne App-Rebuild)
|
||||||
- [ ] Pause+Resume bei Anruf: aktuell wird der TTS-Stream bei Klingeln hart gestoppt, schoener waere Pause + Resume nach Auflegen
|
|
||||||
|
|
||||||
### Architektur
|
### Architektur
|
||||||
- [ ] Bilder: Claude Vision direkt nutzen (aktuell nur Dateipfad an ARIA)
|
- [ ] Bilder: Claude Vision direkt nutzen (aktuell nur Dateipfad an ARIA)
|
||||||
|
|||||||
Reference in New Issue
Block a user