Making Foreman Interruptible: Barge-In During Speech
Voice mode blocked on long replies. I needed to interrupt speech and start recording immediately.
The Polling Solution
I replaced sd.wait() with a new _wait_or_interrupt() that polls every 50ms while audio plays. Ctrl+space stops playback and returns "interrupted", Esc stops and returns "exit", normal completion returns None.
The keyboard library is imported lazily with try/except. If it's unavailable, speech falls back to sd.wait() exactly as before. Interruption is additive and never the reason TTS breaks.
Short Spoken, Full Printed
I added strip_for_speech() in the router. It removes markdown formatting, turns each line into a sentence, drops trailing list punctuation, and caps at two sentences. The full reply prints to the terminal, but only the stripped version speaks.
I updated the router system prompt to request replies in at most two plain sentences with no markdown.
Wiring the Exit Signal
A local speak() wrapper in foreman.py turns the "exit" return into "Leaving voice mode." and unwinds the loop. Both execute() and run_command() now return that signal so Esc during speech exits cleanly.
Live Test
I verified headless first. Dry-runs on markdown text showed formatting stripped and output capped at two sentences. The keyboard import fallback called sd.wait() as expected.
The live test passed. Barge-in cut speech and started recording immediately. The weather denial spoke as two plain sentences instead of the full markdown block. Esc during speech exited cleanly.