Half-duplex capture and barge-in in mavwaked (#287) #76
Closed
claude
wants to merge 1 commits from
overnight/voice-barge-in into overnight/stt-golden-audio
pull from: overnight/voice-barge-in
merge into: kami:overnight/stt-golden-audio
kami:master
kami:task/725-capability-ledger-and-empirical-baseline
kami:task/692-heads-path-may-equal-model-path-and-noth
kami:task/694-staticcheck-and-deadcode-are-still-not-i
kami:task/682-go-1-25-5-and-x-text-0-14-0-carry-20-rea
kami:task/674-caveats
kami:task/673-mavgpud-serves-the-model-to-the-whole-la
kami:task/487-capture-device-doc
kami:task/487-capture-device
kami:task/487-wake-word-deploy
kami:task/487-wake-word-threshold
kami:task/487-wake-word-stage-two
kami:task/671-mavwaked-registers-as-a-voice-consumer-i
kami:task/670-cut-claude-md-to-200-lines
kami:task/515-deploy-mavwaked-workpc
kami:task/669-prune-claude-md
kami:task/668-e4b-phrasing
kami:task/668-title-capital
kami:task/668-kiwix-answers-a-question-it-cannot-answe
kami:task/666-only-a-stage-0-grammar-may-take-the-pers
kami:task/487-mavwaked-has-no-wake-word-only-an-energy
kami:task/486-deploy-the-workstation-transcriber
kami:task/486-move-stt-and-tts-to-the-workstation-wher
kami:task/665-crisperwhisper-2-russian
kami:task/664-routing-heads-in-go
kami:task/662-usage-harness-source-badge
kami:task/661-post-merge-usage-rerun
kami:task/661-routing-heads-step-3-train-the-multi-hea
kami:task/660-router-prompt-destination
kami:task/659-destination-fixture
kami:task/655-query-source-is-a-routing-decision-made
kami:task/654-a-pending-clarify-has-no-way-out-neither
kami:task/654-week-of-usage-eval-docs
kami:task/649-needs-kami-telegram-is-the-only-reach-an
kami:task/643-memorystore-search-decodes-and-unmarshal
kami:task/641-two-maps-grow-for-the-process-lifetime-w
kami:task/644-mavcaldav-is-built-documented-as-running
kami:task/642-the-store-caps-sqlite-at-one-connection
kami:task/647-factenrichmentworker-walks-the-pending-q
kami:task/646-v-637-follow-up-telegram-intake-has-no-d
kami:task/638-no-deadline-survives-the-turn-path-from
kami:task/637-inbound-telegram-turns-and-corrections-f
kami:task/636-correcting-a-turn-from-telegram-and-from
kami:task/634-an-act-alias-resolves-the-verb-but-not-t
kami:task/630-one-gesture-correction-on-chat-v-628
kami:task/629-persist-the-routing-trace-and-record-it
kami:task/631-mode-inventory-written-from-the-handlers
kami:task/586-defaultfactparser-uses-hand-written-russ
kami:task/633-reconcile-the-seed-labels-with-the-handl
kami:task/627-reminder-verbs-has-no-alarm-verb-so-an-a
kami:task/626-the-classifier-seeds-teach-an-older-inte
kami:task/546-route-with-a-fine-tuned-e5-small-instead
kami:task/586-measure-the-fact-parser
kami:fix/gofmt-ecosystem-acts
kami:task/584-media-store-a-failed-write-leaks-its-bud
kami:task/518-no-write-path-for-a-backdated-event-so-t
kami:task/287-qa-voice-session-quality-polish
kami:task/492-qa-plan-reconcile
kami:task/530-sweep-tail-four-files-the-russian-sweep
kami:task/405-score-how-often-a-real-utterance-reaches
kami:task/529-money-and-list-pick-a-mechanism
kami:task/528-sweep-tail-the-three-files-on-467
kami:task/527-embedder-open-set-phrasings-stop-being-r
kami:task/526-morphology-a-dictionary-answers-the-gram
kami:task/525-lexicons-the-finite-russian-sets-move-to
kami:task/524-entity-reference-ask-nexus-about-every-l
kami:task/523-risk-tiers-take-hexis-s-tier-for-a-hexis
kami:task/521-review-pr-111-query-strings-declension-h
kami:task/491-llama-server-core-dumps-on-every-sigterm
kami:task/479-bug-an-unconfigured-capability-does-not
kami:task/467-bug-spoken-task-capture-is-dead-the-rout
kami:task/463-deploy-mavwaked-and-mavenclient-run-nowh
kami:task/480-hearing-no-shipped-client-can-start-a-re
kami:task/432-ambient-calendar-intake-is-fragile-and-p
kami:task/431-board-surface-maven-holds-the-work-board
kami:task/433-reactivehandler-has-30-fields-and-is-pas
kami:task/371-swap-the-embedder-for-an-asymmetric-retr
kami:task/408-review-31-07-split-the-30-method-coreapi
kami:task/410-review-31-07-hand-rolled-string-enums-st
kami:task/423-review-pr50-split-internal-ipc-server-go
kami:task/422-review-pr50-split-cmd-mavend-tick-go-860
kami:task/409-review-31-07-finish-moving-mavweb-markup
kami:task/482-ambient-ingest-reads-a-notification-s-ti
kami:task/444-kuma-a-fact-per-monitor-so-she-can-name
kami:task/452-capability-model-homelab-docker-restart
kami:task/449-destructive-confirm-policy-risk-tiers-no
kami:task/453-grocery-list-items-table-fourth-append-o
kami:task/399-run-the-persona-checks-inside-the-daemon
kami:task/448-bounded-follow-up-state-pending-candidat
kami:task/455-conversation-repair-name-the-misroute-co
kami:task/454-go-mod-tidy
kami:task/458-pronunciation-dictionary-for-piper
kami:task/456-command-history-read-only-query-over-exi
kami:task/457-clarification-templates-for-the-router-s
kami:task/474-query-source-ordering-feeds-and-calendar
kami:task/469-reminders-spelled-out-times-fail-the-bod
kami:task/475-bug-the-praxis-attention-capability-is-u
kami:task/481-bug-a-transient-complaint-is-stored-as-a
kami:task/476-bug-the-router-transliterates-latin-enti
kami:task/385-decide-whether-a-parked-clarify-question
kami:task/377-backfill-routines
kami:task/421-weather-geocoder
kami:task/390-no-read-path-for-delivery-attempts
kami:task/386-recall-fixture-filler-note-ids
kami:task/473-bug-morning-item-has-no-required-flag
kami:task/465-bug-make-simulate-routes-with-an-empty
kami:task/467-bug-spoken-task-capture-is-dead
kami:task/466-bug-a-pending-clarify-is-global-so-one-u
kami:task/468-bug-pattern-detect-has-no-minimum-interv
kami:task/462-bug-checkfeminine-flags-second-person-ma
kami:task/443-safekey-drops-cyrillic-so-russian-calend
kami:task/471-bug-agendaquerygrammars-covers-today-but
kami:task/383-slottext-in-clarify-answer-would-clobber
kami:task/323-qa-phraser-coverage-is-65-3-but-the-llam
kami:task/498-bug-and-x-reach-the-model-with-no-determ
kami:task/506-strings-family-6-summaries-and-reports-i
kami:task/504-strings-family-4-act-and-smart-home-repl
kami:task/503-strings-family-3-query-answers-and-gaps
kami:task/502-strings-family-2-capture-acknowledgement
kami:task/501-strings-family-1-phrasing-fallbacks-into
kami:task/397-phrasechat-and-phrasequery-hide-model-fa
kami:task/396-the-reply-path-can-t-be-tested-llmreplie
kami:task/496-recall-a-cross-language-question-loses-i
kami:task/495-bug-x-escapes-the-personal-boundary-and
kami:task/499-llama-server-holds-7-9gb-rss-for-a-1-1gb
kami:task/470-bug-a-question-writes-invented-knowledge
kami:task/493-bug-the-memory-index-stores-the-raw-utte
kami:task/490-name-the-gap-world-questions-through-the
kami:task/485-run-the-big-model-on-the-workstation-wit
kami:task/489-workstation-deploy-mavgpud-on-workpc-and
kami:task/488-workstation-a-supervisor-that-keeps-llam
kami:task/483-docs-offload-design
kami:task/483-design-offload-ml-to-the-workstation-kee
kami:task/459-docs-refresh-the-qa-plan-against-the-liv
kami:task/446-doc-reorg-tier-the-tree-retire-the-three
kami:fix/367-voice-parks-routine-accept
kami:task/365-dialogue-slots-and-router-slots-are-hand
kami:task/364-snooze-does-nothing-at-runtime-the-gate
kami:task/447-retire-progress-md-the-backlog-and-the-f
kami:task/445-session-workflow
kami:overnight/eco-versioned-traces
kami:overnight/eco-entity-refs
kami:overnight/eco-degraded-suite
kami:overnight/netscan
kami:overnight/smarthome
kami:overnight/replay-simulator
kami:overnight/event-envelope
kami:overnight/coldstart-unlock
kami:overnight/stt-golden-audio
kami:overnight/senses-speaker
kami:overnight/senses-hearing
kami:overnight/senses-media-vision
kami:overnight/mcp-tools
kami:overnight/mcp-client
kami:overnight/self-update
kami:overnight/model-swap
kami:overnight/web-crawler
kami:overnight/rss-feeds
kami:overnight/email-poller
kami:overnight/email-extract
kami:overnight/email-imap
kami:overnight/money-zenmoney
kami:overnight/task-priority
kami:overnight/task-capture
kami:overnight/behavior-profile
kami:overnight/day-plan
kami:overnight/ambient-calendar
kami:overnight/local-calendar
kami:overnight/memory-eval
kami:overnight/proactive-proposals
kami:overnight/split-voice-quiet
kami:overnight/nginx-maven-block
kami:overnight/stepup-chat-surface
kami:integration/small-batch
kami:docs/fix-drift
kami:fix/ru-wording
kami:integration/jul31
kami:overnight/resident-1.7b
kami:overnight/nudge-templates
kami:overnight/kiwix-rewrite
kami:overnight/eval-writeup
kami:overnight/fix-truncation
kami:overnight/kiwix-client
kami:overnight/ru-prompts
kami:overnight/external-data
kami:overnight/phrasing-grammar
kami:overnight/talk-eval
kami:overnight/prompt-context
kami:overnight/prompt-address
kami:overnight/eval-label-kill
kami:overnight/delivery-boundary
kami:overnight/address-check
kami:overnight/system-replies-pr
kami:overnight/clock-intent-pr
kami:overnight/embedder-backfill-pr
kami:overnight/embedder-marker-pr
kami:overnight/note-recall-pr
kami:overnight/thinking-off-pr
kami:overnight/dialogue-persist-pr
kami:overnight/persona-2p-pr
kami:overnight/clarify-expiry-pr
kami:overnight/clarify-rework
kami:overnight/phrasing
kami:overnight/bakeoff
kami:overnight/recall-margin
kami:overnight/router-on
kami:overnight/slot-extract
kami:overnight/embedder-e5
kami:overnight/router-refusal
kami:overnight/eval-rerun
kami:overnight/eval-harnesses
kami:overnight/eval-rerun-base
kami:overnight/fmt-gate
kami:overnight/routines-fire
kami:overnight/router-prompt
kami:overnight/away-leak
kami:overnight/recall-eval
kami:overnight/snooze-works
kami:overnight/clarify-wiring
kami:overnight/delivery-tests
kami:overnight/routine-accept
kami:overnight/llm-router-flag
kami:overnight/clarify-data-layer
kami:overnight/loop-rule-tests
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "overnight/voice-barge-in"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What changed
cmd/mavwaked— playback and the gate around it.playback.go—playerinterface plusaplayPlayer, which holds the aplay process handle so playback can be cut mid-utterance. Previouslygo playAudio(...)with nothing to cancel.session.go— the per-frame decision, lifted out ofmain.go'sprocessFramebehindplayerandutteranceSenderinterfaces.main.go— wires them, adds-barge-in,-barge-in-rms,-barge-in-frames.session_test.go,playback_test.go— 11 tests, no mic, no speaker, no daemon.Why
Two of the eight items on #287, and the two that are actually audible.
She answered herself. The capture loop kept feeding the VAD while the speaker was running, so Maven's own reply came back in through the mic, tripped the VAD, and went to the daemon as a fresh command. There is no acoustic echo canceller here, so the correct fix is half-duplex: while she speaks, capture is muted. This part is unconditional — it repairs a defect rather than adding a capability.
Talking over her did nothing. Now it can, but only when configured.
-barge-inis off by default because the threshold is room-specific: without an echo canceller the only signal distinguishing "he is talking over her" from "the mic is hearing her" is that he is much louder, and how much louder depends on where the mic sits relative to the speaker. Defaults when enabled are 0.12 normalised RMS sustained over 5 frames (150 ms) — an order of magnitude above the VAD's own 0.01 floor, and long enough that a door closing does not cut her off.Both are the same mechanism seen from opposite sides, which is why they are one PR.
How it was verified
make buildandmake testboth exit 0.cmd/mavwakedcoverage 32.4% to 44.4%. The tests assert the behaviour, not the tuning:aplayPlayeris safe toStop()when idle and spawns nothing for empty audio — neither needs aplay(1) installed.The acoustic half (does 0.12 actually clear this room's speaker leak) cannot be measured without the hardware; it is a QA step on the task.
Not in this PR
The other six open items on #287 — partial STT display, per-segment retry, per-room mic profiles, noise-floor calibration, short-response-while-speaking — are untouched and stay on the task. Partial STT in particular needs a streaming wire change, not a client change.
Vikunja #287
Half-duplex is the right fix and the reasoning holds. With no echo canceller, muting capture is the only honest option. Barge-in has to be off by default because the threshold is a property of the room. Putting
aplaybehind aplayerinterface makes both halves testable. Thegencounter inaplayPlayercloses a real race, where a finishing playback clears the flag of the one that replaced it.TestSessionEchoLevelAudioNeverBargesInis the test that matters most and it is there.1.
dispatchresets the VAD for a reason that is not the real one, and skips it on both paths that need itThe comment above the reset reads:
It has not.
feedis called synchronously fromcaptureLoop, anddispatchblocks inside it. Whilesender.Sendis in flight, nothing reads fromsrcand nothing is fed to the VAD. The audio piles up in arecord's pipe and the kernel buffer, not inv.utterance. The VAD state at that moment is exactly whatFeedleft it as when it emitted the utterance.The backlog is real, but it arrives after
dispatchreturns. Which means the reset is placed correctly by luck on one path and is missing on the two that matter:dispatchlogs and returns before the reset and beforePlay. The player never starts,Playing()is false, and the whole pipe backlog feeds straight into the VAD at pipe speed. A round-trip is p50 2.7s through the LLM router. That is roughly 90 frames of room, of him finishing his sentence, of the TV. A spurious utterance goes to the daemon.TestSessionEmptyReplyLeavesCaptureOpenpasses becausefakeSenderreturns instantly. No test models a slow round-trip followed by a burst of buffered frames. That is the only condition under which any of this shows.Reset unconditionally at the top of
dispatch's return path, or better, drain the reader before resuming.2. The same backlog is scored for barge-in, so she can be cut off before she says a word
With
-barge-inon, the frames drained afterPlaystarts are evaluated againstbarge.RMSwith no notion of when they were captured. Walk it:-max-ms. The VAD caps the utterance at 10s and emits it while he is still talking.dispatchblocks for the round-trip. The rest of his sentence goes into the pipe.Playstarts.Playing()is true.feedcallsplayer.Stop().She is cut off by audio recorded before she started speaking. The
Frames: 5guard is documented as "long enough that a door or a cough does not cut her off". That argument assumes the frames are real time. Draining a backlog breaks the assumption. Either discard whatever is already buffered at the momentPlaystarts, or make the loud-frame counter time-aware rather than count-aware.3. Barge-in throws away the 150ms that proved he was talking
When
loudFramesreachesbarge.Frames, the handler stops playback, callsvad.Reset(), and returns. The five frames that triggered it are gone. The VAD begins from the next frame, so the first 150ms of his interruption is clipped before whisper ever sees it. On a short interrupt, that is the whole first word.TestSessionCapturesTheInterruptingUtteranceis named for this and does not test it. After the five loud frames it callsspeakThenPause, which synthesises a fresh complete utterance. It shows that a later utterance is captured, which was never in doubt. It never checks what happened to the trigger frames. Keep them in a small ring and feed them to the VAD after the reset.4. A stuck
aplaynow deafens her, where before it could notPlaying()gates all capture.aplayPlayeronly clears the flag when the child exits or the 30s timeout fires. If the ALSA device is busy,aplayblocks on the write. The flag then stays set for the full 30s. During that window every frame is discarded and she answers nothing. Barge-in cannot rescue it, because barge-in is off by default.Before this PR,
go playAudio(reply)could not do this: a wedged player was inaudible, not deafening. The 30s ceiling is inherited from the old code, where it only bounded a leaked goroutine. Now it bounds how long she can be deaf. Cap the mute window by the reply's own duration plus a small margin, which you already have fromreply.Duration().Smaller notes
-barge-in -barge-in-rms 0logsbarge-in on (rms 0.0000 x 5)and then does nothing, becauseEnabled()requiresRMS > 0. The log is the only feedback the operator gets, and it says the opposite of what happened.s.sent++indispatchruns before the error check, so the counter reads "utterances shipped to the daemon" but counts failed round-trips too.main.gosays barge-in needs "sustained energy well above the speaker's leak level".bargeInConfig.RMSis compared against a raw frame RMS, with no reference to what the speaker leaks. So "well above" is a number the operator guesses, and guesses again when the mic moves. Log the observed RMS of suppressed frames at intervals. He can then tune it from data.Landed on master. The stack was one linear chain, so #84 carried every commit from #50 up, and master now contains this branch in full. Merging this PR on its own is an empty diff, so it is closed rather than merged. The review findings for it were fixed in the 2026-08-01 pass and are on master as commits on the stack tip, not on this branch.
Pull request closed