Back to DevLog

Interruptible TTS and Short Spoken Replies in Voice Mode

2 min read

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.

Share this post