Compare commits

..
4 Commits
Author SHA1 Message Date
duffyduck 761217cb5a release: bump version to 0.2.1.6 2026-07-12 10:09:49 +02:00
duffyduckandClaude Opus 4.8 ff6d31acbd docs(readme): Pro-Projekt-Queue, Rueckfrage-Loop, Drafts + TTS-Abspiel-Queue
App-Features + Completed-Log aktualisiert; veraltetes Barge-In-Cancel-Verhalten
durch das neue Anstellen-statt-Abbrechen ersetzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:07:18 +02:00
duffyduckandClaude Opus 4.8 2d7a5784c2 docs(changelog): 0.2.1.5 — Pro-Projekt-Queue mit Rueckfrage-Loop
Queue pro Projekt (anstellen statt abbrechen) + [[AWAIT]]-Rueckfrage-Loop,
Pro-Projekt-Textfeld-Entwuerfe, TTS-Abspiel-Queue (back-to-back sprechen
nacheinander), Voice bricht nicht mehr ab. App + Diagnostic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:05:08 +02:00
duffyduckandClaude Opus 4.8 fa871219ae feat(queue): TTS-Abspiel-Queue — back-to-back-Antworten sprechen nacheinander
Bisher war die serialisierte Sprachausgabe zweier fast gleichzeitig fertiger
Antworten Timing-Glueck: PcmStreamPlayer.start() ruft stopInternal() (flush+
release), eine neue Antwort haette die laufende also abgeschnitten, sobald ihr
Audio waehrend der Wiedergabe der ersten ankam.

Jetzt echte Abspiel-Queue im audioService: kommt eine neue HOERBARE Antwort
waehrend eine andere noch hoerbar spielt (pcmAudiblePlaying bis
PcmPlaybackFinished, nicht nur bis Stream-Ende), werden ihre PCM-Chunks
gepuffert und erst nach dem Drain der laufenden nachgespielt. Bei wartender
Antwort meldet PcmPlaybackFinished NICHT 'fertig' (kein Wake-Word-Re-Arm).
Harter Stop/Barge-In/Mute verwirft die Queue. Race gegen gleichzeitige Chunks
einer dritten Antwort geschlossen (Flags vor await gesetzt). onPcmCached meldet
den WAV-Pfad nachgespielter Antworten fuer Mund-Button-Replay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:03:31 +02:00
6 changed files with 166 additions and 5 deletions
+21
View File
@@ -10,6 +10,27 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
---
## [0.2.1.5] — 2026-07-12 — Pro-Projekt-Queue mit Rückfrage-Loop
### Hinzugefügt
**Nachrichten-Queue pro Projekt (App + Diagnostic)**
- Eine zweite Nachricht, während ARIA am aktuellen Task arbeitet, wird jetzt **angestellt** statt den laufenden Task abzubrechen (vorher: Barge-In-Cancel). Sie läuft der Reihe nach, **pro Projekt unabhängig** (paralleles Arbeiten in mehreren Projekten bleibt). Wartende Nachrichten zeigen als ⏸-Bubble — tippen entfernt sie aus der Warteschlange.
- **Rückfrage-Loop:** Stellt ARIA eine echte, blockierende Rückfrage, **pausiert** die Queue und deine nächste Eingabe beantwortet sie — bis eine finale Antwort kommt, dann läuft der nächste Queue-Eintrag (gleiches Muster). Banner „❓ ARIA fragt nach — deine Eingabe beantwortet das". Der Stop-Button bricht den aktuellen Task ab und schaltet zum nächsten.
- ARIA signalisiert eine Rückfrage über einen **unsichtbaren `[[AWAIT]]`-Marker** — wie speak/converse deklariert das Modell den Zustand selbst (kein „endet-mit-?"-Raten). Brain strippt ihn, gibt `awaiting_reply` durch `chat()``ChatOut` → Bridge-Chat-Payload. Local (tool-los) und Fast-Path markieren nie.
**Pro-Projekt-Textfeld-Entwürfe (App + Diagnostic)**
- Der Feldinhalt bleibt beim Projektwechsel erhalten: in Projekt X tippen, zu Y wechseln (leeres Feld), zurück zu X → dein Entwurf steht wieder da. In Storage persistiert.
**TTS-Abspiel-Queue (App)**
- Zwei fast gleichzeitig fertige Antworten sprechen jetzt garantiert **nacheinander** statt sich gegenseitig abzuschneiden. Vorher war das Timing-Glück (`PcmStreamPlayer.start()` ruft `stopInternal()` = flush/release, hätte die laufende gecuttet). Jetzt: „spielt hörbar" gilt bis zum echten `PcmPlaybackFinished` (nicht nur bis Stream-Ende); eine neue hörbare Antwort, die währenddessen ankommt, wird gepuffert und danach nachgespielt (Kette für 3, 4, …). Harter Stop/Barge-In/Mund-Button verwirft die Queue.
### Geändert
- **Voice bricht nicht mehr ab:** eine neue Sprachnachricht während ARIA arbeitet stoppt nur akustisch das TTS (sauberes Mikro) und wird über den Brain-Projekt-Lock serialisiert, statt den laufenden Task abzubrechen (passend zu „immer anstellen + Stop-Button"). Text-Senden erkennt Brain-busy als Fallback, damit auch nach einem voice-gestarteten Turn korrekt angestellt wird. Grenze: eine per Sprache gestartete Aufgabe erscheint nicht als löschbare ⏸-Bubble (Aufnahme wird live gestreamt, nicht app-seitig gepuffert).
---
## [0.2.1.4] — 2026-07-12 — Lokales LLM: der ehrliche Rückbau
### Geändert
+7 -1
View File
@@ -474,7 +474,9 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
- **Gespraechsmodus** (Ohr-Button): Nach jeder ARIA-Antwort startet automatisch die Aufnahme — wie ein natuerliches Gespraech hin und her
- **Wake-Word** (on-device, openWakeWord ONNX): "Hey Jarvis", "Alexa", "Hey Mycroft", "Hey Rhasspy" — Mikrofon hoert passiv mit, Konversation startet beim Schluesselwort. Komplett on-device via ONNX Runtime, kein API-Key, kein Cloud-Roundtrip, Audio verlaesst das Geraet nicht.
- **VAD (Voice Activity Detection)**: Adaptive Schwelle (Baseline aus ersten 500ms Mic-Pegel + 6dB Offset). Konfigurierbare Stille-Toleranz (1.08.0s, Default 2.8s) bevor Auto-Stop greift. Max-Aufnahme einstellbar (130 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"
- **Nachricht anstellen statt abbrechen** (Queue pro Projekt): Schickst du eine zweite Nachricht waehrend ARIA noch am aktuellen Task arbeitet, wird sie **angestellt** statt den laufenden abzubrechen — laeuft der Reihe nach, pro Projekt unabhaengig. Wartende zeigen als `⏸`-Bubble (tippen entfernt sie aus der Warteschlange). Explizites Abbrechen laeuft ueber den Stop-Button am „ARIA denkt". Eine neue Sprachnachricht stoppt nur akustisch das TTS (sauberes Mikro), bricht die laufende Arbeit aber nicht mehr ab
- **Rueckfrage-Loop**: Stellt ARIA eine echte, blockierende Rueckfrage, **pausiert** die Queue und deine naechste Eingabe beantwortet sie — bis eine finale Antwort kommt, dann laeuft der naechste Queue-Eintrag (gleiches Muster). Banner „❓ ARIA fragt nach — deine Eingabe beantwortet das". ARIA signalisiert das ueber einen unsichtbaren `[[AWAIT]]`-Marker im Antworttext (das Modell deklariert den Zustand selbst, kein „endet-mit-?"-Raten; Brain strippt ihn und gibt `awaiting_reply` an App + Diagnostic durch)
- **Pro-Projekt-Textfeld-Entwuerfe**: Der Feldinhalt bleibt beim Projektwechsel erhalten — in Projekt X tippen, zu Y wechseln (leeres Feld), zurueck zu X → dein Entwurf steht wieder da. Persistiert ueber Neustart. Gleiches Verhalten im Diagnostic
- **Wake-Word waehrend TTS**: Du kannst "Computer" sagen waehrend ARIA noch redet — AcousticEchoCanceler verhindert dass ARIAs eigene Stimme das Wake-Word triggert
- **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
@@ -482,6 +484,7 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
- **"ARIA denkt..." Indicator**: Zeigt live den Status vom Core (Denken, Tool, Schreiben) + Abbrechen-Button
- **TTS-Wiedergabe**: F5-TTS PCM-Streaming direkt in AudioTrack mit konfigurierbarem Pre-Roll-Buffer (1.06.0s, Default 3.5s) gegen Gaps bei Render-Pausen
- **Audio-Pause**: Andere Apps (Spotify, YouTube etc.) pausieren komplett waehrend ARIA spricht und kommen erst wieder nach echtem Wiedergabe-Ende
- **TTS-Abspiel-Queue**: Zwei fast gleichzeitig fertige Antworten sprechen garantiert **nacheinander** statt sich abzuschneiden — eine neue hoerbare Antwort, die waehrend der Wiedergabe einer anderen ankommt, wird gepuffert und erst nach deren echtem Wiedergabe-Ende (`PcmPlaybackFinished`, nicht nur Stream-Ende) nachgespielt. Harter Stop / Barge-In / Mund-Button verwirft die Queue
- **Lokale Voice-Wahl**: Pro Geraet eigene Stimme moeglich (in Settings). Diagnostic-Wechsel ueberschreibt alle App-Wahlen.
- **Voice-Ready Toast**: Beim Wechsel zeigt die App "Stimme X bereit (X.Ys)" sobald der Preload durch ist
- **Play-Button**: Jede ARIA-Nachricht kann nochmal vorgelesen werden (aus Cache wenn vorhanden, sonst neu rendern)
@@ -994,6 +997,9 @@ docker exec aria-brain curl localhost:8080/memory/stats
- [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] PcmPlaybackFinished-Event: AudioFocus wird erst released wenn AudioTrack wirklich durch ist — kein Spotify-mid-TTS mehr
- [x] Edge-Case: neue Frage waehrend Telefonat verwirft pending Auto-Resume, neueste Antwort gewinnt
- [x] **Pro-Projekt-Nachrichten-Queue** (loest das alte Barge-In-Cancel ab): zweite Nachricht wird **angestellt** statt den laufenden Task abzubrechen; **Rueckfrage-Loop** via unsichtbarem `[[AWAIT]]`-Marker (Queue pausiert, naechste Eingabe beantwortet die Rueckfrage); Stop-Button schaltet zum naechsten; sichtbare `⏸`-Bubbles (loeschbar). Pro Projekt unabhaengig, App + Diagnostic
- [x] Pro-Projekt-Textfeld-Entwuerfe (Feldinhalt bleibt beim Projektwechsel erhalten, persistiert; App + Diagnostic)
- [x] **TTS-Abspiel-Queue**: zwei fast gleichzeitig fertige Antworten sprechen garantiert nacheinander statt sich abzuschneiden (Puffern bis `PcmPlaybackFinished` der laufenden)
- [x] Settings-Sub-Screens: 8 Kategorien statt langer Liste
- [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
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20105
versionName "0.2.1.5"
versionCode 20106
versionName "0.2.1.6"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.2.1.5",
"version": "0.2.1.6",
"private": true,
"scripts": {
"android": "react-native run-android",
+8
View File
@@ -1731,6 +1731,13 @@ const ChatScreen: React.FC = () => {
// das Mikro greifen kann.
wakeWordService.stopBargeListening().catch(() => {});
});
// Aus der TTS-Queue nachgespielte (zweite) Antwort: ihren WAV-Cache-Pfad an
// die Bubble haengen, damit der Mund-Button/Play sie auch abspielen kann.
const unsubPcmCached = audioService.onPcmCached((messageId, audioPath) => {
if (!messageId || !audioPath) return;
setMessages(prev => prev.map(m =>
m.messageId === messageId ? { ...m, audioPath } : m));
});
return () => {
unsubWake();
@@ -1739,6 +1746,7 @@ const ChatScreen: React.FC = () => {
unsubPassive();
unsubTtsStart();
unsubTtsEnd();
unsubPcmCached();
};
}, [wakeWordActive]);
+127 -1
View File
@@ -284,6 +284,13 @@ class AudioService {
private pcmSampleRate: number = 24000;
private pcmChannels: number = 1;
private pcmBuffer: string[] = []; // base64-chunks zum spaeteren WAV-Build
// ── TTS-Abspiel-Queue: zwei back-to-back-Antworten sollen sich NICHT
// gegenseitig abschneiden. Eine neue hoerbare Antwort, die reinkommt waehrend
// eine andere noch HOERBAR spielt, wird gepuffert und nach PcmPlaybackFinished
// nachgespielt (statt via start()→stopInternal() die laufende zu cutten). ──
private pcmAudiblePlaying: boolean = false; // eine hoerbare Antwort spielt (bis PcmPlaybackFinished)
private pcmPlayingMsgId: string = ''; // deren messageId
private pcmPendingStreams: Array<{ messageId: string; sampleRate: number; channels: number; chunks: string[]; final: boolean }> = [];
private pcmBytesCollected: number = 0;
private readonly PCM_MAX_CACHE_BYTES = 30 * 1024 * 1024; // 30MB
@@ -374,6 +381,16 @@ class AudioService {
const emitter = new NativeEventEmitter(NativeModules.PcmStreamPlayer as any);
emitter.addListener('PcmPlaybackFinished', () => {
console.log('[Audio] PcmPlaybackFinished — AudioTrack drained');
this.pcmAudiblePlaying = false;
this.pcmPlayingMsgId = '';
// TTS-Abspiel-Queue: steht eine naechste Antwort bereit? Dann NICHT
// "fertig" melden (kein Wake-Word-Re-Arm / Conversation-Ende) — ARIA
// spricht gleich weiter. Die naechste gepufferte Antwort direkt spielen.
if (this.pcmPendingStreams.length > 0) {
this._promoteNextPendingStream().catch(err =>
console.warn('[Audio] promote next pending stream err:', err));
return;
}
this._releaseFocusDeferred();
// Erst HIER playbackFinished-Listener feuern — nicht schon beim
// Empfang des letzten PCM-Chunks (siehe handlePcmChunk). AudioTrack
@@ -1401,6 +1418,23 @@ class AudioService {
const base64 = payload.base64 || '';
const isFinal = !!payload.final;
// ── TTS-Abspiel-Queue ──
// Kommt eine NEUE hoerbare Antwort rein, waehrend eine andere noch hoerbar
// spielt? Dann NICHT starten (start()→stopInternal() wuerde die laufende
// abschneiden) — puffern und nach deren PcmPlaybackFinished nachspielen.
if (!silent && this.pcmAudiblePlaying && messageId && messageId !== this.pcmPlayingMsgId) {
let entry = this.pcmPendingStreams.find(e => e.messageId === messageId);
if (!entry) {
entry = { messageId, sampleRate, channels, chunks: [], final: false };
this.pcmPendingStreams.push(entry);
console.log('[Audio] TTS-Queue: Antwort %s wird gepuffert (spielt gerade %s)',
messageId, this.pcmPlayingMsgId);
}
if (base64) entry.chunks.push(base64);
if (isFinal) entry.final = true;
return ''; // Live-Player nicht anfassen
}
// Neuer Stream? (messageId Wechsel oder nicht aktiv)
if (!this.pcmStreamActive || this.pcmMessageId !== messageId) {
if (this.pcmStreamActive && !silent) {
@@ -1448,6 +1482,8 @@ class AudioService {
this._cancelDeferredFocusRelease();
AudioFocus?.requestDuck().catch(() => {});
this._firePlaybackStarted();
this.pcmAudiblePlaying = true;
this.pcmPlayingMsgId = messageId;
}
}
@@ -1490,6 +1526,73 @@ class AudioService {
return '';
}
/** Naechste gepufferte TTS-Antwort abspielen (TTS-Abspiel-Queue). Wird nach
* PcmPlaybackFinished der vorherigen aufgerufen — so sprechen zwei
* back-to-back-Antworten NACHEINANDER statt sich abzuschneiden. */
private async _promoteNextPendingStream(): Promise<void> {
const entry = this.pcmPendingStreams.shift();
if (!entry) return;
// Inzwischen global gemutet / im Anruf / vom User gestoppt? Dann NICHT
// hoerbar abspielen — nur cachen und die naechste promoten.
const mutedNow = this._muted || this._pausedForCall ||
(!!this._stoppedMessageId && this._stoppedMessageId === entry.messageId);
console.log('[Audio] TTS-Queue: spiele gepufferte Antwort %s (%d chunks, final=%s, muted=%s)',
entry.messageId, entry.chunks.length, entry.final, mutedNow);
// SOFORT als "spielt" markieren (vor jedem await) — sonst koennte ein
// gleichzeitig eintreffender Chunk einer DRITTEN Antwort in der await-Luecke
// einen konkurrierenden Stream starten statt zu puffern.
this.pcmPlayingMsgId = entry.messageId;
this.pcmAudiblePlaying = !mutedNow;
// Cache-State fuer den WAV-Build (Mund-Button-Replay) setzen.
this.pcmMessageId = entry.messageId;
this.pcmSampleRate = entry.sampleRate;
this.pcmChannels = entry.channels;
this.pcmBuffer = entry.chunks.slice();
this.pcmBytesCollected = entry.chunks.reduce((n, c) => n + Math.floor(c.length * 0.75), 0);
this.pcmStreamActive = true;
if (!mutedNow && PcmStreamPlayer) {
try {
const prerollSec = await loadPrerollSec();
await PcmStreamPlayer.start(entry.sampleRate, entry.channels, prerollSec);
this._cancelDeferredFocusRelease();
AudioFocus?.requestDuck().catch(() => {});
this._firePlaybackStarted();
this.pcmAudiblePlaying = true;
this.pcmPlayingMsgId = entry.messageId;
for (const c of entry.chunks) {
try { await PcmStreamPlayer.writeChunk(c); } catch (err) { console.warn('[Audio] promote writeChunk', err); }
}
if (entry.final) { try { await PcmStreamPlayer.end(); } catch {} }
} catch (err) {
console.error('[Audio] TTS-Queue promote start fehlgeschlagen:', err);
this.pcmAudiblePlaying = false;
this.pcmPlayingMsgId = '';
}
}
// War die Antwort schon komplett (final) da: WAV cachen + State wie im
// Normalpfad zuruecksetzen. Bei NICHT-final laeuft der Rest live ueber
// _handlePcmChunkImpl (messageId == pcmPlayingMsgId → Normalpfad).
if (entry.final) {
this.pcmStreamActive = false;
if (this.pcmBuffer.length > 0) {
const audioPath = await this._savePcmBufferAsWav(entry.messageId).catch(() => '');
if (audioPath) {
this.pcmCachedListeners.forEach(cb => {
try { cb(entry.messageId, audioPath); } catch (e) { console.warn('[Audio] pcmCached cb err:', e); }
});
}
}
this.pcmBuffer = [];
this.pcmBytesCollected = 0;
this.pcmMessageId = '';
// Nicht hoerbar abgespielt (gemutet)? Dann feuert PcmPlaybackFinished nicht
// → die naechste gepufferte Antwort selbst nachziehen (Kette).
if (!this.pcmAudiblePlaying) {
await this._promoteNextPendingStream();
}
}
}
/** Gesammelte PCM-Chunks als WAV speichern. Gibt file:// Pfad zurueck. */
private async _savePcmBufferAsWav(messageId: string): Promise<string> {
try {
@@ -1578,6 +1681,19 @@ class AudioService {
// Callback wenn alle Audio-Teile abgespielt sind
private playbackFinishedListeners: (() => void)[] = [];
private playbackStartedListeners: (() => void)[] = [];
// Feuert wenn eine aus der TTS-Queue NACHgespielte Antwort ihren WAV-Cache
// geschrieben hat — der Normalpfad meldet den Pfad ueber den handlePcmChunk-
// Rueckgabewert, gepufferte (zweite) Antworten koennen das aber nicht (ihre
// Chunks returnen '' waehrend sie warten). Damit setzt die App auch fuer die
// nachgespielte Antwort m.audioPath (Mund-Button-Replay).
private pcmCachedListeners: Array<(messageId: string, audioPath: string) => void> = [];
onPcmCached(callback: (messageId: string, audioPath: string) => void): () => void {
this.pcmCachedListeners.push(callback);
return () => {
this.pcmCachedListeners = this.pcmCachedListeners.filter(cb => cb !== callback);
};
}
onPlaybackFinished(callback: () => void): () => void {
this.playbackFinishedListeners.push(callback);
@@ -1759,6 +1875,10 @@ class AudioService {
}
// AudioTrack IMMER hart stoppen (idempotent) — auch im Drain-Fall.
PcmStreamPlayer?.stop().catch(() => {});
// Wartende TTS-Antworten verwerfen (Mund-Button = still sein).
this.pcmPendingStreams = [];
this.pcmAudiblePlaying = false;
this.pcmPlayingMsgId = '';
stopBackgroundAudio().catch(() => {});
this._cancelDeferredFocusRelease();
AudioFocus?.release().catch(() => {});
@@ -1770,7 +1890,8 @@ class AudioService {
// 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);
|| this.pcmStreamActive || this.audioQueue.length || this.isPlaying
|| this.pcmPendingStreams.length);
if (!hasAnything) return;
console.log('[Audio] stopPlayback: currentSound=%s queue=%d pcm=%s',
this.currentSound ? 'aktiv' : 'null', this.audioQueue.length, this.pcmStreamActive);
@@ -1804,6 +1925,11 @@ class AudioService {
this.pcmBuffer = [];
this.pcmBytesCollected = 0;
this.pcmMessageId = '';
// TTS-Abspiel-Queue verwerfen — harter Stop/Abbruch/Barge-In soll auch
// wartende Antworten fallenlassen (sonst sprechen sie nach dem Stop weiter).
this.pcmPendingStreams = [];
this.pcmAudiblePlaying = false;
this.pcmPlayingMsgId = '';
// Audio-Focus sofort freigeben — User hat explizit abgebrochen.
// Unser Focus war TRANSIENT, Spotify resumed darum automatisch beim
// Abandon. Den frueheren kickReleaseMedia haben wir entfernt: er