fix(voice): ARIA schliesst bei Steuerbefehl die Aufnahme selbst (stiller Stop)

Steuerbefehle laufen stumm (speak=false in Fast-Path/Local/Claude-Pfad) — ohne
TTS feuert aber onPlaybackFinished nie, und daran hing bisher das Aufraeumen der
Aufnahme. Folge: nach "spotify play" blieb das Mikro-/Konversations-Fenster offen
(30s Leerlauf), obwohl der Befehl laengst ausgefuehrt war.

Neu: ariaStopRecording() — das programmatische Gegenstueck zum Stop-Button. Nach
einer stillen Antwort (speak=false) schliesst ARIA jede offene Aufnahme selbst und
geht zurueck aufs Wake-Word, egal in welchem Zustand: passives Lauschen sauber
beenden, offene Streaming-Aufnahme (aktiv/Barge) verwerfen, sonst endConversation.
Vorher nur der conversing-Fall abgedeckt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 13:49:18 +02:00
co-authored by Claude Opus 4.8
parent 0265aabb5e
commit d9bb7239c6
+38 -5
View File
@@ -1407,11 +1407,14 @@ const ChatScreen: React.FC = () => {
// sofort (zurueck aufs Wake-Word), nur echte Gespraeche lauschen weiter. // sofort (zurueck aufs Wake-Word), nur echte Gespraeche lauschen weiter.
converseRef.current = (message.payload as any).converse === true; converseRef.current = (message.payload as any).converse === true;
const _isSilent = (message.payload as any).speak === false; const _isSilent = (message.payload as any).speak === false;
if (_isSilent && wakeWordService.isConversing()) { if (_isSilent) {
// Klarer Steuerbefehl (Liedersteuerung etc.) = KEINE Konversation → // Steuerbefehl (speak=false) ist ausgefuehrt und wird NICHT vorgelesen.
// STOP: direkt zurueck aufs Wake-Word. Kein Gong, keine Aufnahme, // Ohne TTS feuert onPlaybackFinished nie — der Mikro-/Konversations-
// kein 30s-Fenster (skipPassive=true). // Lifecycle haengt sonst (das Ohr bleibt grau, das Aufnahme-Fenster offen).
wakeWordService.endConversation(true).catch(() => {}); // ARIA "drueckt hier selbst Stop": jede offene Aufnahme schliessen +
// zurueck aufs Wake-Word, egal in welchem Zustand (conversing, passives
// Lauschen ODER eine noch offene Streaming-Aufnahme).
ariaStopRecording('silent-command').catch(() => {});
} }
} }
@@ -2298,6 +2301,36 @@ const ChatScreen: React.FC = () => {
return true; return true;
}, [getCurrentLocation, interruptAriaIfBusy, scheduleStaleAudioCleanup]); }, [getCurrentLocation, interruptAriaIfBusy, scheduleStaleAudioCleanup]);
// ARIA schliesst die Aufnahme SELBST — das programmatische Gegenstueck zum
// Stop-Button. Aufgerufen nach einem stillen Steuerbefehl (speak=false): der
// Befehl ist ausgefuehrt, ARIA hat die Rueckinfo (Skill-Ergebnis) und antwortet
// NICHT vorgelesen. Weil ohne TTS kein onPlaybackFinished kommt, muss der
// Aufnahme-/Konversations-Zustand hier aktiv aufgeraeumt werden, sonst bleibt
// das Ohr haengen bzw. das Aufnahme-Fenster laeuft leer weiter (Stefans
// Reproduktion: "spotify play" und das Mikro wartet trotzdem 30s).
// Unterschied zum manuellen Stop: der verwirft NICHT, sondern finalisiert die
// Aufnahme (User will seinen Satz verarbeitet haben) — hier ist der Befehl
// schon durch, ein evtl. offenes Folge-Fenster wird verworfen.
const ariaStopRecording = useCallback(async (reason: string): Promise<void> => {
converseRef.current = false;
// 1) Passiv-Lauschen: sauber beenden (cancelt den Stream selbst, startet
// KEINE neue passive Aufnahme).
if (wakeWordService.getState() === 'listening') {
await wakeWordService.exitPassiveListening('manual').catch(() => {});
return;
}
// 2) Noch offene Streaming-Aufnahme (aktiv / Barge-In) verwerfen.
if (audioService.isStreamingRecording()) {
await audioService.cancelStreamingRecording(reason).catch(() => {});
}
// 3) Konversation beenden → zurueck aufs Wake-Word (skipPassive: kein 30s-Fenster).
if (wakeWordService.isConversing()) {
await wakeWordService.endConversation(true).catch(() => {});
} else if (!wakeWordService.isActive()) {
setWakeWordActive(false);
}
}, []);
// Manueller Aufnahme-Knopf — Stop. Sendet stt_stream_end an Whisper, die // Manueller Aufnahme-Knopf — Stop. Sendet stt_stream_end an Whisper, die
// dann ihrerseits den finalen Text als stt_endpoint emittiert. aria-bridge // dann ihrerseits den finalen Text als stt_endpoint emittiert. aria-bridge
// forwarded direkt an Brain. Im wake-word-conversing-Fall zusaetzlich // forwarded direkt an Brain. Im wake-word-conversing-Fall zusaetzlich