Luigi commited on
Commit
86a9219
·
verified ·
1 Parent(s): 075a6fd

Model card: rewrite for current on-device v1b (8k, 4.09M, snake_8k_lite) + vocoder-family/param table; synthesized + shortened (424->155 lines)

Browse files
Files changed (1) hide show
  1. README.md +127 -371
README.md CHANGED
@@ -18,407 +18,163 @@ library_name: onnxruntime
18
  pipeline_tag: text-to-speech
19
  ---
20
 
21
- # PrimeTTS — tiny bilingual zhTW + English TTS (24 kHz, CPU)
22
 
23
- A **4.63M‑parameter** Mandarin (Taiwan) + English texttospeech model that runs **entirely on CPU**
24
- and emits **24 kHz** audio — sized for **on‑device (Jetsonclass)** and contactcentre / GPS / transit
25
- use. One model, **one youngfemale voice**: Chinese, English, and codemix through a single frontend
26
- (no language routing). Built for **entity correctness** — phone numbers, emails, addresses, prices,
27
- dates, temperatures, percentages, serial numbers, and a broad bank of Taiwan/world named entities.
28
 
29
- > 🔊 **Live demo:** https://huggingface.co/spaces/Luigi/PrimeTTS-vs-Inflect-Nano-v1 · 🧩 **Base:** [`owensong/Inflect-Nano-v1`](https://huggingface.co/owensong/Inflect-Nano-v1) (warm‑started fine‑tune, **same frozen architecture**)
30
 
31
  | | |
32
  |---|---|
33
- | **Parameters** | 6.85M (5.43M acoustic + 1.17M vocoder) *v4 default; the 4.63M v3 remains in `checkpoints/`* |
34
- | **Sample rate** | 24 kHz |
35
- | **Runtime** | `onnxruntime`, **CPUonly**, torchfree at inference |
36
- | **Languages** | zhTW (Traditional) + English + codemix, single voice |
37
- | **Voice** | young female, **TaiwanMandarin accent** |
38
- | **Architecture** | FastSpeech‑style (no attention) + Snake‑HiFiGAN **frozen**, no NAS |
39
- | **License** | Apache2.0 |
40
-
41
- ### Held‑out quality (eval_big, 36 unseen phone‑attendant sentences)
42
-
43
- | metric | **v4default (6.85M)** | v3 (4.63M, `v3_4.6M/`) |
44
- |---|---|---|
45
- | zh‑CER overall (Breeze‑ASR‑25) | 0.108 | **0.087** |
46
- |   · pure‑zh | **0.083** | 0.087 |
47
- |   · code‑mix | 0.134 | **0.092** |
48
- | en‑WER (Whisper) | 0.083 | 0.083 |
49
- | SQUIM PESQ | 3.11 | 3.15 |
50
- | SQUIM STOI | 0.968 | 0.969 |
51
- | SQUIM MOS | 4.41 | 4.42 |
52
-
53
- **Why v4 is the default despite the higher ASR CER:** v4 was chosen by ear for **clearer, more
54
- naturally‑timed** speech. It **matches v3 on pure Mandarin** (zh‑CER 0.083 ≈ 0.087); the overall gap is
55
- **code‑mixed zh+en** (0.134 vs 0.092), where v3's English‑base warm‑start still helps and v4 (trained from
56
- scratch) hasn't caught up. Same CPU real‑time envelope as v3 — **RTF 0.04 on 2 cores (≈25× real‑time)**
57
- on a desktop; **0.75 (1.3× real‑time) on 2 cores of a Jetson Nano**. If code‑mix accuracy matters more than
58
- the perceptual gain for your deployment, **`v3_4.6M/` is the better choice** and is kept in‑repo. (Both far
59
- exceed the original 8 kHz release: zh‑CER 0.090, code‑mix 0.178, MOS 4.24.)
60
-
61
- ¹ `CER(generic ASR) − CER(Taiwan‑tuned Breeze‑ASR‑25)` per zh clip; `>0` ⇒ a Taiwan‑tuned recognizer
62
- understands it better ⇒ genuine Taiwan accent present.
63
-
64
- > **v3 pronunciation fix:** corrected the forced-aligner's bopomofo→IPA map (ㄜ/ㄟ/ㄡ were mapped to `ɤ`/`ei`/`ou`, absent from the aligner vocab, so those vowels were starved to ~2 frames and dropped) and added the syllabic-vowel symbol **ㄭ** for empty-rime syllables (是/十/日/司/資…, previously rendered as a bare consonant). Both classes — 額/給/走 and 是/司 — now render correctly; zh‑CER 0.106→**0.087**. 88 phone symbols.
65
 
 
 
66
 
67
- ---
 
 
68
 
69
- ## Pronunciation diagnostic — two-ASR cross-check (entity sentence)
70
-
71
- The entity-dense diagnostic sentence was synthesized by the **teacher**, **PrimeTTS v1 (4.63M)** and
72
- **PrimeTTS v2 (6.85M)**, then transcribed by two recognizers that treat **empty-rime syllables** (a bare
73
- retroflex/dental sibilant + the syllabic vowel ㄭ — 十/日/之…) very differently:
74
- - **Breeze-ASR-25** — Taiwan-tuned, robust; tends to **over-read** short empty-rimes.
75
- - **X-ASR** — a fine-tuned zh-TW/en streaming zipformer2 transducer; **stricter** on those syllables.
76
-
77
- *(All evidence below is ASR transcripts only — no listening judgment is implied.)*
78
-
79
- > Diagnostic sentence: `Anderson 先生您好,您 2024年3月15日 訂的 3 件商品總共 NT$1,299,序號 AB1234CD,…,降雨機率 70%,謝謝。`
80
-
81
- | token | Breeze-ASR-25 (robust) | X-ASR (strict) |
82
- |---|---|---|
83
- | **3月15日** — teacher | `3 月 15 日` ✓ | date span lost (`…二零二四年 YING 总共…`) |
84
- | **3月15日** — v1 4.63M | `3 月 15 日` ✓ | `三月十五日` ✓ |
85
- | **3月15日** — v2 6.85M | `3 月 15 日` ✓ | `Y 号` — lost |
86
- | **序號 AB1234CD** | teacher `AB1234CD` ✓ · v1 `1B1234City` · v2 `PB1234CD` | teacher `A B 1 2 3 4 C D` ✓ · v1 serial dropped · v2 `DB … C D` |
87
- | **降雨機率 70%** | numeral `70%` for all three | `百分之七` (teacher) · `百分之` (v1, v2) — 十 blurred |
88
- | **松高路11號5樓櫃台** | ✓ all three | v2 `松高路十一号五楼柜台` ✓ · v1 dropped · teacher garbled (`LL ZU Y 号`) |
89
-
90
- **Findings**
91
- 1. **Empty-rime 日 (3月15日).** Robust Breeze renders the date for all three (it over-reads the syllabic ㄭ).
92
- The stricter X-ASR yields it **only for v1 (4.63M)**; **v2 (6.85M)** and the **teacher** both lose it. So the
93
- syllable is fragile across the board and the two ASRs disagree — v2 shows **no X-ASR gain on 日, a soft
94
- regression vs v1, but not vs the teacher**. *Caveat:* v1's X-ASR transcript is otherwise the **most degraded**
95
- of the three (it also drops the serial, the address tail, and 七十), and `三月十五日` is a high-frequency date
96
- the strict ASR may be pattern-completing — so treat v1's 日 as a **soft win**, not proof of an acoustic edge.
97
- 2. **Serial `AB1234CD`.** Only the **teacher** is clean on both ASRs. Both students mis-render the **leading
98
- letter "A"**; even with the frontend emitting the correct letter name, a 4–7M acoustic renders an isolated
99
- spelled letter weakly. (The frontend letter-name fix lands the right *phoneme*; the *acoustic* is the limit.)
100
- 3. **降雨機率 70% (百分之七十).** **X-ASR** blurs the final 十 **even on the teacher** (`百分之七`); **Breeze
101
- sidesteps it** by emitting the numeral `70%` for all three. A hard empty-rime (十 = ㄕㄭ), present on the
102
- teacher and both students — **not a v2-specific regression** (under X-ASR v2 drops both 七 and 十, slightly
103
- worse than the teacher here, consistent with a hard syllable).
104
- 4. **Address tail (松高路11號5樓櫃台).** v2 renders it cleanly (X-ASR ✓); v1 drops it and the teacher is itself
105
- garbled there — so **v2 beats both v1 and the teacher on the long tail**.
106
-
107
- **Takeaway.** The two ASRs disagree precisely on the empty-rime syllables, and that disagreement *is* the
108
- method: a robust ASR (Breeze) over-reads them, so its CER **understates** empty-rime fragility; a strict ASR
109
- (X-ASR) exposes it. Net, **v2 (6.85M) trades a clearer, more complete long tail** (address/temperature — beating
110
- both v1 *and* the teacher) **for no gain on the empty-rime 日/十 family and a soft regression vs v1 on 日.**
111
- Catching this required cross-checking a robust against a strict ASR — a single CER number hides it.
112
 
113
- ---
 
 
114
 
115
- ## 3-pass phonemic diagnostic — teacher vs v1 (4.63M) vs v2 (6.85M)
116
-
117
- One utterance can't cover every phoneme inside the model's ~1400-frame input window, so the diagnostic is
118
- **3 natural-prose passes**, each ≤1 window, that together exercise the full inventory + the entity normalizer.
119
- Coverage was verified with the frontend; the strings are fluent (not pangram-gibberish), so a mis-render
120
- reflects the *model*, not out-of-distribution text.
121
-
122
- | Pass | Coverage | Utterance |
123
- |---|---|---|
124
- | **1 · zh-TW** | all 37 bopomofo + ㄭ = **38/38** | 小明知道今天是好日子,他喝了四次熱湯。女兒給媽媽買肥皂、青菜、綠茶和八顆雞蛋,婆婆很歡喜。二月的雨不停,風很涼,我們走回家。 |
125
- | **2 · English** | all 39 arpabet = **39/39** | On a beige autumn morning, she shyly measured both choices with joy and pleasure; he laughed, thought it through, and quickly chose the rough path. Good vision, you know, can bring change now. |
126
- | **3 · entities** | date · price (NT$) · % · °C · phone · alphanumeric serial · ordinal | 訂單 AB1234CD,三月十五日,NT$299,折扣 70%,氣溫 28 度,請撥 0918,第3名,謝謝。 |
127
-
128
- Each was synthesized by the **teacher** (VoxCPM2), **v1 (4.63M)** and **v2 (6.85M)**, then transcribed by
129
- **Breeze-ASR-25** (robust) and **X-ASR** (a stricter zh-TW/en zipformer2). Raw results below — **bold = error**,
130
- ✓ = clean — so you can judge for yourself.
131
-
132
- **Pass 1 (zh)** — ref: 小明知道今天是好日子,…,二月的雨不停,風很涼,我們走回家。
133
-
134
- | model | Breeze (robust) | X-ASR (strict) |
135
- |---|---|---|
136
- | teacher | ✓ | ✓ |
137
- | v1 4.63M | ✓ | ✓ |
138
- | v2 6.85M | …**與兒**給媽媽…(女兒→與兒) | …八顆雞蛋**QUIP'N'月**的雨…風很**亮**…回家**家**(婆婆很歡喜二月→Latin bleed) |
139
-
140
- **Pass 2 (English)** — ref: …she shyly measured both choices…he laughed…chose the rough path. Good vision, you know, can bring change now.
141
-
142
- | model | Breeze | X-ASR |
143
- |---|---|---|
144
- | teacher | ✓ | drops "**morning**" |
145
- | v1 4.63M | ✓ | she shyly→**XI SHAILY**, laughed→**LOVED** |
146
- | v2 6.85M | ✓ | Good→**QUOTE** |
147
-
148
- **Pass 3 (entities)** — ref: 訂單 AB1234CD,三月十五日,NT$299,折扣 70%,氣溫 28 度,請撥 0918,第3名。
149
-
150
- | model | Breeze | X-ASR |
151
- |---|---|---|
152
- | teacher | 訂單→**DingDang**, else ✓ (AB1234CD ✓) | serial ✓ but tail **garbled** |
153
- | v1 4.63M | serial→**AB1234C** (drops D), else ✓ | A→**EIGHT**, 三月→**先月** |
154
- | v2 6.85M | AB1234CD ✓, 70%→**77%**, else ✓ | A→**AV**(B→V), 三月→**现月** |
155
-
156
- **Diagnosis**
157
- 1. **Dense zh (P1): v1 ≥ v2.** teacher and v1 are clean on both ASRs; **v2 still degrades on a dense zh run**
158
- (婆婆很歡喜二月 → Latin "QUIP'N'" under X-ASR, 女兒→與兒 under Breeze). This persists on *natural* text, so
159
- it is a real v2 weakness, not OOD noise — the larger, more code-mix-exposed model over-triggers English on
160
- dense zh.
161
- 2. **English (P2): on natural prose all three are good.** v2 and the teacher are clean on Breeze; X-ASR shows
162
- only minor slips (v1 "she shyly"→"XI SHAILY"/laughed→loved; v2 Good→"QUOTE"). **This corrects an earlier
163
- pangram result** that made the students look far worse than the teacher — that gap was largely an
164
- out-of-distribution artifact of dense, unnatural English.
165
- 3. **Entities (P3): the serial letter "A" is the hard case.** Under robust Breeze, teacher and **v2 render the
166
- full serial AB1234CD correctly** (v1 drops a digit); v2's only number error is 70%→77%. Under the strict
167
- X-ASR the leading **A** garbles for all (→EIGHT/AV) and even the teacher's tail breaks up — i.e. an isolated
168
- spelled letter is near the capacity limit for a 4–7M model. The text-norm expansions themselves are correct;
169
- the failures are acoustic.
170
-
171
- **Net:** naturalizing the test removed an OOD penalty on English (P2) — students are close to the teacher
172
- there. The weaknesses that survive natural text are **v2's dense-zh Latin-bleed** and the **serial letter "A"**;
173
- on entity-dense, long-form text v2 (6.85M) is otherwise the strongest student.
174
 
175
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
- ## Quickstart (inference, CPU)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  ```bash
180
  pip install onnxruntime numpy soundfile g2pw g2p_en cn2an
181
  huggingface-cli download Luigi/PrimeTTS --local-dir PrimeTTS
182
  ```
183
  ```python
184
- # from inside the PrimeTTS dir (uses the bundled frontend + scripts)
185
- import sys; sys.path.insert(0, "scripts")
186
  import json, numpy as np, onnxruntime as ort, soundfile as sf
187
  import frontend_bopomofo as F
188
- from synth_from_text import host_regulate # numpy lengthregulator
189
 
190
- meta = json.load(open("meta.json"))
191
- enc = ort.InferenceSession("acoustic_encoder.onnx", providers=["CPUExecutionProvider"])
192
- dec = ort.InferenceSession("acoustic_decoder.onnx", providers=["CPUExecutionProvider"])
193
- voc = ort.InferenceSession("vocoder.onnx", providers=["CPUExecutionProvider"])
 
194
 
195
- o = F.text_to_ids("您好,歡迎使用 PrimeTTS。Thank you for calling.") # text -> phone/tone/lang ids
196
  ph, tn, lg = (np.array([o[k]], np.int64) for k in ("phone_ids", "tone_ids", "lang_ids"))
197
- cond, dur, pitch = enc.run(None, {"phone": ph, "tone": tn,
198
- "lang": lg, "speaker": np.zeros(1, np.int64)})
199
  reg = host_regulate(cond, dur, pitch, meta["abs_frame_bins"], meta["max_frames"])
200
  mel = dec.run(None, {k: reg[k] for k in
201
  ["frames","frame_meta","local_ctx_raw","abs_pos","pitch_frame","frame_mask"]})[0]
202
  wav = voc.run(None, {"mel": mel.astype(np.float32)})[0].reshape(-1)
203
  sf.write("out.wav", wav, meta["sample_rate"])
204
  ```
205
- The whole pipeline — `encoder.onnx → numpy lengthregulator → decoder.onnx → vocoder.onnx` — is
206
- torch‑free and runs as‑is on a Jetson Nano CPU. See `scripts/synth_from_text.py` for the full runtime.
207
-
208
- ---
209
-
210
- ## Training data
211
-
212
- Everything is **distilled from a single teacher voice** so zh / en / code‑mix share one timbre and accent.
213
-
214
- - **Reference voice** a young Taiwan‑female speaker from **[Mozilla Common Voice](https://commonvoice.mozilla.org/) zh‑TW**,
215
- released **CC0 / public domain** (commercial‑use and voice‑cloning clear). ~13 s assembled from that
216
- one speaker's cleanest validated clips. This fixes the accent (Taiwan Mandarin comes from the *reference*,
217
- not from prompting) **and** keeps the model commercially shippable no proprietary/voice‑likeness encumbrance.
218
- - **Teacher** **VoxCPM2** (`openbmb/VoxCPM2`) voice‑clones that one reference for every line, giving a
219
- consistent young‑female voice across all three languages (48 kHz, resampled to 24 kHz for training).
220
- - **Text** Taiwan office / phone‑attendant / GPS / transit register: diverse Mandarin, general +
221
- domain English, and frame‑bank code‑mix with English in varied positions, **plus a large named‑entity
222
- bank**: Taiwan place & road names, transit stations, top Taiwan + world companies, famous people
223
- (TW + world), movies, electronics products, and time/date/metric expressions.
224
- - **Entity normalization** (`text_norm.py`, applied identically to teacher text and at inference) gives
225
- consistent readings for **phone numbers, extensions, email addresses, street addresses, prices,
226
- dates (zh + en), times, temperatures (°C), percentages, decimals, counts, and serial numbers**
227
- digit‑by‑digit vs cardinal vs ordinal chosen by entity + language context.
228
- - **ASR quality gate** generic clips are transcribed and kept only if they match their text, using a
229
- **Taiwan‑tuned recognizer** so the gate never penalizes the accent we want (proper‑noun‑heavy coverage
230
- clips are trusted unfiltered, since ASR mangles proper nouns):
231
- - zh & code‑mix → **Breeze‑ASR‑25** Han‑level CER
232
- - English **Whisper‑medium** WER
233
-
234
- | split | clips (post‑gate) |
235
- |---|---|
236
- | pure Chinese | 11,842 |
237
- | code‑mix (zh+en) | 13,422 |
238
- | pure English | 4,283 |
239
- | **total** | **29,547** |
240
-
241
- The corpus is assembled from **32,500** teacher clips; the generic subset passes the ASR gate (≈21%
242
- dropped), the named‑entity coverage subset is trusted unfiltered, and English rows are upsampled ×2
243
- (~27% exposure) to protect English quality. English phones additionally carry v1's native pronunciation
244
- via the warm‑start.
245
-
246
- ---
247
-
248
- ## How it was trained — the levers
249
-
250
- The recipe was established on the **4.63M v3** (kept in `v3_4.6M/`) and grown into the **6.85M v4
251
- default**. Three data/alignment levers carry across both and matter most for a tiny model:
252
-
253
- 1. **Phone‑level alignment** (`scripts/align_durations_v4.py`) — true per‑phone durations (espeak
254
- phoneme‑CTC + `torchaudio.forced_align`) instead of crude char/letter CTC. Sub‑syllable boundary
255
- accuracy is what separates intelligible speech from fluent babble; skipping this makes tiny TTS garble.
256
- 2. **Vocabulary coverage + diverse code‑mix** — broad character coverage and a code‑mix **frame bank**
257
- (varied syntax, English in varied positions) so the model isn't overfit to a few templates.
258
- 3. **Teacher choice** — the English a tiny model learns is only as native as the teacher's; **VoxCPM2**
259
- gives clean, natural zh *and* en in one voice.
260
-
261
- **What changed from v3 (4.63M) → v4 (6.85M, current default):**
262
- - **More capacity** — hidden 168→184, decoder 6→7 layers, ff×3→×4, and **contextual prosody predictors**
263
- on (a Conv‑FFN refinement block + per‑phone duration/energy/bright/pitch deltas). Latency is unchanged
264
- (vocoder + host length‑regulation dominate) — still CPU real‑time, incl. **1.3× real‑time on 2 cores of
265
- a Jetson Nano**.
266
- - **Multi‑resolution‑STFT clarity loss** added alongside the 2D mel‑GAN (both ramp in after a 25k pure‑
267
- reconstruction warmup) to sharpen the predicted mel.
268
- - **Trained from scratch** (the architecture changed, so the v1 warm‑start no longer applies). v3 was
269
- warm‑started from Inflect‑Nano‑v1's English‑native checkpoint, which is why v3 still edges v4 on
270
- **code‑mixed** text; v4 matches v3 on pure Mandarin (zh‑CER 0.083) and was chosen for clearer, more
271
- naturally‑timed delivery (preferred by ear).
272
-
273
- The shipped v4 checkpoint is the **held‑out best (50k step)** — past that the mel‑GAN keeps sharpening the
274
- train mel but held‑out intelligibility drifts, so sweep the held‑out set and pick the optimum rather than
275
- the last step. **A v1 warm‑start of the v4 architecture is the clear next lever** for closing the code‑mix
276
- gap, since pure‑zh is already at parity.
277
-
278
- ---
279
-
280
- ## Architecture
281
-
282
- - **Acoustic** — `MicroFastSpeech` (v4: **5.43M**): depthwise Conv‑FFN, **no attention**, external
283
- durations + length regulator, frame‑pitch, BiGRU, postnet, plus **contextual prosody predictors**.
284
- Exact v4 config (read from the checkpoint; the trainer builds from these flags):
285
-
286
- ```json
287
- { "vocab_size": 256, "tone_size": 16, "lang_size": 4, "n_mels": 80,
288
- "hidden": 184, "encoder_layers": 6, "decoder_layers": 7, "decoder_ff_mult": 4,
289
- "kernel_size": 7, "speaker_count": 2, "speaker_dim": 64, "dropout": 0.08,
290
- "sample_rate": 24000, "max_frames": 1400, "postnet_scale": 0.1,
291
- "use_frame_pitch": true, "use_frame_pitch_refiner": true, "abs_frame_bins": 512,
292
- "use_contextual_predictors": true, "use_group_duration_planner": true }
293
- ```
294
- > The **group‑duration planner** is trained but **disabled at export** (it uses a non‑ONNX‑able host
295
- > loop and only adjusts inference‑time durations); `scripts/export_8k.py` sets it to `None`
296
- > automatically, so the ONNX uses the plain per‑phone durations (with the contextual delta).
297
- > The v3 4.63M config (`hidden 168, enc 5 / dec 6, ff×3`, predictors off) is preserved in `v3_4.6M/`.
298
-
299
- - **Vocoder** — Snake‑HiFiGAN (~1.17M), **24 kHz variant** `snake_v2mid` (sr 24000, n_fft 1024, hop 256,
300
- 80 mels, fmax 12000), retrained on the teacher corpus. **Shared by v3 and v4.**
301
- - **Frontend** — `g2pw` (Taiwan bopomofo + polyphone disambiguation) + `g2p_en` (arpabet), merged into
302
- one phone sequence with per‑phone language ids → handles zh, en, and code‑mix in a single pass.
303
- **88‑symbol table** (`symbol_table.json`), identical for v3 and v4.
304
- - **Long text** — the absolute positional code saturates past `max_frames` (~1400 frames ≈ 15 s), so
305
- utterances longer than that are **auto‑chunked at punctuation** (`scripts/synth_long.py`); the live
306
- Space does this transparently.
307
-
308
- ---
309
-
310
- ## Reproduce / fine‑tune your own
311
-
312
- Pipeline: **teacher corpus → ASR gate → align → train vocoder → warm‑start + train acoustic → export**.
313
- Repo layout:
314
-
315
- ```
316
- acoustic_encoder.onnx acoustic_decoder.onnx vocoder.onnx meta.json symbol_table.json ← DEFAULT = v4 6.85M (24 kHz)
317
- v3_4.6M/{acoustic_encoder,acoustic_decoder,vocoder}.onnx v3_4.6M/meta.json ← prior 4.63M default, for record/rollback
318
- checkpoints/inflect-micro-fastspeech-v4-50000.pt ← v4 acoustic (shipped)
319
- checkpoints/inflect-micro-fastspeech-v3-30000.pt ← v3 acoustic (4.63M)
320
- checkpoints/hifigan-snake_v2mid-final.pt ← vocoder (shared by v3 & v4)
321
- scripts/ frontend, aligner, corpus‑gen, train/export (export_8k.py), long‑text chunking (synth_long.py), eval
322
- inflect_nano/ the trainer (acoustic.py + vocoder.py), forked from Inflect‑Nano‑v1 (LICENSE included)
323
- ```
324
-
325
- **Prerequisites:** Python 3.12, a GPU for training; `pip install torch torchaudio transformers
326
- onnxruntime soundfile librosa g2pw g2p_en cn2an opencc faster-whisper edge-tts`.
327
-
328
- ### 1 · Teacher corpus (one cloned voice)
329
- ```bash
330
- # make a Taiwan‑female reference, then VoxCPM2‑clone every line in that voice
331
- edge-tts --voice zh-TW-HsiaoChenNeural --text "<ref sentence>" --write-media ref.mp3
332
- python gen_voxcpm_corpus.py --texts texts.jsonl --ref ref.wav --ref-text ref.txt \
333
- --out-dir corpus --manifest manifest.jsonl
334
- ```
335
-
336
- ### 2 · ASR quality gate (Taiwan‑tuned)
337
- ```bash
338
- python asr_filter.py --manifest manifest.jsonl --out manifest \
339
- --device cuda # Breeze‑ASR‑25 (zh/mix) + Whisper‑medium (en) → manifest.clean.jsonl
340
- ```
341
-
342
- ### 3 · Phone‑level alignment ⭐ *the key step*
343
- ```bash
344
- python scripts/align_durations_v4.py --manifest manifest.clean.jsonl --out align.jsonl
345
- ```
346
-
347
- ### 4 · Train the 24 kHz vocoder
348
- ```bash
349
- PYTHONPATH=. python -m inflect_nano.vocoder --train-jsonl voc_rows.jsonl \
350
- --out-dir vocoder_24k --variant snake_v2mid --steps 40000 --segment-size 16384 --stft-weight 2.5
351
- ```
352
-
353
- ### 5 · Train the acoustic model
354
-
355
- **v4 (current default, 6.85M, from scratch)** — capacity + contextual prosody + mel‑GAN + MR‑STFT clarity:
356
- ```bash
357
- PYTHONPATH=. PITCH_CACHE_DIR=pitch_cache python -m inflect_nano.acoustic \
358
- --durations-jsonl align.jsonl --out-dir acoustic_24k_v4 \
359
- --hidden 184 --encoder-layers 6 --decoder-layers 7 --decoder-ff-mult 4 \
360
- --contextual-predictors --group-duration-planner --group-duration-weight 0.05 \
361
- --vocoder-variant snake_v2mid --sample-rate 24000 \
362
- --vocoder-checkpoint vocoder_24k/hifigan-snake_v2mid-final.pt --vocoder-mel-weight 1.0 \
363
- --vocoder-mrstft-weight 1.0 --mrstft-warmup-steps 25000 \
364
- --mel-gan-weight 0.1 --gan-2d --gan-fm-auto --gan-r1-gamma 1.0 --gan-crop 128 --gan-warmup-steps 25000 \
365
- --frame-pitch-weight 1.0 --duration-weight 0.08 --pitch-weight 0.04 \
366
- --steps 60000 --batch-size 8 --lr 2e-4 --max-frames 1400 --en-upsample 2 \
367
- --save-interval 5000 --preload-features --device cuda
368
- # ~5 h on one 24 GB GPU. Ship the HELD-OUT best (step 50000 here), not the last step.
369
- ```
370
-
371
- **v3 (4.63M, warm‑started)** — the prior default, reproduced for record:
372
- ```bash
373
- PYTHONPATH=. python -m inflect_nano.acoustic --durations-jsonl align.jsonl \
374
- --out-dir acoustic_24k_v3 --vocoder-variant snake_v2mid --sample-rate 24000 \
375
- --vocoder-checkpoint vocoder_24k/hifigan-snake_v2mid-final.pt --vocoder-mel-weight 1.0 \
376
- --init-checkpoint inflect_nano_v1_acoustic.pt \
377
- --mel-gan-weight 0.1 --gan-2d --gan-fm-auto --gan-r1-gamma 1.0 --gan-crop 128 --gan-warmup-steps 25000 \
378
- --steps 60000 --batch-size 8 --max-frames 1400 --en-upsample 2 # ship step 30000
379
- ```
380
-
381
- ### 6 · Export to ONNX + evaluate
382
- ```bash
383
- python scripts/export_8k.py --acoustic-ckpt acoustic_8k/…pt --vocoder-ckpt vocoder_8k/…pt --out-dir onnx/
384
- python scripts/synth_from_text.py --onnx-dir onnx --out-dir syn --texts eval.jsonl
385
- python scripts/assess_big.py --synth-dir syn # offline CER/WER
386
- ```
387
- Evaluate on **≥30 held‑out sentences** — small eval sets are too noisy to trust. Sweep checkpoints and
388
- pick the **held‑out** sweet spot (the GAN keeps improving train‑set sharpness past the held‑out optimum).
389
-
390
- ---
391
-
392
- ## Train on your OWN voice — one command
393
-
394
- Swap the reference voice; everything else (text pools, ASR gate, alignment, recipe) is fixed. Both
395
- vocoder and acoustic are retrained (both are voice-specific). Text pools + eval sets are bundled in
396
- `data/` and at the repo root, so it reproduces exactly.
397
-
398
- ```bash
399
- # 0. one venv with the deps (see prereqs in scripts/rebuild_voice.sh), PYTHONPATH=repo root,
400
- # and inflect_nano_v1_acoustic.pt from owensong/Inflect-Nano-v1 for the warm-start.
401
- huggingface-cli download Luigi/PrimeTTS --local-dir PrimeTTS && cd PrimeTTS
402
- cp data/*.jsonl data/*.txt . # text pools at root
403
-
404
- # 1. a ~10 s clip of your voice. For a commercial-clear reference, use a CC0 source such as
405
- # Mozilla Common Voice zh-TW (the shipped model uses a young-female Common Voice speaker). Or synth one:
406
- edge-tts --voice zh-TW-HsiaoYuNeural --text "您好,歡迎來電。Thank you for calling." --write-media ref.mp3
407
- ffmpeg -y -i ref.mp3 -ar 24000 -ac 1 ref.wav ; printf '%s' "您好,歡迎來電。Thank you for calling." > ref.txt
408
-
409
- # 2. ONE command -> corpus -> gate -> align -> vocoder -> acoustic -> export
410
- PY=/path/to/venv/bin/python ./scripts/rebuild_voice.sh ref.wav ref.txt myvoice
411
- # -> pick best corpus_myvoice/onnx_<K>/ (~35k is the usual held-out sweet spot)
412
- ```
413
- **Time on dual RTX 5090:** ≈ 9 h end-to-end (~6.5 h to a shippable 35k checkpoint) — synth ~2 h,
414
- gate+align ~25 min, then vocoder (~3 h) ∥ acoustic (~4–7 h) in parallel, export ~15 min.
415
 
416
  ## Credits & licenses
417
 
418
- - **Base model / trainer:** [`owensong/Inflect-Nano-v1`](https://huggingface.co/owensong/Inflect-Nano-v1) (Apache2.0; see `inflect_nano/LICENSE.inflect-nano`)
419
- - **Teacher TTS:** [`openbmb/VoxCPM2`](https://huggingface.co/openbmb/VoxCPM2) · **Reference voice:** [Mozilla **Common Voice** zh‑TW](https://commonvoice.mozilla.org/datasets) (**CC0 / public domain**)
420
- - **Gate ASR:** `Breeze-ASR-25` (MediaTek Research, Taiwan Mandarin + code‑switch) · OpenAI **Whisper‑medium**
421
- - **Aligner:** `facebook/wav2vec2-lv-60-espeak-cv-ft` + `torchaudio.forced_align`
422
- - **Frontend:** `g2pw` (Taiwan readings) + `g2p_en` · **Eval ASR:** sherpaonnx XASR (zh‑en Zipformer)
423
 
424
- This repository: **Apache2.0**.
 
18
  pipeline_tag: text-to-speech
19
  ---
20
 
21
+ # PrimeTTS — tiny on-device zh-TW + English TTS
22
 
23
+ A **4.09M-parameter** Taiwan-Mandarin + English text-to-speech model that runs **entirely on CPU** — small
24
+ and fast enough for **Jetson-class on-device** use (contact-centre, GPS, transit). One model, **one
25
+ young-female voice**: Chinese, English, and code-mix through a single frontend (no language routing), built
26
+ for **entity correctness** — phone numbers, emails, addresses, prices, dates, temperatures, %, serials.
 
27
 
28
+ > 🔊 **Live demo:** https://huggingface.co/spaces/Luigi/PrimeTTS-vs-Inflect-Nano-v1
29
 
30
  | | |
31
  |---|---|
32
+ | **Parameters** | **4.09M** = 3.56M acoustic *(incl. 97K pitch refiner)* + 0.53M vocoder |
33
+ | **Sample rate** | 8 kHz *(on-device; a 24 kHz variant is in-repo)* |
34
+ | **Runtime** | `onnxruntime`, **CPU-only**, torch-free at inference |
35
+ | **Languages** | zh-TW (Traditional) + English + code-mix single voice |
36
+ | **Voice** | young female, Taiwan-Mandarin accent |
37
+ | **Measured** | zh-CER 0.109 · en-WER 0.083 · **Jetson Nano RTF 0.35** (1 thread) |
38
+ | **License** | Apache-2.0 |
39
+
40
+ ## Highlights
41
+
42
+ - **Tiny + CPU-only** — ~4M params, ONNX, torch-free; real-time on a Jetson Nano (**RTF 0.35, single thread**).
43
+ - **One voice, three modes** — zh / en / code-mix share one timbre and accent through a single frontend; no
44
+ language tag needed.
45
+ - **Mandarin tones via a frame-pitch refiner** a 97K-param module turns coarse per-phoneme pitch into a
46
+ per-frame F0 contour (the tone carrier). Ablating it costs **+18% relative zh-CER** (zh-only; English is
47
+ unaffected no lexical tone).
48
+ - **Entity-correct** a normalization layer reads numbers, dates, prices, emails, addresses, serials, and
49
+ spells acronyms/letters (VIP V-I-P), applied identically in training and at inference.
50
+
51
+ ## Performance — held-out (36 unseen phone-attendant sentences)
52
+
53
+ | metric | value |
54
+ |---|---|
55
+ | zh-CER (Breeze-ASR-25) | **0.109** (pure-zh 0.119 · code-mix 0.098) |
56
+ | en-WER (Whisper) | **0.083** |
57
+ | Taiwan-accent gap¹ | **+0.033** (genuine TW accent) |
58
+ | SQUIM PESQ / STOI | 2.22 / 0.90 |
59
+ | **On Jetson Nano** (ORT-CPU, 1 thread) | **RTF 0.347** · on-device CER ≈ 0.117 |
 
 
 
 
60
 
61
+ ¹ `CER(generic ASR) − CER(Taiwan-tuned Breeze-ASR-25)` per zh clip; `>0` ⇒ a Taiwan recognizer understands
62
+ it better ⇒ a real Taiwan accent is present.
63
 
64
+ > **On audio quality:** at 8 kHz the band ceiling is 4 kHz (Nyquist), which discards the brightness/sibilance
65
+ > above it — clear and intelligible, but *telephone-band*. A **16 kHz variant (0–8 kHz, ~5.0M) is in
66
+ > progress** for higher fidelity while staying on-device; the **24 kHz** files (below) are the fullest today.
67
 
68
+ ## Architecture
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ **Acoustic — `MicroFastSpeech` (3.56M).** FastSpeech-style, **no attention**: depthwise gated Conv-FFN,
71
+ external durations + length regulator, frame-pitch, BiGRU, postnet — plus the **frame-pitch refiner**
72
+ (`Conv1d → SiLU → Conv1d(groups=4) → SiLU → Conv1d`, 97K) that builds the per-frame F0 contour = Mandarin tones.
73
 
74
+ ```json
75
+ { "vocab_size": 256, "tone_size": 16, "lang_size": 4, "n_mels": 80,
76
+ "hidden": 168, "encoder_layers": 5, "decoder_layers": 6, "decoder_ff_mult": 3,
77
+ "sample_rate": 8000, "max_frames": 1000, "use_frame_pitch_refiner": true }
78
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ **Vocoder — Snake-HiFiGAN.** The on-device model uses the lightweight `snake_8k_lite`. The family — same
81
+ architecture, different width / sample rate — is **why a model's headline param count varies**:
82
+
83
+ | variant | used by | params | rate | band | PESQ² |
84
+ |---|---|---|---|---|---|
85
+ | **`snake_8k_lite`** | **PrimeTTS (on-device)** | **0.53M** | 8 kHz | 0–4 kHz | 2.34 |
86
+ | `snake_8k` | 8 kHz, full width | 1.15M | 8 kHz | 0–4 kHz | 2.60 |
87
+ | `snake_v2mid` | 24 kHz variant | 1.17M | 24 kHz | 0–12 kHz | 3.23 |
88
+ | `snake_16k` | 16 kHz *(roadmap)* | 1.43M | 16 kHz | 0–8 kHz | — |
89
+
90
+ ² SQUIM-PESQ, 5-utterance mean. The big 8 → 24 kHz jump is the **sample rate** (band), *not* the vocoder;
91
+ `_lite` trades ~0.26 PESQ vs the full `snake_8k` for ~2.2× less compute (the lower Nano RTF). So the **4.09M
92
+ on-device total** = 3.56M acoustic + the **0.53M lite vocoder**; the 24 kHz files pair the same-class acoustic
93
+ with the heavier 1.17M `snake_v2mid`.
94
 
95
+ **Frontend.** `g2pw` (Taiwan bopomofo + polyphone disambiguation) + `g2p_en` (arpabet), merged into one phone
96
+ sequence with per-phone language ids — zh, en, code-mix in a single pass. **88-symbol table.** Entity
97
+ normalization (`text_norm.py`) handles numbers / dates / prices / emails / serials, spells ALL-CAPS acronyms
98
+ and a small brand lexicon. Text past `max_frames` is auto-chunked at punctuation.
99
+
100
+ ## Model files
101
+
102
+ ```
103
+ v1b_8k/{acoustic_encoder,acoustic_decoder,vocoder}.onnx + meta.json ← RECOMMENDED on-device (4.09M, 8 kHz) — the demo serves this
104
+ {acoustic_encoder,acoustic_decoder,vocoder}.onnx + meta.json ← 24 kHz higher-fidelity variant (6.85M)
105
+ v3_4.6M/… ← legacy 24 kHz (4.63M), kept for record
106
+ scripts/ frontend, aligner, corpus-gen, train / export, eval
107
+ inflect_nano/ the trainer (acoustic.py + vocoder.py), forked from Inflect-Nano-v1 (LICENSE included)
108
+ ```
109
+
110
+ ## Quickstart (CPU)
111
 
112
  ```bash
113
  pip install onnxruntime numpy soundfile g2pw g2p_en cn2an
114
  huggingface-cli download Luigi/PrimeTTS --local-dir PrimeTTS
115
  ```
116
  ```python
117
+ import sys; sys.path.insert(0, "PrimeTTS/scripts")
 
118
  import json, numpy as np, onnxruntime as ort, soundfile as sf
119
  import frontend_bopomofo as F
120
+ from synth_from_text import host_regulate # numpy length-regulator
121
 
122
+ D = "PrimeTTS/v1b_8k" # the on-device model
123
+ meta = json.load(open(f"{D}/meta.json"))
124
+ enc = ort.InferenceSession(f"{D}/acoustic_encoder.onnx", providers=["CPUExecutionProvider"])
125
+ dec = ort.InferenceSession(f"{D}/acoustic_decoder.onnx", providers=["CPUExecutionProvider"])
126
+ voc = ort.InferenceSession(f"{D}/vocoder.onnx", providers=["CPUExecutionProvider"])
127
 
128
+ o = F.text_to_ids("您好,歡迎使用 PrimeTTS。Thank you for calling.")
129
  ph, tn, lg = (np.array([o[k]], np.int64) for k in ("phone_ids", "tone_ids", "lang_ids"))
130
+ cond, dur, pitch = enc.run(None, {"phone": ph, "tone": tn, "lang": lg, "speaker": np.zeros(1, np.int64)})
 
131
  reg = host_regulate(cond, dur, pitch, meta["abs_frame_bins"], meta["max_frames"])
132
  mel = dec.run(None, {k: reg[k] for k in
133
  ["frames","frame_meta","local_ctx_raw","abs_pos","pitch_frame","frame_mask"]})[0]
134
  wav = voc.run(None, {"mel": mel.astype(np.float32)})[0].reshape(-1)
135
  sf.write("out.wav", wav, meta["sample_rate"])
136
  ```
137
+ The pipeline — `encoder → numpy length-regulator → decoder → vocoder` — is torch-free and runs as-is on a
138
+ Jetson Nano CPU. (`scripts/synth_long.py` adds the punctuation auto-chunking for long text.)
139
+
140
+ ## Training
141
+
142
+ **Distilled from a single teacher voice** so zh / en / code-mix share one timbre and accent:
143
+
144
+ - **Reference voice** a young Taiwan-female speaker from **Mozilla Common Voice zh-TW** (**CC0 / public
145
+ domain**, commercial-clear). ~13 s of the cleanest clips fix the accent (it comes from the *reference*, not
146
+ prompting) and keep the model shippable.
147
+ - **Teacher** **VoxCPM2** (`openbmb/VoxCPM2`) voice-clones that reference for every line (48 kHz → resampled).
148
+ - **Corpus** Taiwan office / phone / GPS / transit register: diverse Mandarin, general + domain English,
149
+ code-mix in varied positions, a large **named-entity bank** (TW + world places / roads / transit / companies
150
+ / people / products), plus a **rare-character + brand + email booster** (the latest data lever).
151
+ - **ASR gate** Breeze-ASR-25 (zh / mix CER) + Whisper-medium (en WER) keep only clips matching their text;
152
+ proper-noun coverage clips are trusted unfiltered.
153
+
154
+ The three levers that matter most for a tiny model: **phone-level alignment** (espeak phoneme-CTC +
155
+ `torchaudio.forced_align` sub-syllable boundaries are what separate intelligible speech from fluent babble),
156
+ **broad coverage + diverse code-mix**, and the **teacher** (a student's English is only as native as its
157
+ teacher's). Pipeline: `teacher corpus ASR gate align train vocoder → warm-start + train acoustic →
158
+ export`. The 8 kHz on-device model warm-starts the 24 kHz acoustic and adapts to 8 kHz the trainer resamples
159
+ audio and rescales durations internally. Full commands and a one-shot `scripts/rebuild_voice.sh` (swap in your
160
+ own ~10 s reference clip) are in the repo.
161
+
162
+ ## Known characteristics & limitations
163
+
164
+ - **8 kHz band ceiling** — telephone-band brightness (the 16 kHz variant in progress addresses it).
165
+ - **Empty-rime syllables** (是 / 十 / 日, the syllabic ㄭ) and **isolated spelled letters** (the leading "A" of a
166
+ serial) are the fragile cases at this size: the frontend emits the right phones, but a ~4M acoustic renders
167
+ them weakly. Cross-checking a robust (Breeze) vs strict (X-ASR) recognizer exposes this where a single CER
168
+ number hides it.
169
+ - **Phrase-initial bare vowels** in *ultra-short isolated* inputs ("二月" alone) can garble; fine in normal
170
+ sentences.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  ## Credits & licenses
173
 
174
+ - **Base / trainer:** [`owensong/Inflect-Nano-v1`](https://huggingface.co/owensong/Inflect-Nano-v1) (Apache-2.0)
175
+ - **Teacher:** [`openbmb/VoxCPM2`](https://huggingface.co/openbmb/VoxCPM2) · **Reference voice:**
176
+ [Mozilla Common Voice zh-TW](https://commonvoice.mozilla.org/datasets) (**CC0 / public domain**)
177
+ - **Gate ASR:** Breeze-ASR-25 (MediaTek Research) · Whisper-medium · **Aligner:**
178
+ `facebook/wav2vec2-lv-60-espeak-cv-ft` + `torchaudio.forced_align` · **Eval:** sherpa-onnx X-ASR
179
 
180
+ This repository: **Apache-2.0**.