Downloads · 30 days
98
26% of all-time downloads
RumiLabs/Qwen3-TTS-12Hz-1.7B-CustomVoice-MLX-4bit
Qwen3-TTS-12Hz-1.7B-CustomVoice-MLX-4bit is a text-to-speech model from RumiLabs. Use it when you need text read aloud. It is set up for mlx. The card lists the license as apache-2.0.
Private RumiLabs MLX build of Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice for on-device Apple Silicon inference. This is the shipping target for the Yobi/rumi TTS pipeline, superseding RumiLabs/Qwen3-TTS-12Hz-1.7B-VoiceDesig…
Downloads · 30 days
98
26% of all-time downloads
All-time downloads
370
Public
Parameters
1.9B
2 GB on disk
Likes
1
Public
Click a slice to open those files.
.safetensors2 GB · 100%
How the weights are stored.
U321.5B · 80%
From the Hugging Face model README
Private RumiLabs MLX build of Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice for on-device Apple Silicon inference. This is the shipping target for the Yobi/rumi TTS pipeline, superseding RumiLabs/Qwen3-TTS-12Hz-1.7B-VoiceDesign-MLX-4bit.
VoiceDesign treats instruct as a whole-voice description (identity + emotion mixed). Two different emotion prompts produce two different speakers — which we verified empirically: 5 emotion templates → 4 different female voices + 1 male, none anchored to a reference. The --voice parameter is a silent no-op on VoiceDesign and --ref_audio cloning is too weak to anchor identity.
CustomVoice decouples them: voice = preset speaker (9 timbres), instruct = emotion/style only. Listening A/B confirmed: same speaker (Aiden) across all 5 emotions, quality matches VoiceDesign, emotions clearly distinct. One-line code change, no training.
See qwen3_tts.py:1071-1078 for the variant API differences.
int4 group-64 affine via mlx-audio convert.speech_tokenizer/ (RVQ codec + Code2Wav): cast from FP32 → FP16 (682 MB → 341 MB). FP16 retains full audible fidelity (40–61 dB SNR on 5-emotion A/B).| Component | Upstream BF16 1 | mlx-community/-8bit 2 | This bundle |
|---|---|---|---|
Qwen3 LLM (model.safetensors) | 3.57 GiB (BF16) | ~1.9 GiB (INT8) | 1.51 GiB (INT4-g64) |
speech_tokenizer/ — RVQ codec + Code2Wav | 651 MiB (FP32) | 651 MiB (FP32) | 325 MiB (FP16) |
Text tokenizer (vocab.json + merges.txt + configs) | 4.25 MiB | 4.25 MiB | 4.25 MiB |
| Total | ~4.21 GiB | ~2.55 GiB | 1.80 GiB |
(SI-decimal totals — what HF Hub displays: 4.52 GB / 2.74 GB / 1.93 GB.)
# 1. LLM: bf16 -> int4 g64
python -m mlx_audio.convert \
--hf-path Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--mlx-path ./bundle \
-q --q-bits 4 --q-group-size 64 --model-domain tts
# 2. Vocoder: fp32 -> fp16 in place
python -c "
from safetensors.numpy import load_file, save_file
import numpy as np
p = 'bundle/speech_tokenizer/model.safetensors'
sd = load_file(p)
save_file({k: v.astype(np.float16) if v.dtype == np.float32 else v for k,v in sd.items()}, p)
"
from mlx_audio.tts.generate import generate_audio
generate_audio(
model="RumiLabs/Qwen3-TTS-12Hz-1.7B-CustomVoice-MLX-4bit",
text="I just heard the most amazing news.",
voice="Aiden", # English male preset
instruct="speaking with bright, lively energy at a faster pace.",
temperature=0.3, # critical — temp=0 truncates 2/5 emotions
output_path=".", file_prefix="out", audio_format="wav",
)
English speakers: Aiden (Sunny American male, clear midrange) — current default. Ryan (Dynamic male, strong rhythmic drive) — alternative.
temperature matters. At temperature=0.0 (greedy), the LLM hits EOS prematurely on some emotion templates, producing audio truncated to "I just..." with seconds of silence padding. temperature=0.3 resolves this without quality cost.
We attempted further INT4-g64 quantization of the non-LLM weights (the 53% of model.safetensors left at BF16 by mlx-audio's model_quant_predicate). All failed:
| Optimization | Predicted disk savings | Actual result |
|---|---|---|
Quantize talker.model.text_embedding (622 MB → 78 MB) | ~544 MB | Audio truncates to ~250 ms on 3/5 emotions |
Quantize 15 × talker.code_predictor.model.codec_embedding.* | ~110 MB | Same failure mode |
Drop speech_tokenizer.encoder.* (113 MB, allegedly only used for ref_audio clone) | ~113 MB | Total silence — encoder feeds the preset-speaker path too |
The mlx-audio model_quant_predicate skip list in qwen3_tts.py:241-249 is load-bearing, not conservative defaults. Whoever wrote that predicate already ran this experiment. The 1.80 GiB / 5.24 GiB-peak bundle is at the floor for this codepath.
Remaining real levers: streaming Code2Wav inference (multi-day engineering, 500 MB–1 GB peak RAM savings, no disk impact), or vocoder retrain at lower precision (multi-week).
The complete stack in this repo (1.7B talker, 16-codebook code predictor, FP16
Code2Wav vocoder) is ported to MLX-Swift and validated stage-by-stage against the
Python-MLX reference (mlx_audio qwen3_tts):
Sources: swift/tts/
in the rumi-cerberus release repo (TTSTalker / TTSSession / TTSVocoder), mirrored
below under swift/ for self-containment.
| stage | measured |
|---|---|
| rumi-cerberus teardown | 0.12 s (2.27 GB → 55 MB) |
| TTS load (fully materialized) | 1.17 s (→ 1.93 GB) |
| synthesis (~5 s utterances, 3 emotion instructs) | RTF 0.80–0.86× (debug build) |
| TTS teardown / rumi-cerberus restore | 0.04 s / 3.50 s |
| peak footprint, entire swap–synth–restore flow | 2.27 GB |
All on-device-synthesized utterances transcribe exactly (whisper-large-v3-turbo).
Deployment note: decode the vocoder in chunks (15 frames + 5 context — see
TTSVocoder.decodeChunked); the single-shot graph's transient working set is
jetsam-fatal on iOS even though steady-state memory fits.
Sampling: voice="Aiden", temp=0.3 (temp 0 occasionally truncates on sad-style
instructs), top_k 50, repetition_penalty 1.05.
The production loop now streams: codec frames vocode in 15-frame chunks onto an
AVAudioPlayerNode while the talker keeps generating, and the LLM restores
during the playback tail. Measured on the same device:
| metric | whole-reply (above) | streaming |
|---|---|---|
| first audio after reply-ready (~8 s utterance) | ≈ 9.3 s | ≈ 4.1–4.4 s (0.07 s swap + 1.19 s load + 2.8–3.0 s synthesis lead-in) |
| next-turn readiness | +3.5 s restore | warm (restore hidden in playback) |
Generation is slower than real-time (audio/compute RTF 0.73–0.87, thermal-dependent),
so the player is fed through a closed-loop prebuffer (EMA of measured RTF ×
token-estimated reply length); if a very long reply outruns the buffer, playback
takes a single pause-and-rebuffer (node-clock detected) rather than accumulating
stutters. Hook: generateSampled(..., onFrame:) in swift/TTSSession.swift.
Release and debug builds measure identically (decode 17.1–17.3 tok/s, RTF
0.78–0.84 both) — the compute is in Metal library kernels.
Apache-2.0, inherited from Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice.