Distributed Qwen3-TTS — Rust-based distributed text-to-speech optimized for low-cost SBC clusters.
We have a Web UI now! Start the server and open the built-in web interface in your browser.
Single binary provides CLI, an OpenAI-compatible HTTP API (with web UI), an MCP stdio bridge, and inference workers. Models auto-download from HuggingFace Hub.
We have WebUI Now!
Run these on the corresponding machines:
# Main node: Talker worker
curl -fsSL https://github.com/darkautism/qwen3-tts/raw/refs/heads/master/deploy/systemd/install-talker.sh | bash
# Compute node: Predictor worker (default q4)
curl -fsSL https://github.com/darkautism/qwen3-tts/raw/refs/heads/master/deploy/systemd/install-predictor.sh | bash
# Compute node: Vocoder worker (ONNX FP32, no RKNN noise)
curl -fsSL https://github.com/darkautism/qwen3-tts/raw/refs/heads/master/deploy/systemd/install-vocoder.sh | bash
# Main node: Frontend (OpenAI API + Web UI)
curl -fsSL https://github.com/darkautism/qwen3-tts/raw/refs/heads/master/deploy/systemd/install-frontend.sh | bashEach script will:
- Install
qwen3-tts-rsfrom GitHub to~/.cargo/bin(cargo install --git https://github.com/darkautism/qwen3-tts --locked --root "$HOME/.cargo" qwen3-tts-rs) if~/.cargo/bin/qwen3-ttsis missing. - Download the corresponding unit file from GitHub raw path to
~/.config/systemd/user/. - Reload and enable the user service.
- Attempt
loginctl enable-linger $USERso user services survive reboot.
Config note:
- Frontend reads
~/.config/qwen3-tts/config.tomlundersystemctl --user. - If missing, generate it with:
qwen3-tts init --talker-ip <IP1> --predictor-ip <IP2> --vocoder-ip <IP2>.
Machine 1 (SBC / Server) Machine 2 (SBC / Server)
┌──────────────────────────┐ ┌──────────────────────────┐
│ qwen3-tts (Rust) │ │ Worker: Predictor │
│ ├── CLI / API / MCP │ TCP │ ├── CodePredictor │
│ └── Orchestrator │◄───────►│ │ (Candle Q8, CPU) │
├──────────────────────────┤ │ │ :9091 │
│ Worker: Talker :9090 │ ├──────────────────────────┤
│ ├── Tokenizer (HF) │ │ Worker: Vocoder │
│ ├── TextEmbedder (npy) │ │ └── Vocoder │
│ └── Talker LLM │ │ (ONNX, CPU) :9092 │
│ (Candle GGUF, CPU) │ └──────────────────────────┘
└──────────────────────────┘
Three Worker Roles:
| Worker | Function | Compute | Default Port |
|---|---|---|---|
| Talker | Tokenizer + TextEmbedder + LLM | CPU | 9090 |
| Predictor | CodePredictor (Candle GGUF, default) + ONNX fallback + feedback embedding | CPU | 9091 |
| Vocoder | Vocoder (ONNX FP32 CPU) | CPU | 9092 |
Distributes workload across low-cost SBCs or any Linux machines. Token generation: ~5.5 tok/s with stripped code-predictor GGUF (default). Predictor backend default is GGUF (Candle/GGML path); ONNX is fallback/diagnostic only.
- 1–3 Linux machines (low-cost ARM SBCs work well; 4GB+ RAM per node)
- Any aarch64 or x86_64 Linux system — tested on RK3588, should work on other ARM boards
Inference core (Talker, Predictor) uses Candle (pure Rust) — no external libraries needed.
Vocoder machine requires:
| Library | Source | Install Path |
|---|---|---|
libonnxruntime.so |
uv pip install onnxruntime or uv pip install onnxruntime-gpu |
System path or ORT_DYLIB_PATH |
# CPU (uv)
uv pip install onnxruntime
# CUDA (uv, GPU build)
uv pip install onnxruntime-gpu
# uvx alternative
uvx pip install onnxruntime
uvx pip install onnxruntime-gpu
# If the library isn't on your system path, set ORT_DYLIB_PATH
export ORT_DYLIB_PATH=/path/to/libonnxruntime.soWith
--features onnx-cuda, install both CUDA Toolkit and cuDNN, otherwise CUDA EP initialization will fail.
Download from:
https://github.com/microsoft/onnxruntime/releases/tag/v1.24.2
After extracting, point ORT_DYLIB_PATH to the runtime library:
- Linux x64 GPU:
.../onnxruntime-linux-x64-gpu-1.24.2/lib/libonnxruntime.so - Linux aarch64 CPU:
.../onnxruntime-linux-aarch64-1.24.2/lib/libonnxruntime.so
You can also place libonnxruntime.so in a standard library search path.
With --features rknn-vocoder, the vocoder runs on Rockchip NPU instead of CPU.
Note: RKNN INT8 quantization introduces audible noise in the output. Use only if RTF reduction is critical.
# Requires librknnrt.so and RKNPU kernel driver
sudo curl -L https://github.com/airockchip/rknn-toolkit2/raw/refs/heads/master/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so \
-o /lib/librknnrt.so# Standard build (Candle inference + ONNX FP32 vocoder — pure Rust, no extra .so)
cargo build --release
# CUDA ONNX (vocoder + ONNX predictor on GPU)
cargo build --release --features onnx-cuda
# RKNN INT8 vocoder (Rockchip NPU only — faster but introduces quantization noise)
cargo build --release --features rknn-vocoder
# Output: target/release/qwen3-tts (~15-20 MB)Cross-compile:
cross build --release --target aarch64-unknown-linux-gnu
| Feature | Description | Extra Dependencies | Performance |
|---|---|---|---|
| (default) | Candle inference + ONNX vocoder | libonnxruntime.so |
~5.5 tok/s |
onnx-cuda |
ONNX CUDA EP for vocoder (+ predictor ONNX fallback path) | onnxruntime-gpu + CUDA/cuDNN |
GPU-dependent |
ggml-backend |
llama.cpp talker + GGML predictor | llama_wrapper.so + static GGML libs |
Fastest on tuned ARM |
ggml-predictor |
GGML predictor only (talker stays Candle) | static GGML libs | Predictor-only acceleration |
rknn-vocoder |
RKNN INT8 vocoder (Rockchip NPU) | librknnrt.so + RKNPU kernel |
Default uses Candle (pure Rust) inference — no C/C++ library installation needed. The Candle backend includes SDOT inline assembly optimization and benefits greatly from stripped GGUF models. RKNN vocoder trades audio quality for speed — INT8 quantization introduces audible artifacts. Predictor does not require ONNX and GGUF at the same time: it prefers GGUF (GGML/Candle), and only falls back to ONNX when GGUF is missing.
ggml-predictor / ggml-backend require native C/C++ static libraries.
build.rs now auto-initializes and auto-builds them from third_party/qwen3-tts.cpp (submodule), so cargo install --git can build GGML mode directly.
Requirements: git, cmake, C++ toolchain (c++), and ar (binutils).
- Recommended: one-command install (auto submodule + auto native build):
# Predictor only (talker stays Candle)
cargo install --git "https://github.com/darkautism/qwen3-tts" \
--locked qwen3-tts-rs \
--features ggml-predictor
# Talker + predictor on GGML
cargo install --git "https://github.com/darkautism/qwen3-tts" \
--locked qwen3-tts-rs \
--features ggml-backendbuild.rs will generate/check these artifacts automatically:
$GGML_LIB_DIR/build/libcode_pred_ggml.a
$GGML_LIB_DIR/build/libtts_transformer.a
$GGML_LIB_DIR/ggml/build/src/libggml.a
$GGML_LIB_DIR/ggml/build/src/libggml-cpu.a
$GGML_LIB_DIR/ggml/build/src/libggml-base.a
- Optional override: use your own
qwen3-tts.cppsource tree:
export GGML_LIB_DIR="$HOME/qwen3_research/qwen3-tts.cpp"
git clone --recursive https://github.com/predict-woo/qwen3-tts.cpp "$GGML_LIB_DIR"
# build.rs will use GGML_LIB_DIR and build missing native libs
cargo build --release --features ggml-predictor- (Only for
ggml-backend) Buildllama_wrapper.sofromllama.cppsource:
git clone https://github.com/ggml-org/llama.cpp "$HOME/llama.cpp"
cmake -S "$HOME/llama.cpp" -B "$HOME/llama.cpp/build" -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build "$HOME/llama.cpp/build" -j
cd /path/to/qwen3-tts
gcc -shared -fPIC -O3 scripts/llama_wrapper.c \
-I"$HOME/llama.cpp/include" \
-I"$HOME/llama.cpp/ggml/include" \
-L"$HOME/llama.cpp/build/bin" \
-lllama -lggml -lggml-base -lggml-cpu \
-Wl,-rpath,"$HOME/llama.cpp/build/bin" \
-o llama_wrapper.so
sudo install -m 0755 llama_wrapper.so /usr/lib/llama_wrapper.so- Runtime check:
qwen3-tts worker -r predictor -b 0.0.0.0:9091 --quant q4
# Expect log: "Using GGML code predictor (optimized C backend)"# Generate ~/.config/qwen3-tts/config.toml with your worker IPs
qwen3-tts init --talker-ip <IP1> --predictor-ip <IP2> --vocoder-ip <IP2># IP1 - Talker Worker (big-core pinning is automatic on RK3588)
qwen3-tts worker -r talker -b 0.0.0.0:9090
# IP2 - Predictor Worker (Q4 quantization for max speed)
qwen3-tts worker -r predictor -b 0.0.0.0:9091 --quant q4
# IP2 - Vocoder Worker (can share machine with Predictor)
qwen3-tts worker -r vocoder -b 0.0.0.0:9092qwen3-tts resolves role files directly from HuggingFace Hub cache paths (from
hf-hubrepo.get(...)). It does not mirror/copy role models into${XDG_DATA_HOME:-$HOME/.local/share}/qwen3-tts/modelsby default. Optional offline fallback is only enabled when you explicitly pass--models /path/to/models. Custom HF repo:--repo your-name/your-repoFor specific core pinning:--cores 4-7or--cores 4,5,6,7On big.LITTLE SoCs (RK3588), big-core pinning is enabled by default (no extra flag required).
# Simple usage (outputs output.wav)
qwen3-tts "你好世界"
# Specify output and language
qwen3-tts speak "Hello world" -o speech.wav --lang english
# Voice cloning (custom voice file)
qwen3-tts speak "你好" --voice my_voice.json -o clone.wavDefault chunk mode is
none(no text splitting). Use--chunk 2or--chunk 4only when you explicitly want punctuation-based chunking.
Runs directly on ARM SBCs — no x86 or Python needed:
# Encode reference audio → outputs a single .json voice file (any sample rate, auto-resampled to 24kHz)
qwen3-tts encode-voice \
-a reference.wav \
-r "Text spoken in the reference audio" \
-o my_voice.json
# Use the custom voice
qwen3-tts speak "New text to synthesize" --voice my_voice.json -o output.wavVoice file format (.json):
{
"ref_text": "Text spoken in the reference audio",
"codec_tokens": [[...], ...]
}Voice cloning now defaults to ref_text ICL conditioning when
ref_textis provided in the voice JSON. If you want speaker-only conditioning instead, setQWEN3_TTS_CLONE_MODE=speakeron worker processes. Legacy.npyand.ptfiles are also supported.
Clone stability note:
- For
ref_textICL mode, talker should log
Loaded 15 code predictor codec embeddings ... code_predictor_weights.npz. - If this line is missing, clone intelligibility may regress; check model layout and worker logs before benchmarking.
Voice encoding uses a native Candle (Rust ML) implementation of the Mimi Speech Tokenizer — processes ~4s of audio in ~2s, entirely on CPU.
qwen3-tts init --talker-ip 127.0.0.1 --predictor-ip 127.0.0.1 --vocoder-ip 127.0.0.1# Terminal 1: Talker
qwen3-tts worker -r talker -b 0.0.0.0:9090
# Terminal 2: Predictor
qwen3-tts worker -r predictor -b 0.0.0.0:9091
# Terminal 3: Vocoder
qwen3-tts worker -r vocoder -b 0.0.0.0:9092
# Terminal 4: Synthesize
qwen3-tts "你好世界"qwen3-tts init --talker-ip 127.0.0.1 --predictor-ip <IP2> --vocoder-ip <IP2># IP1:
qwen3-tts worker -r talker -b 0.0.0.0:9090
# IP2:
qwen3-tts worker -r predictor -b 0.0.0.0:9091
qwen3-tts worker -r vocoder -b 0.0.0.0:9092
# IP1:
qwen3-tts "你好世界"qwen3-tts init --talker-ip <IP1> --predictor-ip <IP2> --vocoder-ip <IP3># IP1:
qwen3-tts worker -r talker -b 0.0.0.0:9090
# IP2:
qwen3-tts worker -r predictor -b 0.0.0.0:9091
# IP3:
qwen3-tts worker -r vocoder -b 0.0.0.0:9092
# Any machine (including IP1):
qwen3-tts "你好世界"Any machine with the qwen3-tts binary and correct config can synthesize speech. The client needs no GPU, NPU, or special hardware.
cat > qwen3-tts.toml << EOF
[workers.talker]
host = "<IP1>"
port = 9090
[workers.predictor]
host = "<IP2>"
port = 9091
[workers.vocoder]
host = "<IP2>"
port = 9092
[defaults]
language = "chinese"
max_tokens = 200
temperature = 0.8
cp_temperature = 0.1
repetition_penalty = 1.2
[server]
host = "0.0.0.0"
port = 8080
EOF
qwen3-tts "你好世界"| Command | Description |
|---|---|
qwen3-tts "text" |
Quick speech synthesis (outputs output.wav) |
qwen3-tts speak "text" -o file.wav |
Specify output file |
qwen3-tts speak "text" --lang english |
Specify language |
qwen3-tts speak "text" --voice voice.json |
Voice cloning |
qwen3-tts speak "text" --chunk none |
No text split (default mode) |
qwen3-tts encode-voice -a ref.wav -r "text" -o voice.json |
Create voice profile (native ARM64) |
qwen3-tts serve --port 8080 |
Start OpenAI-compatible API server |
qwen3-tts mcp |
Start MCP server (stdio) |
qwen3-tts worker -r talker |
Start Talker Worker (auto big-core pinning on RK3588) |
qwen3-tts worker -r predictor |
Start Predictor Worker |
qwen3-tts worker -r vocoder |
Start Vocoder Worker |
qwen3-tts worker -r talker --big-cores |
Legacy alias (now default behavior) |
qwen3-tts worker -r talker --cores 4-7 |
Worker pinned to specific cores |
qwen3-tts init --predictor-ip <IP> |
Generate config file |
# Start server (includes built-in web UI at http://localhost:8080)
qwen3-tts serve --port 8080Open http://<server-ip>:8080 in a browser. The built-in web UI provides:
- Text input with Ctrl+Enter to synthesize
- Voice selector dropdown — populated from 500+ voices on HuggingFace, grouped by game/character
- Language selector (Chinese, English, Japanese, Korean)
- Chunk selector (
nonedefault, optional2/4) - Audio playback directly in browser
No installation required — pure HTML/JS, served by the same binary.
# Basic synthesis
curl -X POST http://<IP1>:8080/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"input": "Hello world", "voice": "default"}' \
--output speech.wav
# Voice cloning (voice = path to voice file, supports .json/.npy/.pt)
curl -X POST http://<IP1>:8080/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"input": "Hello world", "voice": "/path/to/my_voice.json"}' \
--output speech.wavSupported parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
input |
string | (required) | Text to synthesize |
voice |
string | "default" |
Voice file path (.json/.npy/.pt) |
language |
string | "chinese" |
Language |
chunk_mode |
string | "none" |
Text chunking mode (none, 2, 4) |
model |
string | "qwen3-tts" |
Model name |
response_format |
string | "wav" |
Output format |
Provides AI tools via stdio JSON-RPC:
qwen3-tts mcp- text_to_speech — Text-to-speech with voice cloning support
{
"mcpServers": {
"qwen3-tts": {
"command": "/path/to/qwen3-tts",
"args": ["mcp"]
}
}
}{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "text_to_speech",
"arguments": {
"text": "Hello, this is a voice cloning test.",
"voice": "/path/to/my_voice.json",
"output_path": "output.wav"
}
}
}Path priority: ./qwen3-tts.toml > ~/.config/qwen3-tts/config.toml
[workers.talker]
host = "127.0.0.1" # Talker Worker IP
port = 9090
[workers.predictor]
host = "<YOUR_IP>" # ⚠️ Change to your Predictor IP
port = 9091
[workers.vocoder]
host = "<YOUR_IP>" # ⚠️ Change to your Vocoder IP
port = 9092
[defaults]
language = "chinese"
max_tokens = 200
temperature = 0.8
cp_temperature = 0.1
repetition_penalty = 1.2
# EOS convergence parameters (tunable, defaults work for most cases)
# eos_start_ratio = 0.6 # Start boosting EOS at 60% of estimated tokens
# eos_max_ratio = 1.2 # Max boost at 120%
# eos_force_ratio = 1.5 # Force stop at 150%
# eos_max_boost = 25.0 # Maximum EOS logit increment
[server]
host = "0.0.0.0"
port = 8080kautism/qwen3-tts-rk3588/
├── talker/ # Talker Worker
│ ├── talker-q8_0.gguf (768 MB, Q8 LLM)
│ ├── tokenizer.json (11 MB)
│ └── embeddings/ (~1.2 GB)
├── predictor/ # Predictor Worker
│ ├── code_predictor/
│ │ ├── code-predictor-q8_0.gguf (206 MB, stripped — default download)
│ │ ├── code-predictor-q4_0.gguf (169 MB, stripped Q4 — use --quant q4)
│ │ └── qwen3-tts-0.6b-q8_0.gguf (1.3 GB, full model — not recommended)
│ └── embeddings/
├── vocoder/ # Vocoder Worker
│ ├── vocoder.onnx (436 MB, FP32 CPU — default)
│ └── vocoder.rknn (128 MB, INT8 NPU — needs rknn-vocoder feature)
└── speech_tokenizer/ # Voice encoding (encode-voice)
└── model.safetensors (651 MB, Mimi encoder)
Workers auto-download their role's models from HuggingFace Hub on first start.
Pass --quant q4 to predictor workers to download/use only the Q4 predictor GGUF (169MB, ~16% faster).
Chinese · English · Deutsch · Русский · Français · 日本語 · 한국어
To achieve the best RTF on ARM SBCs, apply these optimizations (in order of impact):
# Default now auto-pins to performance cores (e.g., A76 on RK3588)
qwen3-tts worker -r predictor -b 0.0.0.0:9091Without big-core pinning, rayon may distribute matmul work to slow efficiency cores → ~43% slower.
# Q4 model is 169MB vs 206MB for Q8. 16% faster prediction, comparable quality.
qwen3-tts worker -r predictor -b 0.0.0.0:9091 --quant q4The Q4 GGUF is auto-downloaded from HuggingFace Hub when --quant q4 is specified.
# Machine 1: talker only (gets all 4 big cores to itself)
qwen3-tts worker -r talker -b 0.0.0.0:9090
# Machine 2: predictor + vocoder (their own 4 big cores)
qwen3-tts worker -r predictor -b 0.0.0.0:9091 --quant q4
qwen3-tts worker -r vocoder -b 0.0.0.0:9092Edit qwen3-tts.toml to point to remote workers:
[workers.talker]
host = "192.168.1.10" # Machine 1
port = 9090
[workers.predictor]
host = "192.168.1.11" # Machine 2
port = 9091
[workers.vocoder]
host = "192.168.1.11" # Machine 2
port = 9092This eliminates core contention and gives ~10% RTF improvement.
# Enable ARM SDOT instruction for faster quantized matmul
RUSTFLAGS='-C target-feature=+dotprod' cargo build --releaseThis is critical on Cortex-A76 and newer cores. Without it, quantized inference uses a slower vmull+vpaddl path.
| What | MEDIUM RTF | Speedup |
|---|---|---|
| Default (no optimization) | 4.96× | baseline |
| + auto big-core pinning + Q4 + SDOT | 2.87× | 42% faster |
| + distribute to 2 machines | 2.61× | 47% faster |
Tested on RK3588 (4×A76 + 4×A55), workers started with auto big-core pinning + --quant q4:
| Test | 1× RK3588 | 2× RK3588 (LAN) | Improvement |
|---|---|---|---|
| SHORT (~40 tokens) | 3.30× | 2.99× | 9% |
| MEDIUM (~175 tokens) | 2.87× | 2.61× | 9% |
| LONG (200 tokens) | 3.31× | 2.99× | 10% |
The second machine offloads predictor + vocoder, freeing all 4 A76 cores on machine 1 for the talker. Single-machine works well for simple deployments; adding a second board reduces RTF by ~10%.
| Metric | 1 Machine | 2 Machines |
|---|---|---|
| Token rate | ~4.9 tok/s | ~5.5 tok/s |
| Talker latency | ~35ms/step | ~35ms/step |
| Predictor latency | ~93ms/step | ~93ms/step |
| Vocoder (ONNX FP32) | ~4.5s/batch | ~4.5s/batch |
| Vocoder (RKNN INT8) | ~2.7s/batch ( |
~2.7s/batch |
| Best MEDIUM RTF | 2.87× | 2.61× |
| External dependencies | libonnxruntime.so only |
same |
RTF = generation time / audio duration. Lower is better; RTF < 1 is real-time.
Important: On big.LITTLE SoCs (e.g., RK3588), big-core pinning is now enabled by default. If you need manual override, use
--cores; otherwise predictor may degrade from 93ms to ~190ms on A55 cores.
| Optimization | Predictor (ms/step) | MEDIUM RTF | Change |
|---|---|---|---|
| Baseline (Candle Q8_0, full 1.3GB GGUF) | 185 | 4.96× | — |
| + Server-side past_tokens + mem::take() | 185 | 4.79× | −3% |
| + Stripped code-predictor GGUF (206MB) | 108 | 3.12× | −37% |
| + CPU affinity (auto big-core pinning) | 108 | 3.08× | −1% |
+ Q4_0 quantization (--quant q4) |
93 | ~2.80× | −3% |
| + Typed wire protocol (ResponseData enum) | 93 | 2.58× | −8% |
Key insight: the full 1.3GB GGUF contains talker weights (1075MB) never used by the predictor. Stripping to a 206MB code-predictor-only GGUF eliminates L2 cache pollution → 41% faster prediction.
The scripts/extract_code_predictor_gguf.py tool creates stripped GGUFs from full models.
Q4 vs Q8: Q4 gives ~16% faster prediction (93ms vs 107ms) with comparable quality. Use
--quant q4on the predictor worker for speed,--quant q8(default) for safety.
If this project has saved you time or helped you in your workflow, consider supporting its continued development.

