Skip to content

Hands-free: Stop steps down during a take (exit lock / cancel), noise-loop exit, stale-audio clear - #1353

Merged
KeithCu merged 2 commits into
masterfrom
hf-stop-step-down
Oct 6, 2026
Merged

KeithCu merged 2 commits into
masterfrom
hf-stop-step-down

Conversation

@KeithCu

@KeithCu KeithCu commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Problem

When hands-free (hold Record about 2 s) is on, the gap between turns is just another take. Stop was greyed out during every take (send_state.py RECORD_CLICKED had stop_enabled=False, and the deal contracts required that). So the only ways out were Clear (which wipes the chat) or closing the sidebar. Headed tester report: Stop is greyed out while hands-free is listening between turns. Stop Rec can't be the exit, because it is the send, and in loud rooms where silence auto-stop never fires you need it to send.

Change

Stop during a take now steps down one level per click:

State Stop click
Locked (hands-free) take Lock off, status "Hands-free off". The take keeps recording, so Stop Rec or silence auto-stop still sends it once, and nothing re-arms afterwards.
One-shot take Cancelled. The new CANCEL_REC_CLICKED / CancelRecordingEffect runs recorder cleanup and deletes the WAV. Nothing is sent, the status is "Recording cancelled" (not "Error"), and the button goes back to Record.
Transcribing / model replying / speaking Unchanged. It still ends hands-free, and Stop during a reply does not re-arm.
  • Stop Rec is unchanged: it sends and keeps the lock.
  • Contracts: the send_state deal contracts now allow Send and Stop to be enabled together only while recording. The why is in the comment above next_state.
  • Where the step-down runs: StopButtonListener.on_action_performed, via SendButtonListener.stop_during_take. The GTK mousePressed hook still only acts while a send is busy, so one click is one step.
  • Speech finished: the speech-finished callback no longer greys out Stop when a take is already recording (barge-in or a hands-free restart).
  • Status while locked: silence progress reads "Hands-free… (N ms silence)".
  • Tooltips: Send and Stop now have tooltips (HelpText) explaining Stop Rec = send and Stop = leave hands-free / cancel. No label changes, so the 1x button row is unaffected.

Other fixes

  • Noise loop: an empty transcript ("[No speech detected.]") finishes as Stopped, so hands-free re-armed every time and noise could loop forever. Hands-free now ends after 2 empty takes in a row (EMPTY_TAKES_EXIT). A non-empty transcript resets the count.
  • Stale audio: if Stop landed after Stop Rec but before the send drain started, the WAV stayed on audio_wav_path and the next typed Send transcribed or attached it. The skip branch now calls clear_pending_audio_wav().
  • Outdated comments: comments in send_handlers._transcribe_audio said Stop kills local Whisper. It doesn't. Stop keeps the transcript for the Ask box, and only sidebar teardown kills the child.
  • Docs: docs/chat/audio-architecture.md gets rows for Stop during a take and for empty takes.

Tests

  • test_send_state.py:
    • Record enables Stop; typing during a take keeps it enabled.
    • Cancel drops the take with no send, no error status, typed text kept, no-op when not recording.
  • test_fsm_verification.py: oracle and Hypothesis invariants updated to the relaxed contracts; CANCEL_REC_CLICKED added to the oracle.
  • test_record_gesture.py: step-down decision, exiting the lock does not swallow the next Stop Rec, hands-free silence text, EMPTY_TAKES_EXIT.
  • test_panel.py (TestHandsFreeRecord):
    • Stop is enabled during a locked take.
    • Locked take + Stop: lock off and still recording; Stop Rec then sends once and nothing re-arms.
    • One-shot take + Stop: cancelled, no send, no "Error", WAV deleted.
    • Two Stops from a locked take cancel it; mousePressed alone does not step down.
    • Hands-free silence text.
    • Speech finishing does not grey out Stop during a take.
    • Two empty transcripts exit hands-free; a non-empty one resets the count.
    • Stop before the send starts clears the pending WAV.
  • Mock sidebar G11 (test_mock_llm_sidebar_uno.py): with an in-process listener, Stop on a one-shot take now asserts cancelled (not recording, not busy, nothing sent).

Local: the related pytest files pass (286), and ruff, basedpyright, ty and mypy are clean on the changed files.

Live checks for the headed tester

  • Locked take: Stop is enabled. First click shows "Hands-free off" and keeps recording; Stop Rec sends once and nothing re-arms. A second Stop cancels.
  • GTK: one Stop click during a take is exactly one step.
  • Tooltips: they show on hover, and the 1x button row isn't clipped.

Mock sidebar hooks fix (2e2a173): in the URP path, a Record click now counts as taken as soon as the label leaves Record or Stop is enabled. Before this, G4's stub auto-stop could send the take before any poll saw Stop Rec, so the fallback started a second take. Master never caught it because URP wait_idle reads "Stop off + Send on" as idle, and Stop used to be greyed out during takes.

CI on 2e2a173: PR CI 37492341929 passed; mock sidebar 37492364772 passed (111 passed, 0 failed).

…-loop exit, stale-audio clear

Stop was disabled during every take, and a hands-free loop is a take
between turns, so the only exits were Clear (wipes the chat) or closing
the sidebar. Stop Rec stays the send (loud rooms need it when silence
auto-stop never fires).

- send_state: Stop is enabled while recording. New CANCEL_REC_CLICKED /
  CancelRecordingEffect drops a take without the "Error" status. The
  deal contracts now allow Send+Stop only while recording.
- panel: StopButtonListener steps down one level per click (locked take
  -> one-shot take still recording, status "Hands-free off" -> cancelled,
  recorder cleanup + WAV deleted). mousePressed stays busy-only so one
  click is one step. The speech-finished callback no longer greys Stop
  during a take. Silence progress reads "Hands-free..." while locked.
- Two consecutive empty transcripts end hands-free (noise loop).
- Stop before the send drain starts clears the pending WAV so a later
  typed Send cannot attach it.
- Send/Stop tooltips explain Stop Rec = send, Stop = leave hands-free /
  cancel; labels unchanged (1x layout).
- Stale send_handlers comments: Stop does not kill local Whisper.
- docs/chat/audio-architecture.md: Stop-during-take and empty-take rows.
G4's stub silence auto-stop sent the take before a poll saw Stop Rec, so
_send_event_or_urp fell back to a RECORD_CLICKED op and started a second
take that never stopped. URP wait_idle (Stop off + Send on) hid it while
Stop was greyed during takes; with Stop now enabled during a take it
timed out.
@KeithCu
KeithCu merged commit ac134d9 into master Oct 6, 2026
2 checks passed
@KeithCu
KeithCu deleted the hf-stop-step-down branch October 6, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant