Put the cans on.

github.com/veronica-agent/cans

Veronica

Put the cans on.

Type a line. She speaks it.

Install

macOS, Apple Silicon. No Python, no uv.

brew install --HEAD veronica-agent/tap/cans

The mouth is a native Qwen3-TTS worker. First cans doctor (or first cans) downloads it once, about 1.6 GB, into ~/.cans/native.

cans doctor
cans
curl
curl -fsSL https://raw.githubusercontent.com/veronica-agent/cans/main/install.sh | sh

Until a tagged release exists, that script needs Go and installs cans onto PATH. Then cans doctor.

From source
git clone https://github.com/veronica-agent/cans.git
cd cans
just install

just install (same as just run install) is go install into $(go env GOBIN), or $(go env GOPATH)/bin if GOBIN is empty. cans doctor fetches the mouth. just uninstall removes the binary. just uninstall --all also wipes ~/.cans (the native mouth).

Commands

cans
cans say "Put the cans on."
cans keep take.wav -text "the words in the wav"
cans doctor

Keep needs the clip and the words spoken in it. Throat stays put for the session.

The CLI is Go. The mouth is a native Qwen3-TTS worker that clones a wav. No Python.

Scripting

The script owns the document. cans speaks what it is handed.

A script piping lines into cans

--stream reads stdin a line at a time and speaks each line through one worker: one model load for the whole document. Blank lines are skipped and do not take an index.

# one wav per paragraph, script decides what a paragraph is
awk -v RS='' '{gsub(/\n/," "); print}' chapter.md | cans say --stream -o 'out/%03d.wav'

# a line at a time, with the script's own naming
i=0
while IFS= read -r line; do
  i=$((i+1))
  cans say "$line" -o "$(printf 'out/%03d.wav' "$i")"
done < lines.txt

# metadata for a build step
cans say --stream --json -o 'out/%03d.wav' < lines.txt | jq -r 'select(.error==null) | .wav' > manifest.txt
FlagEffect
-o, --outWrite the wav here. Under --stream the path takes one %d, as in out/%03d.wav.
--streamRead stdin line by line, one utterance per line, one worker for all of them. Text on argv is a usage error.
--jsonOne JSON record per utterance on stdout; --stream adds "line". wav is present only with -o.
--playPlay the wav as well as writing it. Needs -o.
--nowaitDo not queue behind another cans: give up at once.
--wait <dur>Queue for at most that long. Without either flag, cans waits.
-Read one utterance from stdin. Text on argv is a usage error.
ExitMeaning
0Spoke it.
1Runtime failure, or a line in a stream failed.
2Usage error.
75Another cans holds the mouth and the wait was refused or ran out.
130Interrupted.

stdout carries data — a JSON record, a wav path, or ttfa_ms=N, the worker's total synthesis time for that line. Prose goes to stderr.

Ctrl-C stops the stream: the line being spoken is dropped, finished wavs stay, exit 130. A second Ctrl-C stops at once.

Without -o the wav is a temp file removed after playback. --json without -o omits wav so a script is not handed a path that will not survive. Pass -o if the file is needed.


Built with Festival