Barge-in that doesn't fire on the assistant's own voice
I built a barge-in detector for ODIN that lets you interrupt his spoken replies. The spec called for a grammar of phrases he never says, which the design claimed would make his own speech structurally incapable of firing it.
I measured that claim against his eight authored utterances at full digital level. Three of eight fired. A constrained decoder must map every sound onto a legal phrase or [unk], so short legal phrases collect near-miss audio even when he does not say the words.
Grammar versus free-form
I tried exact match next. The whole result had to be a phrase, nothing else. That drove false fires to zero out of eight, but real barge detection dropped to zero out of twelve. During a barge-in his voice is in the mic too, so every result carries [unk].
Free-form decoding fires on zero of his eight utterances. A grammar is right for a rare proper noun, where the language model fights the name and must be overruled. It is wrong for common words, where the language model is precisely what knows "stop" is not what he just said.
The phrase list
The phrases are stop, quiet, never mind, that's enough, that is enough. Matching is containment, not tail-anchored. A wake is an address and lands at the end of an utterance. A barge-in lands in the middle of a transcript that also contains his bleed on both sides.
Multi-token phrases fire over a full-volume bleed, around 0.035 RMS at the mic. Single-token phrases like "stop" and "quiet" miss until the bleed drops to 0.010-0.020. I kept them because a miss costs a repeat and "stop" is what a person reaches for first.
The deaf window
There is a 500 ms deaf window at the head of every reply while the input device opens. Replies under about a second cannot be interrupted at all. I initially attributed the two-second total latency to the keyboard hook, then to the output device open. Instrumenting the real timeline showed the mic goes live at 500 ms and 310 ms of the tail is device teardown after the audio has already gone silent.
Implementation details
Barge-in is opt-in per say() call. The signature is say(text, barge_in=None). The carve-out is the absence of an argument, so forgetting is safe rather than unsafe. The confirmation question is spoken with allow_barge_in=False and is the only suppressed call site.
When barge-in fires, a 12 ms raised-cosine fade is applied in place to the buffer sounddevice is still reading, then the tail is zeroed, then sd.stop() is called.
The wake detector and barge-in detector share the same Vosk model. Sharing costs 0.25 seconds versus 0.96 seconds for a second lgraph load of around 350 MB.
_assert_excludes_name refuses to construct a BargeInDetector whose phrases contain the wake name in any spelling. Token-level containment, so "wooden" is still allowed.
A module-level _MIC guard raises if a second input stream is opened in one process. I routed capture_utterance through _close_stream so the guard is released.
Testing
scripts/bargein_test.py runs 53 checks, offline by default. --devices opens the real mic and output device using a near-silence buffer. The test re-measures the rejected grammar every run so the comparison cannot rot.
Commit b0cebcb on dev, not pushed.