Interruptible TTS and Short Spoken Replies in Voice Mode
Voice mode blocked in sd.wait() during long replies. I needed barge-in and speech that was readable, not screen-formatted markdown.
Barge-In
I added _wait_or_interrupt() in tools/speak.py. It polls every 50ms while the output stream is active. Ctrl+space stops playback and returns "interrupted". Esc stops playback and returns "exit". Normal completion returns None.
The keyboard library is imported lazily with try/except. If it's unavailable, the code falls back to sd.wait() exactly as before. Keys are checked separately to match tools/listen.py.
I wired foreman.py so Esc mid-speech unwinds the whole voice loop. Every in-loop say() call routes through a local wrapper that turns the "exit" return into "Leaving voice mode." and returns.
Short Spoken Replies
I added strip_for_speech() in tools/router.py. It removes asterisks, backticks, and leading bullet markers. It turns each source line into its own sentence. It drops trailing list-intro punctuation and caps at two sentences.
foreman.py prints the full router reply but speaks strip_for_speech(...) for both the denial branch and describe_capabilities.
I appended an instruction to the router system prompt: "Replies will be read aloud. Answer in at most two short plain sentences. No markdown, no bullet points, no offers to help further."
The live weather denial arrived already speech-shaped as two plain sentences.
Verification
Headless checks passed. Live test passed: barge-in cut speech and recorded immediately, weather denial spoke as two plain sentences, Esc mid-speech exited clean.
I changed the active TTS voice to af_bella in memory/voice_settings.json. That file is gitignored.
Commit 563bca3 on main and dev.