speech
Talk to the agent, and let it talk back
Agent Utils turns Pi into something you can use with the screen off: it reads what you send, speaks replies or narrates its work, takes dictation, holds full voice conversations, and coordinates all of that across sessions and machines so two agents never talk over each other.
From words to a speaker
Speech out
| Command | Speaks |
|---|---|
/read | What you send, read from the editor as it is submitted. Text transformed by /pipe is spoken as finally sent. |
/tts | Each finished assistant reply, verbatim. The agent never has to call a speak tool. |
/narrate | One first-person line before and after each batch of tool calls, written by a small, cheap model. It never blocks tools, and its summaries are queued for your next real turn rather than starting one. |
| spoken choices | Questions and their options, read out when an agent asks (see Sessions). |
Sessions under pi-daemon get the same /tts, /narrate and choice audio from a durable module, which follows the session's events on the daemon host whether or not a client is attached.
Providers
Connections are configured once by name and selected per surface: Azure (with keys, or Entra tokens from an az broker), OpenAI, a speech daemon that can queue and play on a server, or any local command for text to speech.
{ "agentUtils": { "audio": { "providers": { "studio": { "type": "daemon", … }, "cloud": { "type": "azure", … } } } }}
One queue per machine
Several Pi sessions on one machine would otherwise speak over one another. They share a playback queue with no daemon at all:
- synthesised audio and small metadata records are spooled under
~/.cache/agent-utils/tts-queue(or$PI_TTS_QUEUE_DIR), and atomic directory locks coordinate independent processes; - one clip plays at a time, starting two seconds before the previous one ends; bounded parallelism is optional;
- the spool is capped at 1 GiB, cancelled and oldest-waiting clips are cleaned up, and abandoned locks are recovered;
- synthesis and agent work never wait for playback.
/tts focus and /tts solo temporarily route a session's speech to a shared focus output or play it alone, with ownership-checked cleanup afterwards.
Speech in
| Command | Does |
|---|---|
/stt | Dictation with local voice detection: each turn is captured locally, wrapped as WAV and sent as one bounded HTTP request to an OpenAI-compatible transcription endpoint |
/ptt, Ctrl+Space | Push-to-talk, toggled from the editor |
/rt | A full voice conversation with an OpenAI Realtime model, used as an ordinary Pi provider (openai-realtime/gpt-realtime-2 by default), with audio as a side channel |
/cascade | A voice group chat: you speak once, then each agent answers in turn, hearing you and everyone before it, in its own voice |
Local voice detection replaced an older streaming subprocess that could hang on "transcribing" forever. Because the whole candidate turn is already in hand, one request with a timeout and explicit errors is enough. The Realtime guide covers routing to a phone, voice-detection tuning, replays and diagnostics.
Muting, everywhere at once
Speech has four kinds (read, replies, narration and choices), and each can be muted on one machine or across the fleet without touching any session's settings:
ag tts mute # all four kinds, every enabled machine ag tts mute --narrate --local # just narration, just here ag tts statusag tts unmute
The policy lives in ~/.local/state/agent-utils/tts/mute.json. Each mute advances that kind's epoch, and queued or in-flight speech carries the epoch it started with, so muting and quickly unmuting can never replay stale speech. Each kind keeps its own PulseAudio client name, so a mixer can control them separately too.
Sharing audio
Speech is one pathway; deliberately shared audio is another. When an agent makes a song, proposes a sound or records a sample, audio_share puts it in a terminal audio reel in the session, with keyboard and mouse transport, and in a durable archive that ag audio gathers across machines. The reel never touches the speech queue, feed or mute policy, and speech never enters the reel.
♪ ▶ 2/5 Bossa riff in E minor (take 3) ━━━━━━━━━━━●──────────────── 1:23 / 3:45 1 intro sting [2 Bossa riff in E m…] 3 rain loop 4 UI click vol 100% · /audio-reel