Utter is a local text-to-speech command-line tool for Apple Silicon Macs. It uses Qwen3-TTS CustomVoice through MLX-Audio and writes mono PCM-16 WAV files without sending synthesis text to a cloud API.
The project is under active development. The current CLI supports explicit model installation, model status inspection, voice listing, and local speech synthesis:
uv sync
uv run utter models pull
uv run utter voices
uv run utter speak "This is a flower." --voice Ryan -o flower.wav
afplay flower.wavInstall the CLI once to make utter available from any project:
uv tool install --python 3.12 \
"utter-mlx @ git+https://github.com/maskedsyntax/utter.git@master"
uv tool update-shell
utter models pullFor reproducible installs, replace master with a release tag or commit SHA.
The distribution is named utter-mlx because utter belongs to an unrelated
project on PyPI; the executable and Python package remain utter.
See AGENT_INTEGRATION.md for a copy-ready agent policy, exit-code contract,
and Python/Node subprocess examples. Calling the global CLI with --json is the
recommended integration for OpenCode, Claude Code, Cursor, VS Code, and other
agents that can execute shell commands.
- macOS on Apple Silicon
- Python 3.12
uv- Approximately 3.1 GB for the pinned model snapshot
- Approximately 4.3-7.8 GB of measured MLX allocation during synthesis, depending on generated length
Intel Macs, Linux, Windows, CUDA, cloud synthesis, voice cloning, and streaming are not supported.
uv sync --all-groups
uv run utter --help
uv run pytestThe dependency graph is pinned in uv.lock.
The default test run does not load or download the model. Run the installed real-model integration tests explicitly:
UTTER_RUN_MODEL_TESTS=1 uv run pytest -m modelThese tests exercise both the Python adapter and the packaged CLI, block socket network calls during synthesis, and validate the generated WAV. To validate a completely fresh multi-gigabyte download in isolated caches:
UTTER_RUN_DOWNLOAD_TEST=1 uv run pytest -m downloadThe fresh-download test can take a long time and consume approximately 3.1 GB, so it is never enabled by the normal suite.
Generate the fixed multi-voice listening corpus with:
uv run python scripts/quality_review.py --forceSee QUALITY_REVIEW.md for the playback order, listening criteria, and review
worksheet. Generated audio is stored under ignored artifacts/quality-review/.
Run the target-machine performance benchmark with:
HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
uv run python scripts/benchmark_m4.pyMeasured M4 results are recorded in BENCHMARK_M4.md.
uv run utter models pullThis downloads and validates the exact pinned model revision in an Utter-owned cache:
~/Library/Caches/utter/models/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit/
The command is resumable and idempotent. Interrupted downloads remain in a
partial directory for a later pull to resume, but are never marked ready or
used for synthesis. Normal utter speak execution loads only the validated
local path with Hugging Face and Transformers offline modes enabled; it does
not implicitly download a missing model.
Inspect installation state in human-readable or JSON form:
uv run utter models status
uv run utter models status --jsonStatus reports the pinned repository and revision, quantization, cache path,
installed/complete/ready states, disk usage, runtime versions, and any detected
issues. A missing model is a successful status query with ready: false.
The lower-level Phase 0 diagnostic remains available for runtime development:
uv run python scripts/runtime_spike.py --forceuv run utter voices
uv run utter voices --jsonVoice IDs are exact and case-sensitive. The current presets are Vivian, Serena, Uncle_Fu, Dylan, Eric, Ryan, Aiden, Ono_Anna, and Sohee.
uv run utter speak "This is a flower." \
--voice Ryan \
--language en \
-o flower.wavStyle instructions are optional:
uv run utter speak "Welcome back." \
--voice Aiden \
--style "Speak calmly and warmly." \
-o welcome.wavText can also be read as strict UTF-8 from stdin:
printf '%s' "This is a flower." | \
uv run utter speak --stdin --voice Ryan -o flower.wavPositional text and --stdin cannot be used together, and empty stdin is an
error.
Use --force to atomically replace an existing destination. Without it, Utter
refuses to overwrite files.
Pressing Ctrl+C during model loading or synthesis exits with status 130. Utter does not publish a partial destination, and any temporary WAV created before an interruption is removed.
Supported language codes are en, zh, ja, ko, de, fr, ru, pt,
es, and it. The default is en.
uv run utter speak "This is a flower." \
--voice Ryan \
-o flower.wav \
--jsonIn JSON mode stdout contains only the final JSON result. Model loading, progress, warnings, and errors are written to stderr.
- Short-to-medium text is the current target. Utter does not yet segment long documents.
- The model is loaded for each CLI invocation; there is no warm daemon.
- WAV is the only output format.
- Audio generation is stochastic and may vary between runs.
Utter is licensed under the MIT License. The pinned Qwen model is identified as
Apache-2.0, and MLX-Audio and MLX are MIT-licensed. Exact pinned artifact and
runtime notices are recorded in THIRD_PARTY_NOTICES.md, which is included in
built wheels and source distributions.