From b5656e872ba892cc9c45c55849b2fc8a230b53cb Mon Sep 17 00:00:00 2001 From: Julie Krasnick Date: Fri, 5 Jun 2026 15:46:01 -0400 Subject: [PATCH 1/2] voice-command: stop clipping users mid-sentence in conversation mode Conversation follow-up turns set Google STT's single_utterance mode on, handing end-of-speech detection to Google's VAD, which commits ~200-500ms after any pause and clips users mid-sentence on natural pauses. No config attribute could override it. Turn single_utterance OFF in conversation mode too (it was already off in wake mode), so our own stable-endpoint timer governs the commit instead. That timer resets on every new word, so it only fires once the user has truly stopped, and is tunable via stable_endpoint_ms. Co-Authored-By: Claude Opus 4.8 (1M context) --- resources/voice-command/voice-command.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/resources/voice-command/voice-command.go b/resources/voice-command/voice-command.go index c8da90c..6c926e3 100644 --- a/resources/voice-command/voice-command.go +++ b/resources/voice-command/voice-command.go @@ -874,15 +874,16 @@ func (s *service) listenForCommand(ctx context.Context, startInConversation bool return "", fmt.Errorf("open STT stream: %w", err) } // Send config first. - // Only enable Google's single_utterance mode in conversation follow-up - // mode. There, it gives snappy turn-taking (~200-500ms after the user - // stops talking). In wake mode we keep it OFF — otherwise every - // ambient utterance from colleagues in the room terminates our STT - // stream, forcing a ~4s reopen cycle during which we can't hear the - // wake word. With single_utterance off, the stream stays open until - // we close it (or the 305s limit); wake-mode latency is then capped - // by our own listen-timeout (after wake-word detection) and the - // max-utterance-words cap. + // Keep Google's single_utterance mode OFF in both wake and conversation + // modes. With it on, Google's VAD ends the turn ~200-500ms after a pause, + // which clips users mid-sentence on natural pauses (and in wake mode lets + // any ambient utterance from colleagues terminate the stream, forcing a + // ~4s reopen cycle during which we can't hear the wake word). With it off, + // the stream stays open until we close it (or the 305s limit) and our own + // stable-endpoint timer governs the commit: that timer resets on every new + // word (see onContentChange), so it only fires once the user has truly + // stopped — and is tunable via stable_endpoint_ms. Latency is then capped + // by our listen-timeout and the max-utterance-words cap. if err := sttStream.Send(&speechpb.StreamingRecognizeRequest{ StreamingRequest: &speechpb.StreamingRecognizeRequest_StreamingConfig{ StreamingConfig: &speechpb.StreamingRecognitionConfig{ @@ -892,7 +893,7 @@ func (s *service) listenForCommand(ctx context.Context, startInConversation bool LanguageCode: s.languageCode, }, InterimResults: true, - SingleUtterance: startInConversation, + SingleUtterance: false, }, }, }); err != nil { From 7030ffba5228f90f8706adf7e766d386bc5a3359 Mon Sep 17 00:00:00 2001 From: Julie Krasnick Date: Wed, 24 Jun 2026 10:31:42 -0400 Subject: [PATCH 2/2] flyby --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d1e1013..9a0cd39 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,9 @@ Returns: ```json {"queued": "Hello, your espresso is ready!"} ``` + +--- + +## Reference: LED indicator firmware + +Example ESP32 firmware for the LED indicator driven by `led-bridge` lives at [`resources/led-bridge/firmware/led-indicator.ino`](resources/led-bridge/firmware/led-indicator.ino).