diff --git a/README.md b/README.md index 365164d..7d6c71c 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,7 @@ API-Endpoint fuer andere Services: `GET http://localhost:3001/api/session` - Text-Chat mit ARIA - **Sprachaufnahme**: Push-to-Talk (halten) oder Tap-to-Talk (tippen, Auto-Stop bei Stille) - **Gespraechsmodus** (Ohr-Button): Nach jeder ARIA-Antwort startet automatisch die Aufnahme — wie ein natuerliches Gespraech hin und her +- **Wake-Word** (optional, Picovoice Porcupine on-device): "Jarvis", "Computer" usw. — Mikrofon hoert passiv mit, Konversation startet beim Schluesselwort. Eigene Wake-Words ueber die Picovoice Console moeglich. Ohne API-Key faellt der Ohr-Button auf Direkt-Aufnahme zurueck. - **VAD (Voice Activity Detection)**: Konfigurierbare Stille-Toleranz (1.0–8.0s, Default 2.8s) bevor Auto-Stop greift. Max-Aufnahme 120s. - **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. @@ -398,6 +399,49 @@ API-Endpoint fuer andere Services: `GET http://localhost:3001/api/session` - GPS-Position (optional) - QR-Code Scanner fuer Token-Pairing +### Wake-Word einrichten (Picovoice Porcupine) + +Das Wake-Word laeuft komplett **on-device** in der App — kein Audio verlaesst dein Telefon +fuer die Erkennung. Picovoice bietet aktuell einen **7-Tage Free Trial** ohne Kreditkarte +und ohne Auto-Renewal an, danach kostenpflichtig (siehe [picovoice.ai/pricing](https://picovoice.ai/pricing)). +Wer das Wake-Word ueberspringen will: der Ohr-Button funktioniert auch ohne AccessKey +(Direkt-Aufnahme statt passivem Lauschen — siehe unten). + +**1) AccessKey holen** (einmalig, ~2 Minuten): + +1. Auf [console.picovoice.ai](https://console.picovoice.ai) registrieren (Email + Passwort, keine Kreditkarte fuer den Trial). +2. Nach dem Login auf dem Dashboard → **AccessKey** kopieren (langer Base64-String). + +**2) AccessKey in der App eintragen:** + +- App → **Einstellungen** → Abschnitt **Wake-Word** +- AccessKey einfuegen, **Keyword** auswaehlen (Default: `jarvis`) +- Speichern → die App initialisiert Porcupine automatisch + +**Eingebaute Keywords** (sofort verfuegbar, kein Training noetig): +`jarvis`, `computer`, `picovoice`, `porcupine`, `bumblebee`, `terminator`, +`alexa`, `hey google`, `ok google`, `hey siri` + +**3) Eigenes Wake-Word erstellen** ("ARIA", "Hey Stefan", was du willst): + +1. [console.picovoice.ai](https://console.picovoice.ai) → **Porcupine** → **Train Wake Word** +2. Wort eingeben (z.B. `ARIA`), Sprache `German` waehlen, Plattform `Android` +3. **Train** druecken — Picovoice trainiert das Modell in ~1–2 Minuten +4. Die fertige `.ppn`-Datei runterladen +5. *(Custom-Upload in der App ist Phase 2 — aktuell nur eingebaute Keywords. + `.ppn`-Dateien koennen schon manuell ins App-Bundle gelegt werden, die UI + dafuer kommt mit dem naechsten Diagnostic-Update.)* + +**Bedienung:** +- **Ohr-Button (👂)** in der Statusleiste tippen → Wake-Word ist scharf, App hoert passiv mit +- Wake-Word sagen → Symbol wechselt auf 🎙️, normale Konversation laeuft +- Nach jeder ARIA-Antwort oeffnet sich das Mikro nochmal — Stille → zurueck zu 👂 +- Erneut tippen → Ohr aus (🔇) + +**Ohne AccessKey:** Der Ohr-Button startet stattdessen die Direkt-Aufnahme (Mikro +ist sofort aktiv, kein passives Lauschen). Auch ein gueltiger Modus, nur halt ohne +"Hands-free" via Schluesselwort. + ### Ersteinrichtung (Dev-Maschine, einmalig) ```bash @@ -744,8 +788,9 @@ docker exec aria-core ssh aria-wohnung hostname - **Proxy Cold Start**: Jede Nachricht spawnt einen neuen `claude --print` Prozess. Dadurch ist ARIA langsamer als die direkte Claude CLI. Timeout ist auf 900s (15 Min). - **Kein Streaming zur App**: Die App zeigt erst die fertige Antwort, keine Streaming-Tokens. -- **Wake Word nur auf VM**: Die Bridge hoert auf "ARIA" ueber das lokale Mikrofon der VM. - In der App gibt es Energy-basierte Erkennung (Phase 1). On-device "ARIA"-Keyword (Porcupine) ist Phase 2. +- **Wake-Word in der App nur eingebaute Keywords**: `jarvis`, `computer` etc. funktionieren + sofort, eigene Wake-Words (`.ppn` aus der Picovoice Console) muessen aktuell noch manuell + ins App-Bundle. Die Upload-UI in Diagnostic ist Phase 2. - **Audio-Format**: App nimmt AAC/MP4 auf, Bridge konvertiert via FFmpeg zu 16kHz PCM. - **RVS Zombie-Connections**: WebSocket-Verbindungen sterben gelegentlich ohne Fehlermeldung. Bridge hat Ping-Check (5s), Diagnostic nutzt frische Verbindungen pro Request. @@ -800,6 +845,7 @@ docker exec aria-core ssh aria-wohnung hostname - [x] Audio-Pause statt Ducking (TRANSIENT statt MAY_DUCK) + release-Timing fix - [x] VAD-Stille-Toleranz und Max-Aufnahme einstellbar (1-8s, 120s) - [x] Disk-Voll Banner in Diagnostic mit copy-baren Cleanup-Befehlen +- [x] Porcupine Wake-Word on-device in der App (eingebaute Keywords + State-Icon) ### Phase 2 — ARIA wird produktiv @@ -815,5 +861,5 @@ docker exec aria-core ssh aria-wohnung hostname - [ ] STARFACE Telefonie-Skill - [ ] Desktop Client (Tauri) - [ ] bKVM Remote IT-Support -- [ ] Porcupine Wake Word (on-device "ARIA" in der App) +- [ ] Custom-`.ppn`-Upload fuer Wake-Word ueber Diagnostic (eigene Trigger-Worte) - [ ] Claude Vision direkt (Bildanalyse ohne Dateipfad-Umweg) diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index d0087f0..e10a570 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -191,6 +191,13 @@ class AudioService { private pcmBytesCollected: number = 0; private readonly PCM_MAX_CACHE_BYTES = 30 * 1024 * 1024; // 30MB + // AudioFocus wird verzoegert freigegeben — wenn ARIA eine zweite Antwort + // direkt hinterherschickt (oder ein neuer Stream startet), bleibt Spotify + // pausiert. Ohne diese Verzoegerung springt Spotify im Mikro-Sekunden-Gap + // zwischen zwei Streams kurz wieder an. + private focusReleaseTimer: ReturnType | null = null; + private readonly FOCUS_RELEASE_DELAY_MS = 800; + // VAD State private vadEnabled: boolean = false; private lastSpeechTime: number = 0; @@ -205,6 +212,24 @@ class AudioService { this.recorder.setSubscriptionDuration(0.1); // 100ms Metering-Updates } + /** AudioFocus mit kleiner Verzoegerung freigeben — Spotify/YouTube + * springen sonst im Gap zwischen zwei TTS-Streams (oder wenn ARIA + * eine zweite Antwort direkt hinterherschickt) kurz wieder an. */ + private _releaseFocusDeferred(): void { + this._cancelDeferredFocusRelease(); + this.focusReleaseTimer = setTimeout(() => { + this.focusReleaseTimer = null; + AudioFocus?.release().catch(() => {}); + }, this.FOCUS_RELEASE_DELAY_MS); + } + + private _cancelDeferredFocusRelease(): void { + if (this.focusReleaseTimer) { + clearTimeout(this.focusReleaseTimer); + this.focusReleaseTimer = null; + } + } + // --- Berechtigungen --- async requestMicrophonePermission(): Promise { @@ -305,6 +330,7 @@ class AudioService { this.setState('recording'); // Andere Apps waehrend der Aufnahme pausieren (Musik, Videos etc.) + this._cancelDeferredFocusRelease(); AudioFocus?.requestExclusive().catch(() => {}); // VAD aktivieren — Stille-Dauer aus AsyncStorage (Settings-konfigurierbar). @@ -387,8 +413,9 @@ class AudioService { await this.recorder.stopRecorder(); this.recorder.removeRecordBackListener(); - // Audio-Focus freigeben — andere Apps duerfen wieder - AudioFocus?.release().catch(() => {}); + // Audio-Focus verzoegert freigeben — gleich kommt die TTS-Antwort, + // im Gap soll Spotify nicht hochkommen. + this._releaseFocusDeferred(); const durationMs = Date.now() - this.recordingStartTime; const hadSpeech = this.speechDetected; @@ -535,6 +562,7 @@ class AudioService { this.pcmStreamActive = false; return ''; } + this._cancelDeferredFocusRelease(); AudioFocus?.requestDuck().catch(() => {}); } } @@ -553,11 +581,12 @@ class AudioService { if (isFinal) { if (!silent) { // end() resolved jetzt erst wenn der native Writer-Thread fertig - // ist (alle Samples ausgespielt) — danach erst AudioFocus freigeben, - // damit Spotify/YouTube nicht waehrend des Pre-Roll-Ausklangs - // wieder aufdrehen. + // ist (alle Samples ausgespielt) — danach AudioFocus verzoegert + // freigeben, damit Spotify/YouTube nicht im Mikro-Gap zwischen zwei + // ARIA-Antworten wieder hochdrehen. Wenn ein neuer Stream innerhalb + // FOCUS_RELEASE_DELAY_MS startet, wird das Release abgebrochen. try { await PcmStreamPlayer!.end(); } catch {} - AudioFocus?.release().catch(() => {}); + this._releaseFocusDeferred(); } this.pcmStreamActive = false; @@ -661,8 +690,9 @@ class AudioService { private async _playNext(): Promise { if (this.audioQueue.length === 0) { this.isPlaying = false; - // Audio-Focus abgeben → andere Apps volle Lautstaerke - AudioFocus?.release().catch(() => {}); + // Audio-Focus verzoegert abgeben → wenn gleich noch eine Antwort kommt, + // bleibt Spotify pausiert. + this._releaseFocusDeferred(); // Alle Audio-Teile abgespielt → Listener benachrichtigen this.playbackFinishedListeners.forEach(cb => cb()); return; @@ -670,6 +700,7 @@ class AudioService { // Beim ersten Playback-Start: andere Apps ducken if (!this.isPlaying) { + this._cancelDeferredFocusRelease(); AudioFocus?.requestDuck().catch(() => {}); } this.isPlaying = true; @@ -755,7 +786,8 @@ class AudioService { this.pcmBytesCollected = 0; this.pcmMessageId = ''; } - // Audio-Focus freigeben + // Audio-Focus sofort freigeben — User hat explizit abgebrochen + this._cancelDeferredFocusRelease(); AudioFocus?.release().catch(() => {}); }