fix(voice): klarer Befehl = STOP, Gespraech = 30s passiv (kein zweiter Gong)

Nach dem Re-Arm-Fix rief die App resume() → das spielte einen zweiten Gong
und oeffnete ein neues Aufnahme-Fenster, obwohl Stefan nur einen Steuerbefehl
gab. Sein gewuenschtes Verhalten:
- Klarer Befehl (Fast-Path, speak=false, z.B. Liedersteuerung) = KEINE
  Konversation → STOP: direkt zurueck aufs Wake-Word (kein Gong, keine
  Aufnahme, kein 30s-Fenster).
- Gespraech (gesprochene Antwort) = kein neuer Gong, direkt 30s passives
  Lauschen (weiterreden wie mit einem Menschen), 30s still → Wake-Word.

endConversation(skipPassive) neu: true springt direkt zu armed statt in
passives Lauschen. onPlaybackFinished (Gespraech) → passiv (Vordergrund) bzw.
direkt armed (Hintergrund); stille Fast-Path-Antwort → skipPassive; manueller
Stop-Button → skipPassive. resume() bleibt nur noch Mic-Fail-Retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 20:52:04 +02:00
co-authored by Claude Opus 4.8
parent a0494e90ee
commit 9fb29aa517
2 changed files with 15 additions and 14 deletions
+10 -12
View File
@@ -1212,11 +1212,10 @@ const ChatScreen: React.FC = () => {
const _isSilent = (message.payload as any).speak === false const _isSilent = (message.payload as any).speak === false
|| ((message.payload as any).answeredBy === 'fast-path'); || ((message.payload as any).answeredBy === 'fast-path');
if (_isSilent && wakeWordService.isConversing()) { if (_isSilent && wakeWordService.isConversing()) {
if (AppState.currentState === 'active') { // Klarer Steuerbefehl (Liedersteuerung etc.) = KEINE Konversation →
wakeWordService.resume(); // STOP: direkt zurueck aufs Wake-Word. Kein Gong, keine Aufnahme,
} else { // kein 30s-Fenster (skipPassive=true).
wakeWordService.endConversation().catch(() => {}); wakeWordService.endConversation(true).catch(() => {});
}
} }
} }
@@ -1446,12 +1445,11 @@ const ChatScreen: React.FC = () => {
useEffect(() => { useEffect(() => {
const unsubPlayback = audioService.onPlaybackFinished(() => { const unsubPlayback = audioService.onPlaybackFinished(() => {
if (!wakeWordService.isActive()) return; if (!wakeWordService.isActive()) return;
if (AppState.currentState === 'active') { // Gespraech (gesprochene Antwort): KEIN neuer Gong / kein Aufnahme-Fenster,
wakeWordService.resume(); // sondern direkt passives Lauschen (30s weiterreden wie mit einem Menschen).
} else { // Im Hintergrund gibt's kein Multi-Turn → direkt re-armen (skipPassive).
console.log('[Chat] TTS fertig im Background → endConversation (kein Multi-Turn)'); const bg = AppState.currentState !== 'active';
wakeWordService.endConversation().catch(() => {}); wakeWordService.endConversation(bg).catch(() => {});
}
}); });
return () => unsubPlayback(); return () => unsubPlayback();
}, []); }, []);
@@ -2072,7 +2070,7 @@ const ChatScreen: React.FC = () => {
await audioService.stopStreamingRecording('user'); await audioService.stopStreamingRecording('user');
if (wakeWordService.isConversing()) { if (wakeWordService.isConversing()) {
console.log('[Chat] Manueller Stop in Konversation → endConversation, zurueck zu armed'); console.log('[Chat] Manueller Stop in Konversation → endConversation, zurueck zu armed');
await wakeWordService.endConversation(); await wakeWordService.endConversation(true); // explizit gestoppt → STOP (kein 30s-Passiv)
} }
}, []); }, []);
+5 -2
View File
@@ -441,7 +441,10 @@ class WakeWordService {
* wuerde sonst OpenWakeWord.stop() rufen weil bargeListening noch true * wuerde sonst OpenWakeWord.stop() rufen weil bargeListening noch true
* ist, und unseren frisch re-armierten Listener killen. * ist, und unseren frisch re-armierten Listener killen.
*/ */
async endConversation(): Promise<void> { /** @param skipPassive true = KEIN passives Lauschen, direkt zurueck aufs
* Wake-Word (armed). Fuer klare Steuerbefehle (Fast-Path) — nach
* "nächster Titel" will Stefan kein 30s-Fenster, sondern Stop. */
async endConversation(skipPassive: boolean = false): Promise<void> {
if (this.state !== 'conversing') { if (this.state !== 'conversing') {
import('./logger').then(m => m.reportAppDebug('wake.end', import('./logger').then(m => m.reportAppDebug('wake.end',
`endConversation called but state=${this.state} → noop`)).catch(()=>{}); `endConversation called but state=${this.state} → noop`)).catch(()=>{});
@@ -461,7 +464,7 @@ class WakeWordService {
// (Phase 3) filtert fremde Stimmen weg, der User kann ohne erneute // (Phase 3) filtert fremde Stimmen weg, der User kann ohne erneute
// Anrede weitersprechen. // Anrede weitersprechen.
const passiveMs = await loadPassiveListenMs(); const passiveMs = await loadPassiveListenMs();
if (passiveMs > 0 && this.nativeReady) { if (!skipPassive && passiveMs > 0 && this.nativeReady) {
this.enterPassiveListening(passiveMs); this.enterPassiveListening(passiveMs);
return; return;
} }