Local streaming speech API for voice agents.
Muzzle runs a FastAPI service with:
- streaming TTS from Chatterbox Turbo
- streaming STT from Faster-Whisper
- WebSocket sessions for live mic input and generated audio output
Install the TTS/STT dependencies:
make sync-real
make torch-checkStart the service:
make devThe first startup downloads model weights for Chatterbox and Whisper. That can take a while.
If your GPU backend is not detected correctly, pick it explicitly:
TORCH_BACKEND=cu128 make sync-real
TORCH_BACKEND=cu130 make sync-realmake dev intentionally uses uv run --no-sync, so run make sync-real first. That avoids uv undoing the repaired torch install.
With make dev running:
make example-ttsUse higher quality audio:
make example-tts QUALITY=highDisable playback and just print events:
make example-tts TEXT="Hello from Muzzle." QUALITY=high PLAYER=noneSave raw PCM output:
make example-pcm PCM_OUT=/tmp/muzzle-example.pcmReplay it with PipeWire:
pw-cat --playback --raw --format s16 --rate 24000 --channels 1 /tmp/muzzle-example.pcmPlay and benchmark streaming TTS directly on CPU, without starting the API server or STT model:
make example-tts-cpuThe CPU example uses the cpu-smooth quality profile, plays through pw-cat when PipeWire is available, prints the resolved chunk_tokens and crossfade_ms, prints per-chunk timing, and ends with a final speed= value. speed=1.00x means the model generated one second of audio per wall-clock second; values near or above 1.00x are near real time. Use CPU_QUALITY=fast, CPU_QUALITY=balanced, or CPU_QUALITY=high to compare other profiles, CROSSFADE_MS=40 CHUNK_TOKENS=64 to tune chunk joins manually, LATENCY_MS=500 to test whether live playback is underrunning, PLAYER=none to disable playback, and PCM_OUT=/tmp/muzzle-cpu.pcm to save the streamed audio.
With make dev running:
make example-sttRecord longer:
make example-stt STT_SECONDS=20Health:
GET /healthz
Voices:
GET /v1/voices
POST /v1/voices
DELETE /v1/voices/{voice_id}
Sessions:
GET /v1/sessions
/v1/sessions is a WebSocket endpoint. Send JSON control events and binary mic frames.
Example client events:
{"type":"session.configure","voice_id":"default","stt_language":"en"}
{"type":"tts.speak","request_id":"tts-1","text":"Hello from Chatterbox Turbo.","quality":"high"}
{"type":"tts.cancel","request_id":"tts-1"}
{"type":"input_audio.commit"}
{"type":"session.close"}Client mic audio is raw mono pcm_s16le, 16 kHz. Keep frames around 20-100 ms for normal interactive use.
TTS audio is returned as:
- JSON
tts.audio.chunkmetadata - the matching binary
pcm_s16leaudio frame
Example server events:
{"type":"stt.partial","text":"hello","is_final":false}
{"type":"stt.final","text":"hello world","is_final":true}
{"type":"tts.audio.chunk","request_id":"tts-1","index":0,"sample_rate":24000,"bytes":5760}
{"type":"tts.done","request_id":"tts-1","status":"completed"}Set quality per generation:
{"type":"tts.speak","request_id":"tts-1","text":"Hello.","quality":"high"}Profiles:
| Quality | Best for | Tradeoff |
|---|---|---|
fast |
lowest latency | rougher chunk joins |
balanced |
default interactive use | moderate latency |
high |
smoother audio | higher latency |
cpu-smooth |
CPU streaming tests | smoother joins with larger chunks |
You can still override generation knobs directly:
{"type":"tts.speak","request_id":"tts-1","text":"Hello.","quality":"high","chunk_tokens":80,"crossfade_ms":40}Create a managed voice with multipart form data:
name: display namereference_audio: reference clip
For Chatterbox, use clean reference audio with one speaker and at least 5 seconds. Ten to thirty seconds is usually better.
Prepared voices are stored under:
data/voices/{voice_id}
The built-in default voice cannot be deleted.
| Variable | Default | Meaning |
|---|---|---|
MUZZLE_MODEL_BACKEND |
real |
real or fake |
MUZZLE_DATA_DIR |
data |
Voice metadata, reference audio, and cached conditionals |
MUZZLE_AUTH_TOKEN |
unset | Optional bearer token for HTTP and WebSocket requests |
MUZZLE_TTS_DEVICE |
auto |
cuda, mps, cpu, or auto |
MUZZLE_WHISPER_MODEL |
distil-large-v3 |
Faster-Whisper model name/path |
MUZZLE_WHISPER_DEVICE |
auto |
cuda, cpu, or auto |
Run tests in Docker:
make docker-testRun the service in Docker:
make docker-runRun CPU-only inference in Docker:
make docker-build-cpu
make docker-run-cpuBuild a pinned CUDA image instead:
make docker-build-cu128
make docker-build-cu130Run one with GPU access:
make docker-run-cu128
make docker-run-cu130The pinned images use Dockerfile.cu128 and Dockerfile.cu130.
They are based on NVIDIA CUDA cuDNN runtime images, and PyTorch is installed without its vendored CUDA wheel dependencies.
The CPU image uses the shared Dockerfile with the PyTorch CPU wheel index and forces TTS/STT devices to cpu at runtime.
Local Docker tags are muzzle:fake, muzzle:cpu, muzzle:cu128, and muzzle:cu130. muzzle:latest is an alias for the CPU image.
Docker builds install PyTorch from explicit PyTorch wheel indexes and pin matching torch and torchaudio versions with DOCKER_TORCH_VERSION, which defaults to 2.11.0.
Run the API tests:
make testThe tests use the fake backend, so they do not download model weights.
You can also run the fake server directly while working on API plumbing:
make sync
make dev-fakeUseful targets:
make sync
make sync-real
make test
make dev-fake
make dev
make example-tts
make example-stt
make torch-checkTTS uses the pinned streaming Chatterbox fork:
github.com/chrishayen/chatterbox @ ce5a900dbdbc776eeb3d00c1c1607143c7604a5b