How to use from
Docker Model Runner
docker model run hf.co/Luigi/PrimeTTS:F32
Quick Links

PrimeTTS โ€” on-device zh-TW + English TTS

Taiwan-Mandarin + English text-to-speech built for on-device use (contact-centre, GPS, transit): one frontend handles Chinese, English, and code-mix with no language routing, and reads entities correctly โ€” phone numbers, emails, addresses, prices, dates, temperatures, %, serials.

Two models to know:

PrimeTTS v2.1 โ€” flagship PrimeTTS v1 โ€” leanest CPU
Folder v21_mbistft_16k/ v1b_16k/ ยท v1b_8k/
Architecture MB-iSTFT-VITS (end-to-end, multi-speaker) FastSpeech + Snake-HiFiGAN (+ pitch refiner)
Params 37.9M ~5.0M (16 kHz) / 4.09M (8 kHz)
Voices 3 selectable โ€” Xinran โ™€, Anchen โ™‚, Bowen โ™‚ 1 โ€” young โ™€ zh-TW
Sample rate 16 kHz 16 kHz / 8 kHz
Held-out CER 0.059 (zh/mix/en, 3-voice avg) 0.11โ€“0.15 (zh)
Best on Jetson Nano GPU (also any CPU) pure CPU โ€” Nano RTF 0.35 (8 kHz, 1 thread)

Pick v2.1 for multiple voices; pick v1 when the budget is CPU-only and tight.

The full family (all MB-iSTFT-VITS except v1; all 16 kHz; single Xinran voice unless noted):

model folder params (deploy) CER use when
v2 v2_mbistft_16k/ 34.7M (17.5M) 0.027 you want the cleanest single Xinran voice
v2.1 v21_mbistft_16k/ 37.9M (~18M) 0.059 you want a choice of 3 voices
V2 Lite v2lite_mbistft_16k/ 24.8M (17.5M) 0.041 a lighter, still-good single voice for tighter GPU budgets
v1 v1b_16k/,v1b_8k/ ~5M 0.11โ€“0.15 pure-CPU, real-time on a Jetson Nano

(v3_4.6M/ and the top-level *.onnx are legacy 24 kHz variants, kept for provenance.) V2 Lite uses the exact same ONNX I/O + frontend as v2 โ€” it's a drop-in, smaller replacement.

๐Ÿ”Š Live demo: https://huggingface.co/spaces/Luigi/PrimeTTS-vs-Inflect-Nano-v1 โ€” pick a model, pick a voice, type text.


PrimeTTS v2.1 (v21_mbistft_16k/)

End-to-end MB-iSTFT-VITS (VAE + normalizing flow + adversarial multi-band iSTFT head; conv-only, no LSTM) with 3 selectable Taiwan-Mandarin voices, chosen by an integer sid input (0 = Xinran โ™€, 1 = Anchen โ™‚, 2 = Bowen โ™‚). 37.9M generator params, 16 kHz, gin_channels=256 speaker conditioning.

Quality (36 held-out zh / code-mix / en sentences, X-ASR normalized CER):

voice (sid) CER note
Xinran โ™€ (0) 0.059 flagship voice, cleanest teacher
Anchen โ™‚ (1) 0.069 slight accent
Bowen โ™‚ (2) 0.066 slight accent

On-device deployment (measured, Jetson Nano gen-1 / Tegra X1)

Same runtime profile as the single-voice v2 (identical architecture). RTF = compute-time รท audio-time (lower is faster; < 1.0 = real-time).

Tier Runtime Precision RTF Notes
GPU RapidSpeech.cpp ggml-CUDA, 1 CPU thread fp32 0.42 (2.4ร— RT) launch-bound floor on Maxwell (sm_53, no CUDA-graph replay)
CPU (default) onnxruntime, 4 threads fp32 0.52 (1.9ร— RT) full quality, 117 MB
CPU onnxruntime, 2 threads fp32 0.77 (1.3ร— RT) fewer cores, leaves headroom

Both tiers are full-fidelity and need no GPU. On this ARMv8.0 Cortex-A57, fp32 is the fast format: int8 is not a speed lever (static-int8 shifts the voice; dynamic-int8 preserves it but runs slower than fp32 โ€” no dot-product / no FP16 arithmetic on this core), fp16 casts to fp32 (no speedup), and XNNPACK โ‰ˆ MLAS. The only on-device speed lever is a smaller/faster architecture, not quantization.

Files

v21_mbistft_16k/primetts_v21_3voice.onnx        3-voice fp32 (117 MB) โ€” full quality, all runtimes

Quickstart

pip install onnxruntime numpy soundfile g2pw g2p_en cn2an
huggingface-cli download Luigi/PrimeTTS --local-dir PrimeTTS
import sys; sys.path.insert(0, "PrimeTTS/scripts")
import numpy as np, onnxruntime as ort, soundfile as sf
import frontend_bopomofo as F                       # g2pw bopomofo + g2p_en, one pass

sess = ort.InferenceSession("PrimeTTS/v21_mbistft_16k/primetts_v21_3voice.onnx",
                            providers=["CPUExecutionProvider"])
o = F.text_to_ids("ๆ‚จๅฅฝ,ๆญก่ฟŽไฝฟ็”จ PrimeTTSใ€‚Thank you for calling.")
blank = lambda s: np.array([[0] + [v for x in s for v in (x, 0)]], np.int64)   # add_blank=true
sid = 0                                             # 0 Xinran โ™€ ยท 1 Anchen โ™‚ ยท 2 Bowen โ™‚
wav = sess.run(None, {
    "x": blank(o["phone_ids"]), "tone": blank(o["tone_ids"]), "lang": blank(o["lang_ids"]),
    "x_lengths": np.array([2*len(o["phone_ids"])+1], np.int64),
    "sid": np.array([sid], np.int64),
    "noise_scale": np.array([0.667], np.float32),
    "length_scale": np.array([1.0], np.float32)})[0].reshape(-1)
sf.write("out.wav", wav, 16000)

PrimeTTS v1 (v1b_16k/, v1b_8k/) โ€” tiny, CPU-only

FastSpeech-style acoustic (no attention: depthwise gated Conv-FFN + external durations + length regulator

  • BiGRU + postnet) with a 97K-param frame-pitch refiner that turns per-phoneme pitch into the per-frame F0 contour = Mandarin tones (ablating it costs +18% relative zh-CER), and a Snake-HiFiGAN vocoder. Torch-free ONNX; runs real-time on a Jetson Nano CPU. One young-female zh-TW voice across zh / en / code-mix.
flagship v1b_16k/ leanest v1b_8k/
Params ~5.0M (3.56M acoustic + 1.43M vocoder) 4.09M (+ 0.53M vocoder)
Sample rate 16 kHz (0โ€“8 kHz band) 8 kHz (telephone band)
Jetson Nano RTF (heavier) 0.35 (1 thread)
.gguf (ggml) โ€” inflect_combined_v1b.gguf

Pipeline is encoder โ†’ numpy length-regulator โ†’ decoder โ†’ vocoder:

import sys; sys.path.insert(0, "PrimeTTS/scripts")
import json, numpy as np, onnxruntime as ort, soundfile as sf
import frontend_bopomofo as F
from synth_from_text import host_regulate

D = "PrimeTTS/v1b_16k"                               # or v1b_8k for the leanest Nano RTF
meta = json.load(open(f"{D}/meta.json"))
enc = ort.InferenceSession(f"{D}/acoustic_encoder.onnx", providers=["CPUExecutionProvider"])
dec = ort.InferenceSession(f"{D}/acoustic_decoder.onnx", providers=["CPUExecutionProvider"])
voc = ort.InferenceSession(f"{D}/vocoder.onnx",          providers=["CPUExecutionProvider"])
o = F.text_to_ids("ๆ‚จๅฅฝ,ๆญก่ฟŽไฝฟ็”จ PrimeTTSใ€‚Thank you for calling.")
ph, tn, lg = (np.array([o[k]], np.int64) for k in ("phone_ids", "tone_ids", "lang_ids"))
cond, dur, pitch = enc.run(None, {"phone": ph, "tone": tn, "lang": lg, "speaker": np.zeros(1, np.int64)})
reg = host_regulate(cond, dur, pitch, meta["abs_frame_bins"], meta["max_frames"])
mel = dec.run(None, {k: reg[k] for k in ["frames","frame_meta","local_ctx_raw","abs_pos","pitch_frame","frame_mask"]})[0]
wav = voc.run(None, {"mel": mel.astype(np.float32)})[0].reshape(-1)
sf.write("out.wav", wav, meta["sample_rate"])

scripts/synth_long.py adds punctuation auto-chunking for long text.


Shared frontend

g2pw (Taiwan bopomofo + polyphone disambiguation) + g2p_en (arpabet) merge into one phone sequence with per-phone language ids โ€” zh / en / code-mix in a single pass, 88-symbol table. Entity normalization (scripts/text_norm.py) reads numbers / dates / prices / emails / addresses / serials and spells acronyms (VIP โ†’ V-I-P), applied identically in training and inference. Both model families consume the same frontend_bopomofo.text_to_ids() output (phone / tone / lang ids).


Reproduce from this repo

Everything needed to rebuild both models is here: the frontend, entity normalizer, aligner, corpus-gen and text-selection scripts, the eval sets + scorer, the export scripts, and the v1 trainer.

scripts/    frontend_bopomofo.py ยท text_norm.py ยท align_durations_v4.py ยท build_corpus_v3.py
            gen_codemix*.py ยท gen_entity_texts.py ยท select_diverse_text.py ยท asr_filter.py
            synth_from_text.py ยท synth_long.py ยท export_8k.py ยท export_onnx_primetts_v21.py
            xasr_offline.py ยท assess_big.py ยท rebuild_voice.sh ยท symbol_table.json
data/       codemix_v2.txt ยท entity_texts.jsonl ยท voxcpm_texts.jsonl   (corpus text sources)
eval_big.jsonl ยท eval_entity.jsonl                                     (held-out eval sets)
inflect_nano/  the v1 trainer (acoustic.py + vocoder.py), forked from Inflect-Nano-v1
configs/    zhtw_mb_istft_16k_v21b.json                                (v2.1 3-voice training config)

Common recipe (both models): teacher corpus โ†’ ASR/CER gate โ†’ phone-level align โ†’ train โ†’ export. The three levers that matter for a tiny model: phone-level alignment (espeak phoneme-CTC + torchaudio.forced_align โ€” sub-syllable boundaries separate speech from fluent babble), broad coverage + diverse code-mix, and the teacher (a student's language is only as good as its teacher's).

v1 (inflect_nano/ trainer, all in-repo):

  1. Generate corpus text โ€” scripts/gen_codemix_v2.py, gen_entity_texts.py, select_diverse_text.py.
  2. Synthesize with the teacher (VoxCPM2 cloning a CC0 zh-TW reference), gate with asr_filter.py.
  3. Align โ€” scripts/align_durations_v4.py. Train acoustic + vocoder (inflect_nano/). Export โ€” scripts/export_8k.py.
  4. One-shot: scripts/rebuild_voice.sh (swap in your own ~10 s reference clip).

v2.1 (MB-iSTFT-VITS; trainer is the upstream repo โ€” see credits):

  1. Synthesize the corpus with a VibeVoice-Large teacher across the 3 zh-capable voices.
  2. CER-gate the teacher audio (X-ASR normalized CER < 0.05) โ€” not voice-similarity โ€” so only intelligible clips train the model. (This is the single most important QC step; ungated multi-voice teacher audio is the main failure mode.)
  3. Train the 3-speaker MB-iSTFT-VITS (configs/zhtw_mb_istft_16k_v21b.json, n_speakers=3, gin_channels=256), warm-started from the single-voice v2 with fresh speaker-conditioning layers.
  4. Export to ONNX โ€” scripts/export_onnx_primetts_v21.py (opset 17, dynamo=False; the tiny gen-head iSTFT n_fft=16, hop=4 is replaced by an exact irFFT + overlap-add matrix, verified vs torch.istft).
  5. Score โ€” scripts/xasr_offline.py + assess_big.py on eval_big.jsonl.

Findings & lessons (what building tiny on-device zh/en TTS actually taught us)

Transferable lessons from taking this from a babbling 5M model to a shippable family. Full analysis in docs/zh-en-tts-arch-survey-2026.md and docs/streaming-arch-design.md.

  • A tiny model's quality is bounded by its inputs, not its parameter count. Held-out Mandarin CER fell 0.88 โ†’ 0.06 at a fixed ~5M purely from phone-level forced alignment + broad character coverage โ€” no architecture change. Sub-syllable (not character) boundaries are the difference between intelligible speech and fluent babble. Gate on resynth CER, not on how balanced the duration histogram looks.
  • CER-gate the teacher audio, never voice-similarity alone. Our first multi-speaker attempt trained on teacher clips filtered only for the right voice; four of the "voices" were speakers that can't actually pronounce Mandarin (teacher CER 0.45โ€“0.79), and the student faithfully learned garbled speech. Filtering on intelligibility (teacher X-ASR CER < 0.05) fixed it.
  • Deterministic (FastSpeech-class) models mean-regress prosody; distributional (VITS/flow) models don't. This is the wall that caps a tiny deterministic model at "intelligible but flat" โ€” and why the flagship is a VITS, not a bigger FastSpeech.
  • On a launch-bound GPU (Maxwell sm_53, no CUDA-graph replay), RTF is set by kernel count, not FLOPs. A smaller VITS is a smaller download but not faster (~0.42 RTF floor regardless of params). The lever for speed is an architecture with fewer, larger kernels (flow-matching + Vocos measured ~0.18) โ€” a different axis from size.
  • On an ARMv8.0 CPU (Cortex-A57): fp32 is the fast format. No int8 dot-product and no fp16 arithmetic, so int8 either breaks the voice (static) or runs slower than fp32 (dynamic), fp16 casts to fp32, and XNNPACK โ‰ˆ MLAS. The only CPU speed lever is a smaller/faster architecture โ€” quantization is a download-size option.
  • "Lighter" and "faster" are different goals. VITS deploy size is dominated by flow + decoder + encoder, which don't shrink with hidden_channels; below ~17M deploy, quality craters. V2 Lite (17.5M) is the practical quality floor for this arch โ€” there is no free "smaller and still good."

Credits & licenses

  • v2.1 architecture: MB-iSTFT-VITS (Kawamura et al., Apache-2.0) ยท Jetson-Nano ggml-CUDA runtime: RapidSpeech.cpp (mbistft-vits arch)
  • v2.1 teacher: VibeVoice-Large (Microsoft, MIT), 3 zh-capable presets (via the MIT community repo) โ€” synthesized / AI-generated voices; mark as such in products
  • v1 base / trainer: owensong/Inflect-Nano-v1 (Apache-2.0) ยท v1 teacher: openbmb/VoxCPM2 ยท v1 reference voice: Mozilla Common Voice zh-TW (CC0 / public domain)
  • Gate ASR: Breeze-ASR-25 (MediaTek Research) ยท Whisper ยท Aligner: facebook/wav2vec2-lv-60-espeak-cv-ft + torchaudio.forced_align ยท Eval: sherpa-onnx X-ASR

This repository: Apache-2.0.

Downloads last month
63
GGUF
Model size
0.3B params
Architecture
gemma3
Hardware compatibility
Log In to add your hardware

32-bit

Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support

Model tree for Luigi/PrimeTTS

Finetuned
(1)
this model

Spaces using Luigi/PrimeTTS 3