Compare commits

...

13 Commits

Author SHA1 Message Date
duffyduck de8eeb69e2 release: bump version to 0.0.9.8 2026-05-10 15:46:36 +02:00
duffyduck f5970ce700 fix(audio): _firePlaybackStarted ueberschrieb playFromPath-Tracking mit leerem pcmMessageId
Logs zeigten: playFromPath setzt currentPlaybackMsgId='db710ff3-...', 9s
spaeter beim Anruf war captureInterruption msgId=(leer). Ursache:
_firePlaybackStarted setzt currentPlaybackMsgId blind aus pcmMessageId —
das ist beim Play-Button leer.

Jetzt nur noch setzen wenn ein PCM-Stream laeuft. Play-Button und Resume-
Sound setzen ihr Tracking selber im Caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:44:28 +02:00
duffyduck ef1a4436ca fix(bridge): WebSocket max_size auf 50MB — grosse Bilder/Uploads gehen wieder
Python websockets Default-Limit ist nur 1 MiB. Stefan's 4MB JPEG (5.8MB als
Base64) sprengte das, Bridge-Verbindung wurde silent gedroppt. App sah
nichts, Diagnostic kriegte kein file_saved, ARIA reagierte nicht — Kamera-
Bilder waren klein genug (<1MB) und gingen darum durch.

f5tts/whisper-bridges hatten max_size=50MB schon drin, nur aria_bridge
hatte's an beiden websockets.connect-Stellen vergessen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:42:48 +02:00
duffyduck 981779cd9e release: bump version to 0.0.9.7 2026-05-10 15:37:45 +02:00
duffyduck 3dcd2ae0b4 fix(audio): msgId-Regex liberaler — auch nicht-UUID-Dateinamen werden erkannt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:36:43 +02:00
duffyduck 2750b867a3 release: bump version to 0.0.9.6 2026-05-10 15:29:03 +02:00
duffyduck f6424add6c debug(chat): Logs fuer Anhang-Send-Pipeline
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:27:24 +02:00
duffyduck 2dfd21d1d0 fix(audio): Play-Button setzt jetzt auch Wiedergabe-Tracking — Anruf-Test via Playback funktioniert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:22:21 +02:00
duffyduck 9d9ddc730b debug+fix(audio): mehr Anruf-Logs + Tracking auch beim Resume-Sound
Im Test 2 (zweiter Anruf in derselben Antwort) kam weder captureInterruption
noch resumeFromInterruption als Log — beide returnen frueh ohne Hinweis warum.
Jetzt loggen sie auch den Skip-Pfad damit man sieht ob's der idempotent-Guard
oder fehlende playbackStartTime ist.

Plus: _playFromPathAtPosition aktualisiert jetzt currentPlaybackMsgId und
playbackStartTime — sonst stehen die auf den Werten der ersten TTS-Wiedergabe
und ein zweiter Anruf-captureInterruption wuerde mit veraltetem Stand laufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:20:43 +02:00
duffyduck 77ccee8331 release: bump version to 0.0.9.5 2026-05-10 15:13:38 +02:00
duffyduck 175dcdf225 fix(audio): Auto-Resume nach Anruf — pcmBuffer bleibt erhalten
Logs zeigten "WAV nicht binnen 30000ms verfuegbar" — pcmBuffer wurde von
haltAllPlayback geleert, isFinal schrieb daher eine leere WAV (oder gar
keine, weil pcmMessageId leer war).

Neue Methode pauseForCall (statt haltAllPlayback im Anruf-Pfad):
- AudioTrack stoppt + AudioFocus release (Spotify resumed)
- pcmBuffer + pcmMessageId BLEIBEN — Bridge-Chunks werden weiter gesammelt
- _pausedForCall macht weitere Chunks "silent" (kein writeChunk, nur Cache)
- isFinal schreibt WAV trotz Anruf → resumeFromInterruption findet sie

Plus captureInterruption idempotent gemacht: ringing→offhook ueberschreibt
die Position vom ersten Halt nicht mehr (Date.now-Tracking laeuft stumpf
weiter obwohl Audio gestoppt ist — der erste Halt ist die echte Position).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:11:46 +02:00
duffyduck 1549e9cd4f docs(issue): vier neue Fixes der Debug-Session festgehalten
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:01:37 +02:00
duffyduck 910e74b497 fix(bridge): GPS-Position auch im STT-chat-Payload an Diagnostic mitgeben
Die App sendet location einmal im audio-Payload. Die Bridge kannte sie
zwar (ging in aria-core's Kontext rein), reichte sie aber nicht im STT-
broadcast an die Diagnostic durch. Diagnostic zeigte darum bei Sprach-
eingaben nie den GPS-Block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:59:09 +02:00
7 changed files with 118 additions and 15 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 904
versionName "0.0.9.4"
versionCode 908
versionName "0.0.9.8"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.0.9.4",
"version": "0.0.9.8",
"private": true,
"scripts": {
"android": "react-native run-android",
+3
View File
@@ -890,6 +890,7 @@ const ChatScreen: React.FC = () => {
// Alle Pending Anhaenge + Text senden
const sendPendingAttachments = useCallback(async (messageText: string) => {
if (pendingAttachments.length === 0) return;
console.log('[Chat] sendPendingAttachments: %d Anhang/Anhaenge', pendingAttachments.length);
const location = await getCurrentLocation();
const msgId = nextId();
@@ -939,6 +940,8 @@ const ChatScreen: React.FC = () => {
}
// An RVS senden
console.log('[Chat] sende file: name=%s mime=%s size=%s b64Bytes=%s',
name, mimeType, file.size, base64.length);
rvs.send('file', {
name,
type: mimeType,
+91 -9
View File
@@ -347,19 +347,65 @@ class AudioService {
this._releaseFocusDeferred();
}
/** TTS-Wiedergabe haart stoppen — z.B. wenn ein Anruf reinkommt.
* Released auch sofort den AudioFocus damit der Anruf-Klingelton hoerbar ist. */
/** TTS-Wiedergabe haart stoppen — z.B. fuer Barge-In. Buffer wird geleert,
* kein Auto-Resume. Released auch sofort den AudioFocus. */
haltAllPlayback(reason: string = ''): void {
console.log('[Audio] haltAllPlayback: %s', reason || '(no reason)');
this._conversationFocusActive = false;
this.stopPlayback();
}
/** Speziell fuer Anrufe: AudioTrack stoppen + Focus releasen, ABER pcm-
* Buffer + messageId behalten damit weitere Chunks der unterbrochenen
* Antwort weiter gesammelt werden. isFinal schreibt dann die WAV trotz
* Anruf — und resumeFromInterruption findet sie. */
pauseForCall(reason: string = ''): void {
console.log('[Audio] pauseForCall: %s', reason || '(no reason)');
this._conversationFocusActive = false;
this._pausedForCall = true;
// Foreground-Service stoppen — Notification waere sonst irrefuehrend
stopBackgroundAudio().catch(() => {});
// SoundPool/RNSound (Resume-Sound, Play-Button) stoppen — nicht relevant fuer Auto-Resume
if (this.currentSound) {
try { this.currentSound.stop(); this.currentSound.release(); } catch {}
this.currentSound = null;
}
if (this.resumeSound) {
try { this.resumeSound.stop(); this.resumeSound.release(); } catch {}
this.resumeSound = null;
}
// AudioTrack hart stoppen damit nichts mehr aus dem Lautsprecher kommt.
// pcmStreamActive bleibt true, pcmBuffer/pcmMessageId BLEIBEN — damit
// weitere Chunks gesammelt werden und isFinal die WAV schreiben kann.
PcmStreamPlayer?.stop().catch(() => {});
this._cancelDeferredFocusRelease();
AudioFocus?.release().catch(() => {});
}
/** Anruf vorbei → weitere Chunks duerfen wieder abgespielt werden.
* resumeFromInterruption uebernimmt die Wiedergabe ab gemerkter Position. */
endCallPause(): void {
if (!this._pausedForCall) return;
this._pausedForCall = false;
console.log('[Audio] endCallPause');
}
/** Bei Anruf: aktuelle Wiedergabe-Position merken damit wir nach dem
* Auflegen von dort weitermachen koennen. Returnt Position in Sekunden
* oder 0 wenn nichts spielte. */
* oder 0 wenn nichts spielte.
*
* Idempotent: bei mehrfachem Aufruf (ringing → offhook) wird die Position
* vom ersten Mal NICHT ueberschrieben. playbackStartTime laeuft stumpf
* weiter obwohl das Audio gestoppt ist — der erste Halt ist der echte. */
captureInterruption(): number {
if (this.pausedMessageId) {
console.log('[Audio] captureInterruption: bereits erfasst (msgId=%s pos=%ss) — skip',
this.pausedMessageId, this.pausedPosition.toFixed(2));
return this.pausedPosition;
}
if (!this.playbackStartTime || !this.currentPlaybackMsgId) {
console.log('[Audio] captureInterruption: nichts spielte (startTime=%s, msgId=%s)',
this.playbackStartTime, this.currentPlaybackMsgId || '(leer)');
this.pausedPosition = 0;
this.pausedMessageId = '';
return 0;
@@ -379,7 +425,12 @@ class AudioService {
async resumeFromInterruption(maxWaitMs: number = 30000): Promise<boolean> {
const msgId = this.pausedMessageId;
const position = this.pausedPosition;
if (!msgId) return false;
if (!msgId) {
console.log('[Audio] resumeFromInterruption: kein gemerkter Stand — skip');
return false;
}
console.log('[Audio] resumeFromInterruption: starte fuer msgId=%s pos=%ss',
msgId, position.toFixed(2));
this.pausedMessageId = ''; // konsumieren
const cachePath = `${RNFS.DocumentDirectoryPath}/tts_cache/${msgId}.wav`;
const startTime = Date.now();
@@ -413,6 +464,14 @@ class AudioService {
this._firePlaybackStarted();
this.isPlaying = true;
this.resumeSound = sound;
// Tracking auch fuer den Resume-Sound aktualisieren — sonst kann
// captureInterruption bei einem zweiten Anruf die Position nicht
// mehr ermitteln (playbackStartTime waere von der ersten Wiedergabe).
const msgIdMatch = path.match(/([^/\\]+)\.wav$/i);
if (msgIdMatch) this.currentPlaybackMsgId = msgIdMatch[1];
// Virtuelle Start-Zeit so setzen, dass captureInterruption (das den
// Leading-Silence-Offset wieder abzieht) die korrekte Position liefert.
this.playbackStartTime = Date.now() - (positionSec + this.LEADING_SILENCE_SEC) * 1000;
console.log('[Audio] Resume von Position %ss aus %s',
positionSec.toFixed(2), path);
sound.setCurrentTime(Math.max(0, positionSec));
@@ -788,7 +847,9 @@ class AudioService {
}): Promise<string> {
// Globaler Mute-Flag uebersteuert das per-Call silent — verhindert
// Race-Conditions wenn der User zwischen Chunks den Mute-Knopf drueckt.
const silent = !!payload.silent || this._muted;
// _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;
if (!silent && !PcmStreamPlayer) {
console.warn('[Audio] PcmStreamPlayer Native Module nicht verfuegbar');
return '';
@@ -946,7 +1007,10 @@ class AudioService {
}
}
/** Audio aus lokaler Datei (file:// Pfad) in die Queue und abspielen. */
/** Audio aus lokaler Datei (file:// Pfad) in die Queue und abspielen.
* Setzt zusaetzlich playbackStartTime + currentPlaybackMsgId damit ein
* Anruf waehrend dieses Playbacks korrekt erfasst wird (ohne dieses
* Tracking liefert captureInterruption nichts → kein Auto-Resume). */
async playFromPath(filePath: string): Promise<void> {
if (!filePath) return;
try {
@@ -955,6 +1019,14 @@ class AudioService {
console.warn('[Audio] Cache-Datei existiert nicht mehr:', cleanPath);
return;
}
// Dateiname ohne .wav als messageId nehmen (egal ob UUID oder andere ID)
const fileMatch = cleanPath.match(/([^/\\]+)\.wav$/i);
const msgId = fileMatch ? fileMatch[1] : '';
console.log('[Audio] playFromPath: cleanPath=%s → msgId=%s', cleanPath, msgId || '(leer)');
if (msgId) {
this.currentPlaybackMsgId = msgId;
this.playbackStartTime = Date.now() - this.LEADING_SILENCE_SEC * 1000;
}
const b64 = await RNFS.readFile(cleanPath, 'base64');
this.playAudio(b64);
} catch (err) {
@@ -983,9 +1055,15 @@ class AudioService {
}
private _firePlaybackStarted(): void {
// Tracking fuer Auto-Resume nach Anruf-Pause
this.playbackStartTime = Date.now();
this.currentPlaybackMsgId = this.pcmMessageId || '';
// Tracking fuer Auto-Resume nach Anruf-Pause: NUR setzen wenn ein
// PCM-Stream laeuft (Live-TTS). Bei Play-Button / Resume-Sound hat der
// Caller (playFromPath / _playFromPathAtPosition) das Tracking schon
// korrekt mit der msgId aus dem Pfad gesetzt — sonst wuerden wir hier
// mit leerem pcmMessageId ueberschreiben.
if (this.pcmMessageId) {
this.playbackStartTime = Date.now();
this.currentPlaybackMsgId = this.pcmMessageId;
}
this.playbackStartedListeners.forEach(cb => {
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
});
@@ -1074,6 +1152,10 @@ class AudioService {
* — die Bridge kann einen Chunk im selben JS-Tick liefern in dem der
* User Mute geklickt hat. */
private _muted: boolean = false;
/** Anruf laeuft → Chunks werden nur in den Cache-Buffer gepusht, nicht
* abgespielt. Wird in pauseForCall gesetzt, in endCallPause/resumeFrom-
* Interruption zurueckgenommen. */
private _pausedForCall: boolean = false;
setMuted(muted: boolean): void {
this._muted = muted;
if (muted) this.stopPlayback();
+6 -1
View File
@@ -189,12 +189,17 @@ class PhoneCallService {
private _haltForCall(toast: string): void {
// Position merken bevor wir den Stream killen — fuer Auto-Resume.
audioService.captureInterruption();
audioService.haltAllPlayback(toast);
// pauseForCall (statt haltAllPlayback): pcmBuffer + messageId bleiben,
// weitere Chunks werden weiter gesammelt damit isFinal die WAV schreibt.
audioService.pauseForCall(toast);
wakeWordService.pauseForCall().catch(() => {});
ToastAndroid.show(toast, ToastAndroid.SHORT);
}
private _resumeAfterCall(toast: string): void {
// Anruf-Pause aufheben — neue Chunks duerfen wieder direkt abgespielt
// werden (falls die Bridge mid-Anruf isFinal noch nicht geschickt hat).
audioService.endCallPause();
wakeWordService.resumeFromCall().catch(() => {});
ToastAndroid.show(toast, ToastAndroid.SHORT);
// Auto-Resume: ab gemerkter Position weiterspielen wenn ARIA vor dem
+11 -2
View File
@@ -677,7 +677,10 @@ class ARIABridge:
while self.running:
try:
logger.info("[core] Verbinde: %s", self.ws_url)
async with websockets.connect(self.ws_url) as ws:
# max_size=50MB damit grosse Bilder/Voice-Uploads durchgehen.
# Python-websockets Default ist nur 1 MiB → 5MB JPEG sprengt
# das Limit, Connection wird silent gedroppt.
async with websockets.connect(self.ws_url, max_size=50 * 1024 * 1024) as ws:
# OpenClaw Handshake durchfuehren
if not await self._openclaw_handshake(ws):
logger.error("[core] Handshake fehlgeschlagen — Reconnect")
@@ -1141,7 +1144,8 @@ class ARIABridge:
try:
url = f"{current_url}?token={self.rvs_token}"
logger.info("[rvs] Verbinde: %s", current_url)
async with websockets.connect(url) as ws:
# max_size=50MB (siehe core-Connect oben — gleicher Grund).
async with websockets.connect(url, max_size=50 * 1024 * 1024) as ws:
self.ws_rvs = ws
retry_delay = 2
logger.info("[rvs] Verbunden — warte auf App-Nachrichten")
@@ -1634,6 +1638,11 @@ class ARIABridge:
}
if audio_request_id:
stt_payload["audioRequestId"] = audio_request_id
# GPS aus dem Original-Audio-Payload mitgeben — Diagnostic
# zeigt sie sonst nicht an (App sendet location nur einmal,
# die im audio-Payload). Reine Anzeige-Information.
if location:
stt_payload["location"] = location
ok = await self._send_to_rvs({
"type": "chat",
"payload": stt_payload,
+4
View File
@@ -85,6 +85,10 @@ Wichtige Mechanismen:
- [x] **App-Resume-Cooldown**: Wechsel von Background → Foreground triggert keinen falschen Wake-Word-Trigger mehr. AppState-Listener setzt 1.5s Cooldown in dem onWakeDetected-Events ignoriert werden (Audio-Pegel-Spike beim AudioFocus-Switch sonst als Wake-Word interpretiert)
- [x] Background-Mikro robust: acquireBackgroundAudio('rec'/'wake') wird jetzt VOR AudioRecord.startRecording gerufen — Foreground-Service mit foregroundServiceType=microphone muss aktiv sein bevor das Mikro greift, sonst blockiert Android ab 11+ den Background-Zugriff
- [x] **Stille-Pegel manuell setzbar** (Settings → Spracheingabe): Override-Wert in dB von -55 bis -15, default "automatisch". Info-Button mit Modal erklaert die Skala (niedriger = sensibler, hoeher = robuster gegen Hintergrundlaerm). Bei manuell gesetztem Wert wird die adaptive Baseline ignoriert
- [x] **Kurze TTS-Texte (1-3 Worte) spielen jetzt ab** — auf OnePlus A12 stallte AudioTrack mit `pos=0` weil der Default-Start-Threshold `bufferSize/2` (= 2s) bei kurzen Streams nie ueberschritten wurde. Fix: `setStartThresholdInFrames(100ms)` direkt nach dem Track-Build (API 31+). Buffer auf 4s entkoppelt von Pre-Roll, `play()` wird beim allerersten data-chunk gerufen
- [x] **Mute-Button stoppt jetzt auch laufenden PCM-Stream**`pcmStreamActive` wurde beim isFinal-Chunk schon false gesetzt, der AudioTrack spielte aber noch sekundenlang aus seinem Buffer. `stopPlayback()` uebersprang darum `PcmStreamPlayer.stop()`. Fix: stop() immer rufen (ist idempotent), kein Flag-Check mehr
- [x] **GPS-Permission im Manifest + Runtime-Request** beim Settings-Toggle — vorher fehlten ACCESS_COARSE_LOCATION / ACCESS_FINE_LOCATION komplett. `Geolocation.getCurrentPosition` schlug lautlos fehl, App sendete nie ein location-Feld
- [x] **GPS-Position auch im STT-Payload an Diagnostic** — die App sendet location einmal im audio-Payload. Die Bridge nutzte sie zwar (ging in aria-core's Kontext rein), reichte sie aber nicht im STT-broadcast an Diagnostic durch. Diagnostic zeigte darum bei Spracheingaben nie den GPS-Block, obwohl der "GPS einblenden"-Toggle aktiv war
### App Features