Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- .gitignore +13 -0
- README.md +103 -0
- STATUS.md +258 -0
- check_precision.py +73 -0
- cpu_fp16/code_predictor.onnx +3 -0
- cpu_fp16/codec_embed.onnx +3 -0
- cpu_fp16/manifest.json +30 -0
- cpu_fp16/residual_embed.onnx +3 -0
- cpu_fp16/talker_cache.onnx +3 -0
- cpu_fp16/text_embed.onnx +3 -0
- cpu_fp16/tok_decoder.onnx +3 -0
- cpu_fp16/tok_encoder.onnx +3 -0
- cpu_fp32/code_predictor.onnx +3 -0
- cpu_fp32/codec_embed.onnx +3 -0
- cpu_fp32/manifest.json +30 -0
- cpu_fp32/residual_embed.onnx +3 -0
- cpu_fp32/talker_cache.onnx +3 -0
- cpu_fp32/text_embed.onnx +3 -0
- cpu_fp32/tok_decoder.onnx +3 -0
- cpu_fp32/tok_encoder.onnx +3 -0
- cpu_int4/code_predictor.onnx +3 -0
- cpu_int4/codec_embed.onnx +3 -0
- cpu_int4/manifest.json +30 -0
- cpu_int4/residual_embed.onnx +3 -0
- cpu_int4/talker_cache.onnx +3 -0
- cpu_int4/text_embed.onnx +3 -0
- cpu_int4/tok_decoder.onnx +3 -0
- cpu_int4/tok_encoder.onnx +3 -0
- cuda_fp16/codec_embed.onnx +3 -0
- cuda_fp16/manifest.json +30 -0
- cuda_fp16/residual_embed.onnx +3 -0
- cuda_fp32/manifest.json +30 -0
- cuda_fp32/talker_cache.onnx.data +3 -0
- cuda_fp32/text_embed.onnx +3 -0
- cuda_int4/manifest.json +30 -0
- cuda_int4/talker_cache.onnx +3 -0
- cuda_int4/text_embed.onnx +3 -0
- cuda_int4/tok_decoder.onnx +3 -0
- cuda_int4/tok_encoder.onnx +3 -0
- eval_cache.py +49 -0
- eval_embed.py +56 -0
- eval_generate.py +98 -0
- eval_predictor.py +74 -0
- eval_speaker.py +55 -0
- eval_tokenizer.py +174 -0
- inference.py +591 -0
- info.yml +11 -0
- optimize.py +263 -0
- requirements.txt +10 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
cuda_fp32/talker_cache.onnx.data filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Downloaded checkpoint + intermediate/build artifacts
|
| 2 |
+
model/
|
| 3 |
+
cache/
|
| 4 |
+
*.onnx
|
| 5 |
+
*.onnx.data
|
| 6 |
+
*.safetensors
|
| 7 |
+
_genai_*/
|
| 8 |
+
__pycache__/
|
| 9 |
+
*.pyc
|
| 10 |
+
|
| 11 |
+
codes/**/__pycache__/
|
| 12 |
+
_load_test.py
|
| 13 |
+
build_tok.log
|
README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
pipeline_tag: text-to-speech
|
| 4 |
+
language:
|
| 5 |
+
- zh
|
| 6 |
+
- en
|
| 7 |
+
- ja
|
| 8 |
+
- ko
|
| 9 |
+
- de
|
| 10 |
+
- fr
|
| 11 |
+
- ru
|
| 12 |
+
- pt
|
| 13 |
+
- es
|
| 14 |
+
- it
|
| 15 |
+
tags:
|
| 16 |
+
- tts
|
| 17 |
+
- qwen
|
| 18 |
+
- audio
|
| 19 |
+
- onnx
|
| 20 |
+
- onnxruntime-genai
|
| 21 |
+
- multilingual
|
| 22 |
+
base_model:
|
| 23 |
+
- Qwen/Qwen3-TTS-12Hz-0.6B-VoiceDesign
|
| 24 |
+
arxiv: 2601.15621
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
# Qwen3-TTS-12Hz-0.6B-CustomVoice
|
| 28 |
+
|
| 29 |
+
[Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS) is a series of advanced multilingual, controllable, robust, and streaming text-to-speech models developed by the Qwen team.
|
| 30 |
+
|
| 31 |
+
This specific checkpoint is the **0.6B CustomVoice** variant, based on the **12Hz** tokenizer. It supports 9 premium timbres and allows for fine-grained style control over target voices via natural language instructions across 10 major languages.
|
| 32 |
+
|
| 33 |
+
- **Paper:** [Qwen3-TTS Technical Report](https://huggingface.co/papers/2601.15621)
|
| 34 |
+
- **GitHub:** [QwenLM/Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS)
|
| 35 |
+
- **Demo:** [Hugging Face Spaces](https://huggingface.co/spaces/Qwen/Qwen3-TTS)
|
| 36 |
+
|
| 37 |
+
## Key Features
|
| 38 |
+
* **Multilingual Synthesis**: Supports Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, and Italian.
|
| 39 |
+
* **Intelligent Control**: Adapts tone, rhythm, and emotional expression based on natural language instructions (e.g., "Speak in a very happy tone").
|
| 40 |
+
* **Low Latency**: Optimized for streaming generation with the Qwen3-TTS-Tokenizer-12Hz, achieving end-to-end synthesis latency as low as 97ms.
|
| 41 |
+
|
| 42 |
+
## Quickstart
|
| 43 |
+
|
| 44 |
+
To use Qwen3-TTS, you can install the `qwen-tts` package:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
pip install -U qwen-tts
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Sample Usage
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
import torch
|
| 54 |
+
import soundfile as sf
|
| 55 |
+
from qwen_tts import Qwen3TTSModel
|
| 56 |
+
|
| 57 |
+
# Load the model
|
| 58 |
+
model = Qwen3TTSModel.from_pretrained(
|
| 59 |
+
"Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice",
|
| 60 |
+
device_map="cuda:0",
|
| 61 |
+
dtype=torch.bfloat16,
|
| 62 |
+
attn_implementation="flash_attention_2",
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Generate speech with specific instructions
|
| 66 |
+
wavs, sr = model.generate_custom_voice(
|
| 67 |
+
text="其实我真的有发现,我是一个特别善于观察别人情绪的人。",
|
| 68 |
+
language="Chinese",
|
| 69 |
+
speaker="Vivian",
|
| 70 |
+
instruct="用特别愤怒的语气说",
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# Save the generated audio
|
| 74 |
+
sf.write("output_custom_voice.wav", wavs[0], sr)
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Supported Speakers
|
| 78 |
+
|
| 79 |
+
For `Qwen3-TTS-12Hz-0.6B-CustomVoice`, the following speakers are supported. We recommend using each speaker’s native language for the best results:
|
| 80 |
+
|
| 81 |
+
| Speaker | Voice Description | Native Language |
|
| 82 |
+
| --- | --- | --- |
|
| 83 |
+
| Vivian | Bright young female voice. | Chinese |
|
| 84 |
+
| Serena | Warm, gentle young female voice. | Chinese |
|
| 85 |
+
| Uncle_Fu | Seasoned male voice, mellow timbre. | Chinese |
|
| 86 |
+
| Dylan | Youthful Beijing male voice. | Chinese (Beijing) |
|
| 87 |
+
| Eric | Lively Chengdu male voice. | Chinese (Sichuan) |
|
| 88 |
+
| Ryan | Dynamic male voice with rhythm. | English |
|
| 89 |
+
| Aiden | Sunny American male voice. | English |
|
| 90 |
+
| Ono_Anna | Playful Japanese female voice. | Japanese |
|
| 91 |
+
| Sohee | Warm Korean female voice. | Korean |
|
| 92 |
+
|
| 93 |
+
## Citation
|
| 94 |
+
If you find Qwen3-TTS useful for your research, please consider citing:
|
| 95 |
+
|
| 96 |
+
```bibtex
|
| 97 |
+
@article{Qwen3-TTS,
|
| 98 |
+
title={Qwen3-TTS Technical Report},
|
| 99 |
+
author={Hangrui Hu and Xinfa Zhu and Ting He and Dake Guo and Bin Zhang and Xiong Wang and Zhifang Guo and Ziyue Jiang and Hongkun Hao and Zishan Guo and Xinyu Zhang and Pei Zhang and Baosong Yang and Jin Xu and Jingren Zhou and Junyang Lin},
|
| 100 |
+
journal={arXiv preprint arXiv:2601.15621},
|
| 101 |
+
year={2026}
|
| 102 |
+
}
|
| 103 |
+
```
|
STATUS.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen3-TTS — Current Status
|
| 2 |
+
|
| 3 |
+
_Updated: 2026-06-19_
|
| 4 |
+
|
| 5 |
+
## ✅ De-duplicated talker — ship talker_cache only (~870 MB/dir saved)
|
| 6 |
+
`talker.onnx` (no-cache) and `talker_cache.onnx` (KV-cache) hold the SAME transformer weights
|
| 7 |
+
→ shipping both duplicated ~870 MB per dir. Fix: default export now builds **only
|
| 8 |
+
`talker_cache`** (it does prefill+decode, faster O(n), and is what inference auto-uses); plain
|
| 9 |
+
`talker` is still buildable via explicit `--components talker`. Dropped the redundant
|
| 10 |
+
`talker.onnx` (+ manifest entry) from all dirs that had both → **freed ~22.5 GB** across the
|
| 11 |
+
8 built variants. inference.py guards for "neither talker present". Verified generation still
|
| 12 |
+
works on talker_cache-only dirs (KV-cache path, audio produced).
|
| 13 |
+
|
| 14 |
+
## ✅ Config-driven dims — 0.6B now exports; 1.7B undisturbed
|
| 15 |
+
`user_script.py` had 1.7B dims hardcoded (hidden 2048, 28 layers, etc.) in the talker /
|
| 16 |
+
talker_cache / code_predictor / residual_embed io+dummy funcs → 0.6B (hidden **1024**) would
|
| 17 |
+
export wrong. Fixed with a module `_DIMS` populated by `_load_tts()` from the loaded model's
|
| 18 |
+
`talker_config` (Olive calls model_loader before io/dummy). **Defaults equal the 1.7B values**,
|
| 19 |
+
so 1.7B exports are byte-identical (verified: voicedesign/customvoice/base17 dirs untouched).
|
| 20 |
+
**Validated**: full `base/0.6B` int4 export — all 9 components incl. talker/talker_cache/
|
| 21 |
+
speaker_encoder — `onnx/base06/cpu_int4/`, talker `inputs_embeds [batch,seq,1024]` ✓.
|
| 22 |
+
(0.6B differs from 1.7B only in hidden_size; layers/kv/head_dim/groups are identical.)
|
| 23 |
+
|
| 24 |
+
## ✅ EXPORT OOM RESOLVED — `optimize.py` isolates each component in a subprocess
|
| 25 |
+
The Windows pagefile/OOM kill on full-run exports is fixed: `main()` now re-invokes itself once
|
| 26 |
+
per component in a fresh subprocess (`--_child`), so memory is fully reclaimed between builds and
|
| 27 |
+
the heavy talker/talker_cache run alone. Default behavior; `--no-isolate` forces single-process.
|
| 28 |
+
`speaker_encoder` auto-dropped from defaults for non-base models. **Validated**: a full
|
| 29 |
+
**cpu_fp16** voicedesign export (the heaviest case — 5.6 GB fp16 talker + talker_cache) completed
|
| 30 |
+
in ONE command, all 8 components, no OOM → `onnx/voicedesign/cpu_fp16/`.
|
| 31 |
+
|
| 32 |
+
## ✅ All 3 model types exported + validated with their feature
|
| 33 |
+
| dir | model_type | feature | validated |
|
| 34 |
+
|---|---|---|---|
|
| 35 |
+
| `onnx/voicedesign/cpu_int4/` | voice_design | `--instruct` | ✅ generates |
|
| 36 |
+
| `onnx/customvoice/cpu_int4/` | custom_voice | `--speaker ryan` | ✅ generates (27 frames) |
|
| 37 |
+
| `onnx/base17_cpu_int4/` | base | clone `--ref-audio/--ref-text` | ✅ runs e2e; speaker_encoder parity 1.0 |
|
| 38 |
+
Model-type gating enforces the right feature per model (wrong flag → clear error).
|
| 39 |
+
Pending exports: 0.6B Base, other precisions/devices (fp16/fp32, cuda) — mechanical repeats
|
| 40 |
+
(component-by-component due to the pagefile cap). KV-cache talker still blocked.
|
| 41 |
+
|
| 42 |
+
## TL;DR (current session)
|
| 43 |
+
- ✅ Full text→speech `generate()` implemented + **100% greedy parity vs PyTorch** (all-16
|
| 44 |
+
codebooks 432/432). Needed talker→(logits,hidden) + `residual_embed.onnx` + the
|
| 45 |
+
`dynamic_shapes` seq fix — all done & validated.
|
| 46 |
+
- ✅ **Model-type feature gating in inference.py** — `model_type` read from config;
|
| 47 |
+
`_check_features` enforces: voice_design→`--instruct`, custom_voice→`--speaker`,
|
| 48 |
+
base→clone (`--ref-audio/--ref-text`). VALIDATED both ways: voice_design+instruct generates;
|
| 49 |
+
voice_design+speaker raises a clear ValueError. `--text` prints `model_type=…`.
|
| 50 |
+
- ✅ **voicedesign `onnx/voicedesign/cpu_int4/` restored** (all 7 components) and generation
|
| 51 |
+
re-validated (model_type=voice_design, 24-frame wav). Re-export had to be done
|
| 52 |
+
**component-by-component** (talker solo) — a single full-run OOM/pagefile-killed the talker
|
| 53 |
+
(`OSError 1455: paging file too small`). The lighter parts export together.
|
| 54 |
+
- ✅ **Env fix:** `optimize.py` + `inference.py` PEP-723 now pin `numba>=0.60 / llvmlite>=0.43`
|
| 55 |
+
— librosa otherwise pulls numba 0.53.1 → llvmlite 0.36 which won't build on Python 3.12.
|
| 56 |
+
- ✅ Base checkpoints downloaded (`base/1.7B`, `base/0.6B`); **Base-1.7B int4 exported** →
|
| 57 |
+
`onnx/base17_cpu_int4/` (7 components incl. `residual_embed` + 2-output dynamic talker).
|
| 58 |
+
- ✅ **Base voice cloning — IMPLEMENTED & runs end-to-end.** `generate(ref_audio, ref_text)`
|
| 59 |
+
→ `_generate_clone`. Pieces:
|
| 60 |
+
• `speaker_encoder.onnx` exported (ECAPA + **inline mel/STFT** front-end, dynamic audio len,
|
| 61 |
+
fp32, dynamo; `audio[B,T]→x-vector[B,2048]`). `SpeakerEncoderWrapper` reimplements
|
| 62 |
+
mel_spectrogram WITHOUT its `if torch.min(y)<-1` debug branch (broke torch.export).
|
| 63 |
+
• ICL prefill mirrors `generate_icl_prompt`: ref_text+text+eos / codec_bos + **per-frame
|
| 64 |
+
ref-code sum (= `step_embed`/residual_embed!)**, with the x-vector injected in the codec
|
| 65 |
+
prefix. Ref audio→codes via `tok_encoder` in 1 s windows (`encode_chunked`).
|
| 66 |
+
• AR loop factored into shared `_ar_loop`. Verified runs: ref 39 frames + x-vector →
|
| 67 |
+
prefill 64 → 40 frames → 3.2 s wav (`onnx/base17_cpu_int4/`, model_type=base).
|
| 68 |
+
✅ **speaker_encoder parity** (`eval_speaker.py`): ONNX x-vector vs PyTorch
|
| 69 |
+
`extract_speaker_embedding` = **cosine 1.000000, max|Δ| ~1e-6** at 2/3.5/6 s → the
|
| 70 |
+
reimplemented mel/STFT is exact. With tok_encoder (100%), residual_embed/talker/predictor
|
| 71 |
+
(100% TTS parity) all verified and the ICL prefill mirroring the reference, clone is correct
|
| 72 |
+
by construction. (A real-voice listen is still the only thing measuring perceptual fidelity.)
|
| 73 |
+
- ✅ **KV-cache talker — SOLVED (was blocked).** Fix: feed the cache as a plain list of
|
| 74 |
+
[k,v] tensor pairs under input name **`past_kv`** (NOT `past_key_values`, so Olive's
|
| 75 |
+
DynamicCache-pytree auto-conversion — which torch.export rejected — doesn't fire), and
|
| 76 |
+
build the `DynamicCache` in-graph (`from_legacy_cache`/`to_legacy_cache`). `talker_cache.onnx`
|
| 77 |
+
exports (59 in / 58 out: flattened 28×2 K/V). Verified: empty-past output == no-cache talker
|
| 78 |
+
(logits/hidden cos 1.0, max|Δ| 0.0); end-to-end greedy **100% exact** vs no-cache. `inference.py`
|
| 79 |
+
auto-uses it (`_ar_loop_cached`) when present. Speedup grows with length (talker O(n) vs O(n²);
|
| 80 |
+
~1.1x at 36 frames where the 15-call predictor dominates, more for long utterances).
|
| 81 |
+
Exported for `onnx/voicedesign/cpu_int4/` so far.
|
| 82 |
+
- ⏳ **Export dirs:** the earlier `onnx/voicedesign/` & `onnx/customvoice/` were cleared in a
|
| 83 |
+
reorg; only `onnx/base17_cpu_int4/` survives. Re-exporting voicedesign cpu_int4 now.
|
| 84 |
+
- TODO: validate generate on base export; export 0.6B; optional tok_decoder dynamic-frames.
|
| 85 |
+
|
| 86 |
+
## Targets (3 models, auto-detected by config `model_type`) — all downloaded locally
|
| 87 |
+
- `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` (qwen3_tts) ← `voicedesign/`
|
| 88 |
+
- `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` (qwen3_tts) ← `customvoice/`
|
| 89 |
+
- `Qwen/Qwen3-TTS-Tokenizer-12Hz` (qwen3_tts_tokenizer_12hz) ← `tokenizer/` (also embedded in each TTS as `speech_tokenizer/`)
|
| 90 |
+
|
| 91 |
+
## Architecture (verified from repo + load)
|
| 92 |
+
TTS `Qwen3TTSForConditionalGeneration` → `talker`:
|
| 93 |
+
- `talker.model` — Qwen3-style **28 L, hidden 2048, MROPE**, dual codec/text embedding
|
| 94 |
+
- `talker.codec_head` — Linear 2048→3072 (first-codebook logits)
|
| 95 |
+
- `talker.code_predictor` — 5 L, hidden 1024, **16 per-group heads** (residual codes)
|
| 96 |
+
- `talker.text_projection`
|
| 97 |
+
Tokenizer `Qwen3TTSTokenizerV2Model` → **encoder** (wav→codes) + **decoder** (codes→wav),
|
| 98 |
+
24 kHz, 12.5/12 fps, 16 quantizers (decode), conv+transformer+RVQ.
|
| 99 |
+
|
| 100 |
+
## Decomposition → `onnx/{device}_{precision}/` (flat)
|
| 101 |
+
- TTS: `talker.onnx`, `code_predictor.onnx` (+ `tok_encoder/decoder.onnx` from embedded codec)
|
| 102 |
+
- Tokenizer: `tok_encoder.onnx`, `tok_decoder.onnx`
|
| 103 |
+
- codecs forced **fp32** (DAC int4/fp16 too lossy); LLM/predictor honor `--precision`.
|
| 104 |
+
|
| 105 |
+
## create_model (ModelBuilder) — empirically checked, NOT viable for the talker
|
| 106 |
+
onnxruntime-genai ModelBuilder supports these archs: Llama, Mistral, Qwen2, **Qwen3**,
|
| 107 |
+
Gemma/2/3, Phi*, Granite, Nemotron, Olmo, SmolLM3, GptOss, InternLM2, Lfm2, HunYuan,
|
| 108 |
+
VideoChatFlashQwen. The talker is **`qwen3_tts_talker`** (not listed) and, decisively,
|
| 109 |
+
uses **MROPE** (`mrope_section=[24,20,20]`, interleaved) + dual codec/text embedding +
|
| 110 |
+
`codec_head`. Even remapped to `Qwen3ForCausalLM`, ModelBuilder applies **standard RoPE
|
| 111 |
+
(no MROPE)** → positionally wrong output. So create_model can't correctly convert it →
|
| 112 |
+
**Olive export (with the real MROPE forward) is the correct path.** Confirmed via the
|
| 113 |
+
supported-architecture list + the talker rope config.
|
| 114 |
+
|
| 115 |
+
## Key decisions / findings
|
| 116 |
+
- **Talker is NOT ModelBuilder-able** (custom MROPE + dual embedding ≠ stock Qwen3) →
|
| 117 |
+
exported via **Olive** (`OnnxConversion` + RtnQuant/Float16), like CSM backbone.
|
| 118 |
+
(ModelBuilder remap attempted only if a talker is detectably stock.)
|
| 119 |
+
- **transformers version**: vendored `qwen_tts` (in `codes/`) needs **transformers==4.57.3**;
|
| 120 |
+
the shared venv is 5.10.2 (incompatible — check_model_inputs, config defaults,
|
| 121 |
+
ROPE_INIT_FUNCTIONS, …). Solved by making **optimize.py a PEP-723 uv script** that pins
|
| 122 |
+
4.57.3 in an isolated env (`uv run optimize.py ...`). Shared venv untouched.
|
| 123 |
+
One vendored patch: `@check_model_inputs()` → `@check_model_inputs` (tf5 API); 25Hz
|
| 124 |
+
tokenizer import made optional (needs `sox`).
|
| 125 |
+
- **Model loads cleanly** under 4.57.3 (instantiates; submodules mapped).
|
| 126 |
+
|
| 127 |
+
## Fixed — external-data relink on flattened models
|
| 128 |
+
`optimize.py` flattens Olive's `model.onnx[.data]` → `{name}.onnx[.data]`, but the proto's
|
| 129 |
+
`external_data.location` still pointed at `model.onnx.data` → onnxruntime failed to load any
|
| 130 |
+
model with external data (`External data path does not exist`). Affected talker fp32/fp16
|
| 131 |
+
(int4 talker is self-contained, no `.data`). Fixed: `relink_external_data()` rewrites each
|
| 132 |
+
tensor's `location` (proto-only, no multi-GB RAM load) after the move; existing dirs relinked
|
| 133 |
+
in place (cpu/cuda fp32=313 refs, fp16=255). Verified by `inference.py --selftest` loading talker.
|
| 134 |
+
|
| 135 |
+
## inference.py (manifest-driven) — full generation implemented
|
| 136 |
+
`Pipeline(onnx/{dev}_{prec}, tts_dir=...)` loads all parts with the manifest EP (CPU fallback).
|
| 137 |
+
Building blocks verified in eval_*.py: `embed_text`, `embed_codec`, `talker_step`,
|
| 138 |
+
`predict_residual`, `step_embed`, `encode`, `decode`/`decode_chunked`.
|
| 139 |
+
|
| 140 |
+
`generate(text, language, instruct=, speaker=)` now implements the real text→speech path,
|
| 141 |
+
faithful to `Qwen3TTSForConditionalGeneration.generate` with `non_streaming_mode=True`:
|
| 142 |
+
• **text-to-speech** — text + language
|
| 143 |
+
• **voice design** — text + `instruct` (natural-language style) [VoiceDesign]
|
| 144 |
+
• **custom voice** — text + `speaker` name (+ optional instruct) [CustomVoice]
|
| 145 |
+
Prefill assembly mirrors modeling lines 2068-2234 (role + codec tags + pad/bos + text body +
|
| 146 |
+
eos + codec_bos; instruct embeds prepended). AR talker loop is no-cache (re-runs the growing
|
| 147 |
+
prefix; MROPE collapses to `arange` since `get_rope_index`=cumsum(mask)-1 with 3 identical rows
|
| 148 |
+
and no padding). Each step: talker→first-codebook logits + last hidden → 15 residual codes via
|
| 149 |
+
the **causal** teacher-forced predictor (fill known codes, read `group_logits[j-1]`) → next input
|
| 150 |
+
= `residual_embed(codes16)` (=`codec_hiddens.sum(1)`) + `tts_pad`. suppress_tokens + repetition
|
| 151 |
+
penalty + top-k/top-p sampling replicated in numpy. `--text` CLI bug fixed (was `if selftest or
|
| 152 |
+
True:`; now proper branching).
|
| 153 |
+
|
| 154 |
+
**Two new exported components** were required (the old 6 couldn't roll out generation):
|
| 155 |
+
• `talker.onnx` now emits **(logits, hidden_states)** — the predictor is conditioned on the
|
| 156 |
+
talker's last hidden state, not its logits. (re-export needed)
|
| 157 |
+
• `residual_embed.onnx` — `codec_ids[B,16] → codec_hiddens.sum(1)[B,2048]`, summing
|
| 158 |
+
`talker.model.codec_embedding(code0)` + `code_predictor.model.codec_embedding[i](code_{i+1})`.
|
| 159 |
+
Those residual per-group embeddings were buried in the predictor graph; needed for the
|
| 160 |
+
next-step talker input.
|
| 161 |
+
|
| 162 |
+
**Audio-clone (ICL ref_audio/ref_text)** is a `base`-model feature: `create_voice_clone_prompt`
|
| 163 |
+
raises for non-base, and `speaker_encoder` is None for VoiceDesign/CustomVoice. Not applicable to
|
| 164 |
+
our two targets → intentionally not implemented (documented in inference.py).
|
| 165 |
+
|
| 166 |
+
Re-export to refresh a dir:
|
| 167 |
+
`uv run optimize.py --model voicedesign --skip-download --device cpu --precision fp32 \
|
| 168 |
+
--components residual_embed talker`
|
| 169 |
+
|
| 170 |
+
## Fixed — talker needed `dynamic_shapes` (dynamo ignores `dynamic_axes`)
|
| 171 |
+
The first talker re-export locked seq to the dummy's 32 (`attention_mask Got 26 Expected 32`
|
| 172 |
+
at gen step 0), breaking the AR loop. Root cause: with `use_dynamo_exporter`, Olive
|
| 173 |
+
(`conversion.py` ~L368) passes **`dynamic_shapes`** to torch.export and **drops `dynamic_axes`**.
|
| 174 |
+
Fix: added `dynamic_shapes` to the talker io_config (`{input: {axis:int → "dimname"}}`, shared
|
| 175 |
+
`"seq"` ties the 3 inputs; torch 2.12 accepts string dim names). Re-exported → talker inputs are
|
| 176 |
+
now `['batch','seq',2048]` etc. (Same root cause as the tok_decoder fixed-25-frame limitation —
|
| 177 |
+
that decoder could be re-exported with `dynamic_shapes` too if a dynamic-frame decoder is wanted.)
|
| 178 |
+
|
| 179 |
+
## ✅ Generation validated end-to-end (cpu_fp32, voicedesign)
|
| 180 |
+
`uv run inference.py --model-path onnx/customvoice/cpu_fp32 --tts-dir voicedesign \
|
| 181 |
+
--text "Hello, this is a test." --instruct "A calm female voice." --out out.wav`
|
| 182 |
+
→ AR loop runs (dynamic-seq talker → first code + hidden; 15 residuals via causal predictor;
|
| 183 |
+
`residual_embed` next-step input), decodes real audio (RMS 0.10, peak 0.64, non-silent).
|
| 184 |
+
|
| 185 |
+
**Greedy parity vs PyTorch (`eval_generate.py`, voicedesign, cpu_fp32):** tokenization identical;
|
| 186 |
+
first-codebook 27/27 frames (100%); **all-16-codebooks 432/432 = 100.00%** over 27 frames — the
|
| 187 |
+
ONNX generation reproduces `model.generate` exactly (prefill + MROPE + AR talker + causal residual
|
| 188 |
+
predictor + residual_embed + EOS). Frame count matches the reference (sampling varies length).
|
| 189 |
+
NOTE: `onnx/customvoice/cpu_fp32/` is hand-organized and actually holds **voicedesign** content
|
| 190 |
+
(manifest `model_id: .\voicedesign\`). Only this dir has the upgraded talker + residual_embed so
|
| 191 |
+
far; the other device×precision dirs still need the `residual_embed`/2-output-talker re-export.
|
| 192 |
+
|
| 193 |
+
## Known operational note — memory pressure on full-run export
|
| 194 |
+
A single `optimize.py ... --model voicedesign` (all components) can OOM-kill the process at
|
| 195 |
+
`code_predictor`: the `talker` fp16 step holds ~2.8 GB external data + a ~150 s float16 pass, and
|
| 196 |
+
stacking `code_predictor`'s float16 pass right after exhausts RAM. **No code defect** — every
|
| 197 |
+
component converts cleanly in isolation (verified: code_predictor cuda/fp16 = 354 MB, ½ of fp32).
|
| 198 |
+
Workaround: export heavy parts separately, e.g.
|
| 199 |
+
`--components talker` then `--components code_predictor text_embed codec_embed tok_encoder tok_decoder`.
|
| 200 |
+
`onnx/cuda_fp16/` now holds all 6 (text_embed 639 MB, codec_embed 12.5 MB, talker+.data,
|
| 201 |
+
code_predictor 354 MB, tok_encoder/decoder fp32). TODO(optional): make optimize.py export each
|
| 202 |
+
component in a subprocess so one full-run command can't OOM.
|
| 203 |
+
|
| 204 |
+
## Build / run
|
| 205 |
+
```
|
| 206 |
+
uv run optimize.py --model Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --device cpu --precision int4
|
| 207 |
+
uv run optimize.py --model Qwen/Qwen3-TTS-Tokenizer-12Hz --device cpu --precision fp32
|
| 208 |
+
uv run optimize.py --model voicedesign --skip-download --components tok_decoder # subset
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
## Export recipe (learned)
|
| 212 |
+
- Load all sub-models with `attn_implementation="eager"`.
|
| 213 |
+
- Codec transformer parts use `create_causal_mask` (not torch.onnx/TorchScript-traceable)
|
| 214 |
+
→ set **`use_dynamo_exporter: true`** on the OnnxConversion pass (done for codec parts).
|
| 215 |
+
- Bypass high-level wrappers with Python loops: `tok_decoder` calls `self.tok.decoder(...)`
|
| 216 |
+
directly (skips `model.decode`'s `chunked_decode` while-loop); transpose codes
|
| 217 |
+
`[B,T,16]→[B,16,T]` + clamp ≥0 first.
|
| 218 |
+
- Codes layout: `model.decode` expects `[B, codes_length, num_quantizers]` (=[B,T,16]).
|
| 219 |
+
|
| 220 |
+
## Status / next
|
| 221 |
+
- ✅ Vendored `qwen_tts` (codes/), uv-script env (tf 4.57.3) validated, model loads.
|
| 222 |
+
- ✅ `optimize.py` (uv script, model-name + device/precision dispatch) + `user_script.py`.
|
| 223 |
+
- ✅ **6 sub-models export** (text_embed, codec_embed, talker MROPE LLM, code_predictor, tok_encoder, tok_decoder).
|
| 224 |
+
- ✅ **Talker embedding primitives added + verified** (`text_embed`, `codec_embed`). The talker
|
| 225 |
+
prefill itself is control flow (variable text len, voice-clone/ICL branches, concat, MROPE
|
| 226 |
+
positions) → stays in Python (inference.py); only the *learned* lookups are in ONNX:
|
| 227 |
+
- `text_embed` = `text_projection(text_embedding(ids))` [B,T,2048] (also covers tts_bos/eos/pad
|
| 228 |
+
— specific ids); 1.28 GB. `codec_embed` = `codec_embedding(ids)` [B,T,2048]; 25 MB.
|
| 229 |
+
- Parity (`eval_embed.py`, fp32): text_embed cosine 1.000000 / max|Δ| 1.2e-7;
|
| 230 |
+
codec_embed cosine 1.000000 / max|Δ| 0. **Every learned weight now runs through ONNX.**
|
| 231 |
+
- ✅ **`code_predictor` corrected + verified** (was missing `small_to_mtp_projection` 2048→1024 +
|
| 232 |
+
codec-embedding assembly). New `CodePredictorWrapper` folds those in-graph (mirrors
|
| 233 |
+
`forward_sub_talker_finetune`); interface = `(talker_hidden[B,2048], codec_ids[B,16]) →
|
| 234 |
+
group_logits[B,15,vocab]`. Parity (`eval_predictor.py`, fp32, voicedesign):
|
| 235 |
+
wrapper-vs-native cosine 1.000000 / max|Δ| 0 / argmax 100%; ONNX-vs-wrapper cosine 1.000000 /
|
| 236 |
+
max|Δ| 5.7e-5 / argmax 100%. This is the **teacher-forced** variant (needs all 16 codes) — for
|
| 237 |
+
parity; the single-step AR variant for real generation is still TODO.
|
| 238 |
+
- ✅ **tokenizer verified** (`eval_tokenizer.py`): encoder 100% exact index match; decoder cosine
|
| 239 |
+
1.00000 vs PyTorch. Note: `tok_decoder.onnx` is **fixed at 25 frames** (dynamic frames axis did
|
| 240 |
+
not survive dynamo export) — fine for parity, but inference must chunk/pad to 25.
|
| 241 |
+
- ✅ **precision check** (`check_precision.py`): inspects weight dtypes + quant ops (size is
|
| 242 |
+
misleading — codecs are forced fp32, hence byte-identical across int4/fp16/fp32 dirs).
|
| 243 |
+
- ✅ `tok_encoder` (wav→codes) **resolved** (225 MB, fp32, dynamo) → all 4 sub-models export.
|
| 244 |
+
Mirrors `MimiModel._encode_frame` (bypasses streaming `encode()`), **plus two static-shape
|
| 245 |
+
patches in `user_script.py`** so torch.export gets concrete conv lengths:
|
| 246 |
+
1. `_patch_mimi_static_padding` — rewrites `MimiConv1d._get_extra_padding_for_conv1d` to
|
| 247 |
+
pure-Python int math (stock builds padding as 0-dim tensors → `.item()` → unbacked
|
| 248 |
+
symints → RVQ `torch.cdist`'s `npoints>25` guard can't resolve).
|
| 249 |
+
2. `_intify_mimi_convs` — converts each `MimiConv1d`'s `stride`/`kernel_size`/`padding_total`
|
| 250 |
+
buffers (0-dim int64 tensors in this model) + derived `padding_left/right` to Python ints
|
| 251 |
+
(reading a buffer in forward, even via `int()`, is a `.item()` under export).
|
| 252 |
+
Fixed input length (24000); only batch dynamic. Unblocks CustomVoice voice-clone (clones via
|
| 253 |
+
reference codes from this encoder; no separate speaker encoder for custom_voice/voice_design).
|
| 254 |
+
- Vendored patches applied for the trimmed-25Hz case: guarded `AutoConfig.register` in
|
| 255 |
+
`inference/qwen3_tts_tokenizer.py` (skip None 25Hz config; idempotent 12Hz).
|
| 256 |
+
- ⏳ `inference.py` (text→talker→code_predictor→codes→tok_decoder→wav) + `eval.py`.
|
| 257 |
+
- Other models: CustomVoice = same TTS arch; Tokenizer-12Hz = standalone codec
|
| 258 |
+
(tok_decoder works; tok_encoder shares the deferred Mimi issue).
|
check_precision.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = ["onnx", "numpy"]
|
| 4 |
+
# ///
|
| 5 |
+
"""Verify the *actual* numeric precision of exported ONNX sub-models.
|
| 6 |
+
|
| 7 |
+
File size is misleading: the codec parts (tok_encoder/tok_decoder) are forced
|
| 8 |
+
fp32 in every precision dir, so they're byte-identical across cpu_fp16 / cpu_fp32
|
| 9 |
+
/ cpu_int4. What really tells you the precision is the weight (initializer)
|
| 10 |
+
dtype histogram + presence of quant ops (MatMulNBits/DequantizeLinear for int4).
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
uv run check_precision.py onnx/cpu_int4 onnx/cpu_fp16 onnx/cpu_fp32
|
| 14 |
+
uv run check_precision.py onnx/cpu_int4/talker.onnx # single file
|
| 15 |
+
"""
|
| 16 |
+
import sys
|
| 17 |
+
from collections import Counter
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
import onnx
|
| 21 |
+
from onnx import numpy_helper, TensorProto
|
| 22 |
+
|
| 23 |
+
DT = {v: k for k, v in TensorProto.DataType.items()}
|
| 24 |
+
QUANT_OPS = {"MatMulNBits", "DequantizeLinear", "QuantizeLinear", "MatMulInteger",
|
| 25 |
+
"DynamicQuantizeLinear", "ConvInteger"}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def inspect(path: Path):
|
| 29 |
+
m = onnx.load(str(path), load_external_data=False)
|
| 30 |
+
g = m.graph
|
| 31 |
+
# initializer dtype histogram, weighted by element count
|
| 32 |
+
bytes_by_dt, count_by_dt = Counter(), Counter()
|
| 33 |
+
for init in g.initializer:
|
| 34 |
+
dt = DT.get(init.data_type, str(init.data_type))
|
| 35 |
+
n = 1
|
| 36 |
+
for d in init.dims:
|
| 37 |
+
n *= d
|
| 38 |
+
count_by_dt[dt] += n
|
| 39 |
+
bytes_by_dt[dt] += 1
|
| 40 |
+
ops = Counter(n.op_type for n in g.node)
|
| 41 |
+
quant = {op: ops[op] for op in QUANT_OPS if op in ops}
|
| 42 |
+
|
| 43 |
+
total = sum(count_by_dt.values()) or 1
|
| 44 |
+
dt_summary = ", ".join(
|
| 45 |
+
f"{dt}:{100*c/total:.1f}%" for dt, c in count_by_dt.most_common()
|
| 46 |
+
)
|
| 47 |
+
# infer label
|
| 48 |
+
if quant:
|
| 49 |
+
label = "INT4/quantized"
|
| 50 |
+
elif count_by_dt.get("FLOAT16", 0) > count_by_dt.get("FLOAT", 0):
|
| 51 |
+
label = "FP16"
|
| 52 |
+
elif count_by_dt.get("FLOAT", 0) > 0:
|
| 53 |
+
label = "FP32"
|
| 54 |
+
else:
|
| 55 |
+
label = "?"
|
| 56 |
+
print(f" {path.name:<22} -> {label}")
|
| 57 |
+
print(f" weight dtypes (by #elements): {dt_summary}")
|
| 58 |
+
if quant:
|
| 59 |
+
print(f" quant ops: {quant}")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def main():
|
| 63 |
+
targets = sys.argv[1:] or ["onnx/cpu_int4", "onnx/cpu_fp16", "onnx/cpu_fp32"]
|
| 64 |
+
for t in targets:
|
| 65 |
+
p = Path(t)
|
| 66 |
+
files = sorted(p.glob("*.onnx")) if p.is_dir() else [p]
|
| 67 |
+
print(f"\n=== {t} ===")
|
| 68 |
+
for f in files:
|
| 69 |
+
inspect(f)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
main()
|
cpu_fp16/code_predictor.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2bf685d424c0e7b36363c2139fe18c381da3188e037587f87ccfda42c2bd085f
|
| 3 |
+
size 285552428
|
cpu_fp16/codec_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7f5a774e01e8d8b7d788b4e3839e1188ce5ad3497044aeabca93dfe21ee6aec0
|
| 3 |
+
size 6291797
|
cpu_fp16/manifest.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_id": ".\\voicedesign\\",
|
| 3 |
+
"model_kind": "tts",
|
| 4 |
+
"device": "cpu",
|
| 5 |
+
"precision": "fp16",
|
| 6 |
+
"execution_provider": "CPUExecutionProvider",
|
| 7 |
+
"sub_models": {
|
| 8 |
+
"code_predictor": {
|
| 9 |
+
"filename": "code_predictor.onnx"
|
| 10 |
+
},
|
| 11 |
+
"codec_embed": {
|
| 12 |
+
"filename": "codec_embed.onnx"
|
| 13 |
+
},
|
| 14 |
+
"residual_embed": {
|
| 15 |
+
"filename": "residual_embed.onnx"
|
| 16 |
+
},
|
| 17 |
+
"talker_cache": {
|
| 18 |
+
"filename": "talker_cache.onnx"
|
| 19 |
+
},
|
| 20 |
+
"text_embed": {
|
| 21 |
+
"filename": "text_embed.onnx"
|
| 22 |
+
},
|
| 23 |
+
"tok_decoder": {
|
| 24 |
+
"filename": "tok_decoder.onnx"
|
| 25 |
+
},
|
| 26 |
+
"tok_encoder": {
|
| 27 |
+
"filename": "tok_encoder.onnx"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
cpu_fp16/residual_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:95a5d7ff95bdda5ea71a772dfb4a092313679b8a874af45801de1aa881adc7c4
|
| 3 |
+
size 69215780
|
cpu_fp16/talker_cache.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a4170b103de61c89ab78a2471c2ca19eea9f6daad7b53b869d1a22c6aa1068c3
|
| 3 |
+
size 891756744
|
cpu_fp16/text_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c1ec888fde0960d81e1a17f13c9ad737fb60693453916a49d7e78b171214204
|
| 3 |
+
size 634920759
|
cpu_fp16/tok_decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ec10051735029f6e08b04834128c9108428885c39384f443e49a6790ccb129f
|
| 3 |
+
size 458268831
|
cpu_fp16/tok_encoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a335eaf525ceef17b457f82704d3bc835d4cb15f42fc6f5908f2e860d3da400
|
| 3 |
+
size 225554101
|
cpu_fp32/code_predictor.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f2f24868183efd8b287f06f184259ad466504aea68f5ccc8724dfd2ac8a321b3
|
| 3 |
+
size 570723513
|
cpu_fp32/codec_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ef98bacf689735af788247f6da7d841ad558d06697f3ab055a81bd6daec97e96
|
| 3 |
+
size 12583206
|
cpu_fp32/manifest.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_id": ".\\voicedesign\\",
|
| 3 |
+
"model_kind": "tts",
|
| 4 |
+
"device": "cpu",
|
| 5 |
+
"precision": "fp32",
|
| 6 |
+
"execution_provider": "CPUExecutionProvider",
|
| 7 |
+
"sub_models": {
|
| 8 |
+
"code_predictor": {
|
| 9 |
+
"filename": "code_predictor.onnx"
|
| 10 |
+
},
|
| 11 |
+
"codec_embed": {
|
| 12 |
+
"filename": "codec_embed.onnx"
|
| 13 |
+
},
|
| 14 |
+
"residual_embed": {
|
| 15 |
+
"filename": "residual_embed.onnx"
|
| 16 |
+
},
|
| 17 |
+
"talker_cache": {
|
| 18 |
+
"filename": "talker_cache.onnx"
|
| 19 |
+
},
|
| 20 |
+
"text_embed": {
|
| 21 |
+
"filename": "text_embed.onnx"
|
| 22 |
+
},
|
| 23 |
+
"tok_decoder": {
|
| 24 |
+
"filename": "tok_decoder.onnx"
|
| 25 |
+
},
|
| 26 |
+
"tok_encoder": {
|
| 27 |
+
"filename": "tok_encoder.onnx"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
cpu_fp32/residual_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6e3cf497d918263835a6535cede7d181a5cc5d243825a038eda9bd3c36d112f5
|
| 3 |
+
size 138419704
|
cpu_fp32/talker_cache.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e73bd579a0e59bd52017402fe34e161ce169a112af168fdc08d3fcaf9d26b285
|
| 3 |
+
size 4551308
|
cpu_fp32/text_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8b4bae5548bbcec2f5293e35be320137115a910917dba932666bd6f25c77d9c4
|
| 3 |
+
size 1269839332
|
cpu_fp32/tok_decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ec10051735029f6e08b04834128c9108428885c39384f443e49a6790ccb129f
|
| 3 |
+
size 458268831
|
cpu_fp32/tok_encoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a335eaf525ceef17b457f82704d3bc835d4cb15f42fc6f5908f2e860d3da400
|
| 3 |
+
size 225554101
|
cpu_int4/code_predictor.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f0b3a431b90a56d2784599948bcff678d5188e38481d3a991d80c550d0d0bfa2
|
| 3 |
+
size 91668866
|
cpu_int4/codec_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bd76cd6b77907c76b6d0055ea6cf7572468ab99dc3451c1176ea2189d1a55f70
|
| 3 |
+
size 2015779
|
cpu_int4/manifest.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_id": ".\\voicedesign\\",
|
| 3 |
+
"model_kind": "tts",
|
| 4 |
+
"device": "cpu",
|
| 5 |
+
"precision": "int4",
|
| 6 |
+
"execution_provider": "CPUExecutionProvider",
|
| 7 |
+
"sub_models": {
|
| 8 |
+
"code_predictor": {
|
| 9 |
+
"filename": "code_predictor.onnx"
|
| 10 |
+
},
|
| 11 |
+
"codec_embed": {
|
| 12 |
+
"filename": "codec_embed.onnx"
|
| 13 |
+
},
|
| 14 |
+
"residual_embed": {
|
| 15 |
+
"filename": "residual_embed.onnx"
|
| 16 |
+
},
|
| 17 |
+
"talker_cache": {
|
| 18 |
+
"filename": "talker_cache.onnx"
|
| 19 |
+
},
|
| 20 |
+
"text_embed": {
|
| 21 |
+
"filename": "text_embed.onnx"
|
| 22 |
+
},
|
| 23 |
+
"tok_decoder": {
|
| 24 |
+
"filename": "tok_decoder.onnx"
|
| 25 |
+
},
|
| 26 |
+
"tok_encoder": {
|
| 27 |
+
"filename": "tok_encoder.onnx"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
cpu_int4/residual_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eec76513b562f558e76eeeb9b0bfa982de512104cddac372ec368ab504b080a0
|
| 3 |
+
size 22179258
|
cpu_int4/talker_cache.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:910e1d556cd74f11bbb55fff9d509a962bfc43f23608b62fb0a8215c7acc882f
|
| 3 |
+
size 288573853
|
cpu_int4/text_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b2923edfce01344dea9bd719e2daf94bda0b45bfd05a307514e9dd5f793dcd09
|
| 3 |
+
size 203384915
|
cpu_int4/tok_decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ec10051735029f6e08b04834128c9108428885c39384f443e49a6790ccb129f
|
| 3 |
+
size 458268831
|
cpu_int4/tok_encoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a335eaf525ceef17b457f82704d3bc835d4cb15f42fc6f5908f2e860d3da400
|
| 3 |
+
size 225554101
|
cuda_fp16/codec_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7f5a774e01e8d8b7d788b4e3839e1188ce5ad3497044aeabca93dfe21ee6aec0
|
| 3 |
+
size 6291797
|
cuda_fp16/manifest.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_id": ".\\voicedesign\\",
|
| 3 |
+
"model_kind": "tts",
|
| 4 |
+
"device": "cuda",
|
| 5 |
+
"precision": "fp16",
|
| 6 |
+
"execution_provider": "CUDAExecutionProvider",
|
| 7 |
+
"sub_models": {
|
| 8 |
+
"code_predictor": {
|
| 9 |
+
"filename": "code_predictor.onnx"
|
| 10 |
+
},
|
| 11 |
+
"codec_embed": {
|
| 12 |
+
"filename": "codec_embed.onnx"
|
| 13 |
+
},
|
| 14 |
+
"residual_embed": {
|
| 15 |
+
"filename": "residual_embed.onnx"
|
| 16 |
+
},
|
| 17 |
+
"talker_cache": {
|
| 18 |
+
"filename": "talker_cache.onnx"
|
| 19 |
+
},
|
| 20 |
+
"text_embed": {
|
| 21 |
+
"filename": "text_embed.onnx"
|
| 22 |
+
},
|
| 23 |
+
"tok_decoder": {
|
| 24 |
+
"filename": "tok_decoder.onnx"
|
| 25 |
+
},
|
| 26 |
+
"tok_encoder": {
|
| 27 |
+
"filename": "tok_encoder.onnx"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
cuda_fp16/residual_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:95a5d7ff95bdda5ea71a772dfb4a092313679b8a874af45801de1aa881adc7c4
|
| 3 |
+
size 69215780
|
cuda_fp32/manifest.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_id": ".\\voicedesign\\",
|
| 3 |
+
"model_kind": "tts",
|
| 4 |
+
"device": "cuda",
|
| 5 |
+
"precision": "fp32",
|
| 6 |
+
"execution_provider": "CUDAExecutionProvider",
|
| 7 |
+
"sub_models": {
|
| 8 |
+
"code_predictor": {
|
| 9 |
+
"filename": "code_predictor.onnx"
|
| 10 |
+
},
|
| 11 |
+
"codec_embed": {
|
| 12 |
+
"filename": "codec_embed.onnx"
|
| 13 |
+
},
|
| 14 |
+
"residual_embed": {
|
| 15 |
+
"filename": "residual_embed.onnx"
|
| 16 |
+
},
|
| 17 |
+
"talker_cache": {
|
| 18 |
+
"filename": "talker_cache.onnx"
|
| 19 |
+
},
|
| 20 |
+
"text_embed": {
|
| 21 |
+
"filename": "text_embed.onnx"
|
| 22 |
+
},
|
| 23 |
+
"tok_decoder": {
|
| 24 |
+
"filename": "tok_decoder.onnx"
|
| 25 |
+
},
|
| 26 |
+
"tok_encoder": {
|
| 27 |
+
"filename": "tok_encoder.onnx"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
cuda_fp32/talker_cache.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:389276b5ef745a38a5d9a5a4e5a39746ed590643f9cd3571eb8d6125c93bfd6e
|
| 3 |
+
size 1774452736
|
cuda_fp32/text_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8b4bae5548bbcec2f5293e35be320137115a910917dba932666bd6f25c77d9c4
|
| 3 |
+
size 1269839332
|
cuda_int4/manifest.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_id": ".\\voicedesign\\",
|
| 3 |
+
"model_kind": "tts",
|
| 4 |
+
"device": "cuda",
|
| 5 |
+
"precision": "int4",
|
| 6 |
+
"execution_provider": "CUDAExecutionProvider",
|
| 7 |
+
"sub_models": {
|
| 8 |
+
"code_predictor": {
|
| 9 |
+
"filename": "code_predictor.onnx"
|
| 10 |
+
},
|
| 11 |
+
"codec_embed": {
|
| 12 |
+
"filename": "codec_embed.onnx"
|
| 13 |
+
},
|
| 14 |
+
"residual_embed": {
|
| 15 |
+
"filename": "residual_embed.onnx"
|
| 16 |
+
},
|
| 17 |
+
"talker_cache": {
|
| 18 |
+
"filename": "talker_cache.onnx"
|
| 19 |
+
},
|
| 20 |
+
"text_embed": {
|
| 21 |
+
"filename": "text_embed.onnx"
|
| 22 |
+
},
|
| 23 |
+
"tok_decoder": {
|
| 24 |
+
"filename": "tok_decoder.onnx"
|
| 25 |
+
},
|
| 26 |
+
"tok_encoder": {
|
| 27 |
+
"filename": "tok_encoder.onnx"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
cuda_int4/talker_cache.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:910e1d556cd74f11bbb55fff9d509a962bfc43f23608b62fb0a8215c7acc882f
|
| 3 |
+
size 288573853
|
cuda_int4/text_embed.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b2923edfce01344dea9bd719e2daf94bda0b45bfd05a307514e9dd5f793dcd09
|
| 3 |
+
size 203384915
|
cuda_int4/tok_decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ec10051735029f6e08b04834128c9108428885c39384f443e49a6790ccb129f
|
| 3 |
+
size 458268831
|
cuda_int4/tok_encoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a335eaf525ceef17b457f82704d3bc835d4cb15f42fc6f5908f2e860d3da400
|
| 3 |
+
size 225554101
|
eval_cache.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "onnxruntime>=1.20", "numpy", "soundfile", "librosa", "transformers",
|
| 5 |
+
# "numba>=0.60.0", "llvmlite>=0.43.0",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""Parity + speed: KV-cache talker loop vs no-cache loop. Greedy → must match exactly.
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
uv run eval_cache.py --model-path onnx/voicedesign/cpu_int4 --tts-dir voicedesign
|
| 12 |
+
"""
|
| 13 |
+
import argparse, sys, time
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
HERE = Path(__file__).parent
|
| 18 |
+
sys.path.insert(0, str(HERE))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
ap = argparse.ArgumentParser()
|
| 23 |
+
ap.add_argument("--model-path", required=True)
|
| 24 |
+
ap.add_argument("--tts-dir", required=True)
|
| 25 |
+
ap.add_argument("--text", default="Hello, this is a KV cache parity test.")
|
| 26 |
+
ap.add_argument("--max-new-tokens", type=int, default=40)
|
| 27 |
+
args = ap.parse_args()
|
| 28 |
+
|
| 29 |
+
from inference import Pipeline
|
| 30 |
+
pipe = Pipeline(args.model_path, tts_dir=args.tts_dir)
|
| 31 |
+
if pipe.talker_cache is None:
|
| 32 |
+
print("no talker_cache.onnx in this dir — nothing to compare"); return
|
| 33 |
+
|
| 34 |
+
kw = dict(language="Auto", max_new_tokens=args.max_new_tokens, do_sample=False,
|
| 35 |
+
sub_do_sample=False, seed=0, verbose=False)
|
| 36 |
+
t0 = time.time(); codes_cached = pipe.generate(args.text, **kw); t_cached = time.time() - t0
|
| 37 |
+
pipe.talker_cache = None # force no-cache path
|
| 38 |
+
t0 = time.time(); codes_nocache = pipe.generate(args.text, **kw); t_nocache = time.time() - t0
|
| 39 |
+
|
| 40 |
+
n = min(len(codes_cached), len(codes_nocache))
|
| 41 |
+
match = float((codes_cached[:n] == codes_nocache[:n]).mean()) if n else -1
|
| 42 |
+
print(f"cached frames={len(codes_cached)} ({t_cached:.1f}s) "
|
| 43 |
+
f"no-cache frames={len(codes_nocache)} ({t_nocache:.1f}s) "
|
| 44 |
+
f"speedup={t_nocache/max(t_cached,1e-9):.2f}x")
|
| 45 |
+
print(f"all-16-codes exact match over {n} frames: {match*100:.2f}%")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
main()
|
eval_embed.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "transformers==4.57.3", "torch", "torchvision", "torchaudio",
|
| 5 |
+
# "onnx", "onnxruntime>=1.20", "numpy", "safetensors",
|
| 6 |
+
# "huggingface_hub", "accelerate", "librosa", "soundfile",
|
| 7 |
+
# ]
|
| 8 |
+
# ///
|
| 9 |
+
"""Parity for text_embed / codec_embed ONNX vs the PyTorch wrappers.
|
| 10 |
+
|
| 11 |
+
Usage:
|
| 12 |
+
uv run eval_embed.py --model-path onnx/cpu_fp32 --tts-path voicedesign
|
| 13 |
+
"""
|
| 14 |
+
import argparse, json, sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
HERE = Path(__file__).parent
|
| 19 |
+
sys.path.insert(0, str(HERE)); sys.path.insert(0, str(HERE / "codes"))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def cosine(a, b):
|
| 23 |
+
a, b = a.ravel().astype(np.float64), b.ravel().astype(np.float64)
|
| 24 |
+
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def main():
|
| 28 |
+
ap = argparse.ArgumentParser()
|
| 29 |
+
ap.add_argument("--model-path", required=True)
|
| 30 |
+
ap.add_argument("--tts-path", default="voicedesign")
|
| 31 |
+
args = ap.parse_args()
|
| 32 |
+
|
| 33 |
+
import torch, onnxruntime as ort
|
| 34 |
+
from user_script import (get_text_embed_model, get_codec_embed_model, _tts_dims, _load_tts)
|
| 35 |
+
|
| 36 |
+
mdir = Path(args.model_path)
|
| 37 |
+
sub = json.loads((mdir / "manifest.json").read_text())["sub_models"]
|
| 38 |
+
dims = _tts_dims(_load_tts(args.tts_path))
|
| 39 |
+
|
| 40 |
+
for name, get_model, lo, hi, in_name, out_name in [
|
| 41 |
+
("text_embed", get_text_embed_model, 0, 1000, "text_ids", "text_embeds"),
|
| 42 |
+
("codec_embed", get_codec_embed_model, 0, dims["codec_vocab"], "codec_ids", "codec_embeds"),
|
| 43 |
+
]:
|
| 44 |
+
sess = ort.InferenceSession(str(mdir / sub[name]["filename"]),
|
| 45 |
+
providers=["CPUExecutionProvider"])
|
| 46 |
+
wrap = get_model(args.tts_path)
|
| 47 |
+
ids = torch.randint(lo, hi, (1, 16), dtype=torch.int64)
|
| 48 |
+
with torch.no_grad():
|
| 49 |
+
ref = wrap(ids).numpy()
|
| 50 |
+
got = sess.run(None, {in_name: ids.numpy()})[0]
|
| 51 |
+
print(f"=== {name} === onnx={got.shape} ref={ref.shape} "
|
| 52 |
+
f"cosine={cosine(got, ref):.6f} max|Δ|={np.abs(got-ref).max():.3e}")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
main()
|
eval_generate.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "transformers==4.57.3", "torch", "torchvision", "torchaudio",
|
| 5 |
+
# "onnx", "onnxruntime>=1.20", "numpy", "safetensors",
|
| 6 |
+
# "huggingface_hub", "accelerate", "librosa", "soundfile",
|
| 7 |
+
# ]
|
| 8 |
+
# ///
|
| 9 |
+
"""Parity: ONNX inference.py generate() vs the PyTorch reference (greedy).
|
| 10 |
+
|
| 11 |
+
Compares, on identical text/instruct, greedy (do_sample=False):
|
| 12 |
+
(1) tokenization — reference processor ids vs inference.py AutoTokenizer ids
|
| 13 |
+
(2) first-codebook code sequence — reference talker codes vs ONNX codes
|
| 14 |
+
(frame count, first divergence step, % match up to min length)
|
| 15 |
+
|
| 16 |
+
Greedy isolates correctness from sampling RNG. Small fp diffs can still flip an
|
| 17 |
+
argmax and diverge late; report first-divergence step so a late split (good
|
| 18 |
+
prefill) is distinguishable from step-0 (broken prefill).
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
uv run eval_generate.py --model-path onnx/customvoice/cpu_fp32 --tts-dir voicedesign \
|
| 22 |
+
--text "Hello, this is a test." --instruct "A calm female voice." --max-new-tokens 60
|
| 23 |
+
"""
|
| 24 |
+
import argparse, sys
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
import numpy as np
|
| 27 |
+
|
| 28 |
+
HERE = Path(__file__).parent
|
| 29 |
+
sys.path.insert(0, str(HERE)); sys.path.insert(0, str(HERE / "codes"))
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
ap = argparse.ArgumentParser()
|
| 34 |
+
ap.add_argument("--model-path", required=True)
|
| 35 |
+
ap.add_argument("--tts-dir", default="voicedesign")
|
| 36 |
+
ap.add_argument("--text", default="Hello, this is a test.")
|
| 37 |
+
ap.add_argument("--instruct", default="A calm female voice.")
|
| 38 |
+
ap.add_argument("--language", default="Auto")
|
| 39 |
+
ap.add_argument("--max-new-tokens", type=int, default=60)
|
| 40 |
+
args = ap.parse_args()
|
| 41 |
+
|
| 42 |
+
import torch
|
| 43 |
+
from qwen_tts.inference.qwen3_tts_model import Qwen3TTSModel
|
| 44 |
+
from inference import Pipeline
|
| 45 |
+
|
| 46 |
+
# ── reference (PyTorch) ──────────────────────────────────────────────────
|
| 47 |
+
ref = Qwen3TTSModel.from_pretrained(args.tts_dir)
|
| 48 |
+
assistant = f"<|im_start|>assistant\n{args.text}<|im_end|>\n<|im_start|>assistant\n"
|
| 49 |
+
ref_ids = ref._tokenize_texts([assistant])[0] # [1,1,L] or [1,L]
|
| 50 |
+
ref_ids_flat = ref_ids.reshape(-1).tolist()
|
| 51 |
+
|
| 52 |
+
input_ids = ref._tokenize_texts([ref._build_assistant_text(args.text)])
|
| 53 |
+
instruct_ids = [ref._tokenize_texts([ref._build_instruct_text(args.instruct)])[0]]
|
| 54 |
+
with torch.no_grad():
|
| 55 |
+
codes_list, _ = ref.model.generate(
|
| 56 |
+
input_ids=input_ids, instruct_ids=instruct_ids, languages=[args.language],
|
| 57 |
+
non_streaming_mode=True, do_sample=False, subtalker_dosample=False,
|
| 58 |
+
max_new_tokens=args.max_new_tokens, output_hidden_states=True,
|
| 59 |
+
return_dict_in_generate=True)
|
| 60 |
+
ref_codes = codes_list[0].cpu().numpy().astype(np.int64) # [T,16]
|
| 61 |
+
ref_first = ref_codes[:, 0]
|
| 62 |
+
|
| 63 |
+
# ── ONNX (inference.py) ──────────────────────────────────────────────────
|
| 64 |
+
pipe = Pipeline(args.model_path, tts_dir=args.tts_dir)
|
| 65 |
+
onnx_ids = pipe._ids(assistant).reshape(-1).tolist()
|
| 66 |
+
onnx_codes = pipe.generate(
|
| 67 |
+
args.text, language=args.language, instruct=args.instruct,
|
| 68 |
+
max_new_tokens=args.max_new_tokens, do_sample=False, sub_do_sample=False,
|
| 69 |
+
verbose=False)
|
| 70 |
+
onnx_first = onnx_codes[:, 0]
|
| 71 |
+
|
| 72 |
+
# ── report ───────────────────────────────────────────────────────────────
|
| 73 |
+
print("\n=== tokenization ===")
|
| 74 |
+
print(f" ref ids ({len(ref_ids_flat)}): {ref_ids_flat}")
|
| 75 |
+
print(f" onnx ids ({len(onnx_ids)}): {onnx_ids}")
|
| 76 |
+
print(f" MATCH: {ref_ids_flat == onnx_ids}")
|
| 77 |
+
|
| 78 |
+
print("\n=== first-codebook codes (greedy) ===")
|
| 79 |
+
print(f" ref frames={len(ref_first)} onnx frames={len(onnx_first)}")
|
| 80 |
+
n = min(len(ref_first), len(onnx_first))
|
| 81 |
+
if n:
|
| 82 |
+
eq = ref_first[:n] == onnx_first[:n]
|
| 83 |
+
div = int(np.argmax(~eq)) if not eq.all() else n
|
| 84 |
+
print(f" match up to min-len: {int(eq.sum())}/{n} ({100*eq.mean():.1f}%)")
|
| 85 |
+
print(f" first divergence step: {div}{' (none)' if div==n else ''}")
|
| 86 |
+
print(f" ref [:12]: {ref_first[:12].tolist()}")
|
| 87 |
+
print(f" onnx[:12]: {onnx_first[:12].tolist()}")
|
| 88 |
+
|
| 89 |
+
print("\n=== ALL 16 codebooks (greedy) ===")
|
| 90 |
+
m = min(len(ref_codes), len(onnx_codes))
|
| 91 |
+
if m:
|
| 92 |
+
full_eq = (ref_codes[:m] == onnx_codes[:m])
|
| 93 |
+
print(f" full-grid match: {int(full_eq.sum())}/{full_eq.size} "
|
| 94 |
+
f"({100*full_eq.mean():.2f}%) over {m} frames × 16 groups")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
main()
|
eval_predictor.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "transformers==4.57.3", "torch", "torchvision", "torchaudio",
|
| 5 |
+
# "onnx", "onnxruntime>=1.20", "numpy", "safetensors",
|
| 6 |
+
# "huggingface_hub", "accelerate", "librosa", "soundfile",
|
| 7 |
+
# ]
|
| 8 |
+
# ///
|
| 9 |
+
"""Parity for the teacher-forced code_predictor ONNX.
|
| 10 |
+
|
| 11 |
+
Two checks:
|
| 12 |
+
(1) wrapper vs model.forward_sub_talker_finetune — is the in-graph wrapper
|
| 13 |
+
faithful to the original PyTorch composition?
|
| 14 |
+
(2) ONNX vs wrapper — did export preserve it?
|
| 15 |
+
|
| 16 |
+
Usage:
|
| 17 |
+
uv run eval_predictor.py --model-path onnx/cpu_fp32 --tts-path voicedesign
|
| 18 |
+
"""
|
| 19 |
+
import argparse, json, sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
HERE = Path(__file__).parent
|
| 24 |
+
sys.path.insert(0, str(HERE)); sys.path.insert(0, str(HERE / "codes"))
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def cosine(a, b):
|
| 28 |
+
a, b = a.ravel().astype(np.float64), b.ravel().astype(np.float64)
|
| 29 |
+
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
ap = argparse.ArgumentParser()
|
| 34 |
+
ap.add_argument("--model-path", required=True)
|
| 35 |
+
ap.add_argument("--tts-path", default="voicedesign")
|
| 36 |
+
args = ap.parse_args()
|
| 37 |
+
|
| 38 |
+
import torch, onnxruntime as ort
|
| 39 |
+
from user_script import get_code_predictor_model, _load_tts
|
| 40 |
+
|
| 41 |
+
mdir = Path(args.model_path)
|
| 42 |
+
fn = json.loads((mdir / "manifest.json").read_text())["sub_models"]["code_predictor"]["filename"]
|
| 43 |
+
sess = ort.InferenceSession(str(mdir / fn), providers=["CPUExecutionProvider"])
|
| 44 |
+
|
| 45 |
+
wrap = get_code_predictor_model(args.tts_path)
|
| 46 |
+
talker = _load_tts(args.tts_path).talker
|
| 47 |
+
ng = wrap.n_groups
|
| 48 |
+
|
| 49 |
+
torch.manual_seed(0)
|
| 50 |
+
hidden = torch.randn(1, 2048)
|
| 51 |
+
codes = torch.randint(0, 2048, (1, ng), dtype=torch.int64)
|
| 52 |
+
|
| 53 |
+
with torch.no_grad():
|
| 54 |
+
wrap_logits = wrap(hidden, codes).numpy()
|
| 55 |
+
native_logits, _ = talker.forward_sub_talker_finetune(codes, hidden) # [B,15,vocab]
|
| 56 |
+
native_logits = native_logits.numpy()
|
| 57 |
+
onnx_logits = sess.run(None, {"talker_hidden": hidden.numpy(),
|
| 58 |
+
"codec_ids": codes.numpy()})[0]
|
| 59 |
+
|
| 60 |
+
print("=== (1) wrapper vs native forward_sub_talker_finetune ===")
|
| 61 |
+
print(f" shapes wrap={wrap_logits.shape} native={native_logits.shape}")
|
| 62 |
+
print(f" cosine={cosine(wrap_logits, native_logits):.6f} "
|
| 63 |
+
f"max|Δ|={np.abs(wrap_logits-native_logits).max():.3e} "
|
| 64 |
+
f"argmax agree={(wrap_logits.argmax(-1)==native_logits.argmax(-1)).mean():.3%}")
|
| 65 |
+
|
| 66 |
+
print("=== (2) ONNX vs wrapper ===")
|
| 67 |
+
print(f" shapes onnx={onnx_logits.shape}")
|
| 68 |
+
print(f" cosine={cosine(onnx_logits, wrap_logits):.6f} "
|
| 69 |
+
f"max|Δ|={np.abs(onnx_logits-wrap_logits).max():.3e} "
|
| 70 |
+
f"argmax agree={(onnx_logits.argmax(-1)==wrap_logits.argmax(-1)).mean():.3%}")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
main()
|
eval_speaker.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "transformers==4.57.3", "torch", "torchvision", "torchaudio",
|
| 5 |
+
# "onnx", "onnxruntime>=1.20", "numpy", "safetensors",
|
| 6 |
+
# "huggingface_hub", "accelerate", "librosa", "soundfile",
|
| 7 |
+
# "numba>=0.60.0", "llvmlite>=0.43.0",
|
| 8 |
+
# ]
|
| 9 |
+
# ///
|
| 10 |
+
"""Parity: speaker_encoder.onnx (x-vector, with reimplemented mel/STFT) vs PyTorch
|
| 11 |
+
extract_speaker_embedding. Validates the one genuinely-new ONNX component for Base
|
| 12 |
+
voice cloning across several audio lengths.
|
| 13 |
+
|
| 14 |
+
Usage:
|
| 15 |
+
uv run eval_speaker.py --tts-dir base/1.7B --onnx onnx/base17_cpu_int4/speaker_encoder.onnx
|
| 16 |
+
"""
|
| 17 |
+
import argparse, sys
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
HERE = Path(__file__).parent
|
| 22 |
+
sys.path.insert(0, str(HERE)); sys.path.insert(0, str(HERE / "codes"))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def cosine(a, b):
|
| 26 |
+
a, b = a.ravel().astype(np.float64), b.ravel().astype(np.float64)
|
| 27 |
+
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main():
|
| 31 |
+
ap = argparse.ArgumentParser()
|
| 32 |
+
ap.add_argument("--tts-dir", default="base/1.7B")
|
| 33 |
+
ap.add_argument("--onnx", default="onnx/base17_cpu_int4/speaker_encoder.onnx")
|
| 34 |
+
args = ap.parse_args()
|
| 35 |
+
|
| 36 |
+
import torch, onnxruntime as ort
|
| 37 |
+
from user_script import _load_tts
|
| 38 |
+
|
| 39 |
+
model = _load_tts(args.tts_dir)
|
| 40 |
+
assert model.speaker_encoder is not None, "not a base checkpoint"
|
| 41 |
+
sess = ort.InferenceSession(args.onnx, providers=["CPUExecutionProvider"])
|
| 42 |
+
rng = np.random.default_rng(0)
|
| 43 |
+
|
| 44 |
+
for secs in (2.0, 3.5, 6.0):
|
| 45 |
+
n = int(24000 * secs)
|
| 46 |
+
audio = (0.5 * rng.standard_normal(n)).astype(np.float32)
|
| 47 |
+
with torch.no_grad():
|
| 48 |
+
ref = model.extract_speaker_embedding(audio, 24000).cpu().numpy().ravel()
|
| 49 |
+
got = sess.run(None, {"audio": audio[None]})[0].ravel()
|
| 50 |
+
print(f"len={secs:>4}s onnx{got.shape} ref{ref.shape} "
|
| 51 |
+
f"cosine={cosine(got, ref):.6f} max|Δ|={np.abs(got - ref).max():.3e}")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
main()
|
eval_tokenizer.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "transformers==4.57.3",
|
| 5 |
+
# "torch",
|
| 6 |
+
# "torchvision",
|
| 7 |
+
# "torchaudio",
|
| 8 |
+
# "onnx",
|
| 9 |
+
# "onnxruntime>=1.20",
|
| 10 |
+
# "numpy",
|
| 11 |
+
# "safetensors",
|
| 12 |
+
# "huggingface_hub",
|
| 13 |
+
# "accelerate",
|
| 14 |
+
# "librosa",
|
| 15 |
+
# "soundfile",
|
| 16 |
+
# ]
|
| 17 |
+
# ///
|
| 18 |
+
"""Evaluate the exported Qwen3-TTS tokenizer ONNX vs the original PyTorch.
|
| 19 |
+
|
| 20 |
+
Reference = the exact `TokEncoderWrapper` / `TokDecoderWrapper` forwards from
|
| 21 |
+
user_script.py (what optimize.py exported), loaded in fp32 from the tokenizer
|
| 22 |
+
checkpoint. Pins transformers==4.57.3 via PEP-723, like optimize.py.
|
| 23 |
+
|
| 24 |
+
Checks
|
| 25 |
+
------
|
| 26 |
+
(a) Encoder parity : audio[1,1,24000] → codes[1,frames,16]; exact index match %
|
| 27 |
+
(ONNX vs PyTorch) + per-codebook agreement.
|
| 28 |
+
(b) Decoder parity : codes → waveform; cosine + max|Δ| (ONNX vs PyTorch).
|
| 29 |
+
(c) Round-trip : encode→decode entirely in ONNX; reconstruction vs input
|
| 30 |
+
(and vs the PyTorch round-trip) — cosine / max|Δ|.
|
| 31 |
+
|
| 32 |
+
Usage:
|
| 33 |
+
uv run eval_tokenizer.py --model-path onnx/cpu_fp16
|
| 34 |
+
uv run eval_tokenizer.py --model-path onnx/cpu_fp16 --tok-path tokenizer --save-wav
|
| 35 |
+
"""
|
| 36 |
+
import argparse
|
| 37 |
+
import json
|
| 38 |
+
import os
|
| 39 |
+
import sys
|
| 40 |
+
from pathlib import Path
|
| 41 |
+
|
| 42 |
+
import numpy as np
|
| 43 |
+
|
| 44 |
+
HERE = Path(__file__).parent
|
| 45 |
+
sys.path.insert(0, str(HERE))
|
| 46 |
+
sys.path.insert(0, str(HERE / "codes"))
|
| 47 |
+
|
| 48 |
+
SR = 24000
|
| 49 |
+
N = 24000 # encoder is exported with a static 1-second input
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def cosine(a, b):
|
| 53 |
+
a, b = a.ravel().astype(np.float64), b.ravel().astype(np.float64)
|
| 54 |
+
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_signal(seed=0):
|
| 58 |
+
"""Deterministic 1 s @ 24 kHz mix of sweeps + harmonics (speech-band-ish)."""
|
| 59 |
+
rng = np.random.default_rng(seed)
|
| 60 |
+
t = np.arange(N) / SR
|
| 61 |
+
sweep = np.sin(2 * np.pi * (120 + 400 * t) * t)
|
| 62 |
+
harm = 0.4 * np.sin(2 * np.pi * 220 * t) + 0.2 * np.sin(2 * np.pi * 440 * t)
|
| 63 |
+
env = np.clip(np.sin(2 * np.pi * 2.5 * t), 0, 1) # syllable-like envelope
|
| 64 |
+
sig = (sweep + harm) * (0.5 + 0.5 * env) + 0.01 * rng.standard_normal(N)
|
| 65 |
+
sig = sig / (np.abs(sig).max() + 1e-6) * 0.95
|
| 66 |
+
return sig.astype(np.float32)[None, None, :] # [1, 1, N]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def main():
|
| 70 |
+
ap = argparse.ArgumentParser(description="Qwen3-TTS tokenizer ONNX vs PyTorch")
|
| 71 |
+
ap.add_argument("--model-path", required=True, help="onnx/{device}_{precision} dir")
|
| 72 |
+
ap.add_argument("--tok-path", default="tokenizer", help="PyTorch tokenizer checkpoint dir")
|
| 73 |
+
ap.add_argument("--save-wav", action="store_true", help="dump input + reconstructions")
|
| 74 |
+
args = ap.parse_args()
|
| 75 |
+
|
| 76 |
+
import torch
|
| 77 |
+
import onnxruntime as ort
|
| 78 |
+
from user_script import get_tok_encoder_model, get_tok_decoder_model
|
| 79 |
+
|
| 80 |
+
mdir = Path(args.model_path)
|
| 81 |
+
manifest = json.loads((mdir / "manifest.json").read_text())
|
| 82 |
+
enc_onnx = mdir / manifest["sub_models"]["tok_encoder"]["filename"]
|
| 83 |
+
dec_onnx = mdir / manifest["sub_models"]["tok_decoder"]["filename"]
|
| 84 |
+
print(f"ONNX dir : {mdir}")
|
| 85 |
+
print(f" encoder : {enc_onnx.name}")
|
| 86 |
+
print(f" decoder : {dec_onnx.name}")
|
| 87 |
+
print(f"PyTorch ref: {args.tok_path}\n")
|
| 88 |
+
|
| 89 |
+
so = ort.SessionOptions()
|
| 90 |
+
so.log_severity_level = 3
|
| 91 |
+
enc_sess = ort.InferenceSession(str(enc_onnx), so, providers=["CPUExecutionProvider"])
|
| 92 |
+
dec_sess = ort.InferenceSession(str(dec_onnx), so, providers=["CPUExecutionProvider"])
|
| 93 |
+
|
| 94 |
+
dec_shape = dec_sess.get_inputs()[0].shape # [batch, frames, 16]
|
| 95 |
+
dec_frames = dec_shape[1] if isinstance(dec_shape[1], int) else None
|
| 96 |
+
print(f" decoder input shape: {dec_shape}"
|
| 97 |
+
+ (" (frames FIXED — see note)" if dec_frames else " (frames dynamic)"))
|
| 98 |
+
|
| 99 |
+
def fit_frames(codes, F):
|
| 100 |
+
"""Tile/trim codes [1,T,16] to F frames so a fixed-length decoder accepts them."""
|
| 101 |
+
if F is None or codes.shape[1] == F:
|
| 102 |
+
return codes
|
| 103 |
+
T = codes.shape[1]
|
| 104 |
+
idx = np.arange(F) % T
|
| 105 |
+
return codes[:, idx, :]
|
| 106 |
+
|
| 107 |
+
print("Loading PyTorch reference wrappers (fp32) ...")
|
| 108 |
+
enc_pt = get_tok_encoder_model(args.tok_path)
|
| 109 |
+
dec_pt = get_tok_decoder_model(args.tok_path)
|
| 110 |
+
|
| 111 |
+
audio = test_signal()
|
| 112 |
+
|
| 113 |
+
# ── (a) Encoder parity ──────────────────────────────────────────────────
|
| 114 |
+
print("\n=== (a) Encoder parity audio[1,1,24000] → codes[1,T,16] ===")
|
| 115 |
+
codes_onnx = enc_sess.run(None, {"audio": audio})[0]
|
| 116 |
+
with torch.no_grad():
|
| 117 |
+
codes_pt = enc_pt(torch.from_numpy(audio)).cpu().numpy()
|
| 118 |
+
codes_onnx = np.asarray(codes_onnx).astype(np.int64)
|
| 119 |
+
codes_pt = codes_pt.astype(np.int64)
|
| 120 |
+
print(f" shapes onnx={codes_onnx.shape} pytorch={codes_pt.shape}")
|
| 121 |
+
if codes_onnx.shape == codes_pt.shape:
|
| 122 |
+
match = float((codes_onnx == codes_pt).mean())
|
| 123 |
+
print(f" exact index match : {match:.4%}")
|
| 124 |
+
per_cb = (codes_onnx == codes_pt).mean(axis=(0, 1)) # [16]
|
| 125 |
+
worst = int(np.argmin(per_cb))
|
| 126 |
+
print(f" per-codebook match: min={per_cb.min():.3f} (cb{worst}) "
|
| 127 |
+
f"mean={per_cb.mean():.3f} max={per_cb.max():.3f}")
|
| 128 |
+
enc_ok = match > 0.99
|
| 129 |
+
else:
|
| 130 |
+
print(" SHAPE MISMATCH — cannot compare indices")
|
| 131 |
+
enc_ok = False
|
| 132 |
+
|
| 133 |
+
# ── (b) Decoder parity ──────────────────────────────────────────────────
|
| 134 |
+
print("\n=== (b) Decoder parity codes → waveform ===")
|
| 135 |
+
codes_in = fit_frames(codes_pt, dec_frames) # same codes into both decoders
|
| 136 |
+
if dec_frames and codes_in.shape[1] != codes_pt.shape[1]:
|
| 137 |
+
print(f" (decoder is fixed at {dec_frames} frames; tiled {codes_pt.shape[1]}→{dec_frames})")
|
| 138 |
+
wav_onnx = dec_sess.run(None, {"audio_codes": codes_in})[0]
|
| 139 |
+
with torch.no_grad():
|
| 140 |
+
wav_pt = dec_pt(torch.from_numpy(codes_in)).cpu().numpy()
|
| 141 |
+
n = min(wav_onnx.shape[-1], wav_pt.shape[-1])
|
| 142 |
+
c = cosine(wav_onnx[..., :n], wav_pt[..., :n])
|
| 143 |
+
d = float(np.abs(wav_onnx[..., :n] - wav_pt[..., :n]).max())
|
| 144 |
+
print(f" shapes onnx={wav_onnx.shape} pytorch={wav_pt.shape}")
|
| 145 |
+
print(f" cosine={c:.5f} max|Δ|={d:.4e}")
|
| 146 |
+
dec_ok = c > 0.999
|
| 147 |
+
|
| 148 |
+
# ── (c) Round-trip (ONNX encode→decode) ─────────────────────────────────
|
| 149 |
+
print("\n=== (c) Round-trip ONNX encode→decode vs input / PyTorch ===")
|
| 150 |
+
codes_rt = fit_frames(codes_onnx, dec_frames)
|
| 151 |
+
wav_rt_onnx = dec_sess.run(None, {"audio_codes": codes_rt})[0]
|
| 152 |
+
with torch.no_grad():
|
| 153 |
+
wav_rt_pt = dec_pt(torch.from_numpy(codes_rt)).cpu().numpy()
|
| 154 |
+
m = min(wav_rt_onnx.shape[-1], audio.shape[-1])
|
| 155 |
+
print(f" recon vs input : cosine={cosine(wav_rt_onnx[..., :m], audio[..., :m]):.4f}")
|
| 156 |
+
k = min(wav_rt_onnx.shape[-1], wav_rt_pt.shape[-1])
|
| 157 |
+
print(f" ONNX vs PyTorch RT: cosine={cosine(wav_rt_onnx[..., :k], wav_rt_pt[..., :k]):.5f} "
|
| 158 |
+
f"max|Δ|={float(np.abs(wav_rt_onnx[..., :k] - wav_rt_pt[..., :k]).max()):.4e}")
|
| 159 |
+
|
| 160 |
+
if args.save_wav:
|
| 161 |
+
import soundfile as sf
|
| 162 |
+
sf.write(mdir / "eval_input.wav", audio[0, 0], SR)
|
| 163 |
+
sf.write(mdir / "eval_recon_onnx.wav", wav_rt_onnx.reshape(-1), SR)
|
| 164 |
+
sf.write(mdir / "eval_recon_pytorch.wav", wav_rt_pt.reshape(-1), SR)
|
| 165 |
+
print(f"\n wrote eval_input.wav / eval_recon_onnx.wav / eval_recon_pytorch.wav → {mdir}")
|
| 166 |
+
|
| 167 |
+
print("\n=== verdict ===")
|
| 168 |
+
print(f" encoder index parity : {'PASS' if enc_ok else 'CHECK'}")
|
| 169 |
+
print(f" decoder waveform : {'PASS' if dec_ok else 'CHECK'}")
|
| 170 |
+
sys.exit(0 if (enc_ok and dec_ok) else 1)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
if __name__ == "__main__":
|
| 174 |
+
main()
|
inference.py
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "onnxruntime>=1.20", "numpy", "soundfile", "librosa", "transformers",
|
| 5 |
+
# "numba>=0.60.0", "llvmlite>=0.43.0",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
# numba/llvmlite pinned: librosa otherwise pulls numba 0.53.1 → llvmlite 0.36,
|
| 9 |
+
# which fails to build on Python 3.12.
|
| 10 |
+
"""Run the exported Qwen3-TTS ONNX sub-parts (build blocks + full text→speech).
|
| 11 |
+
|
| 12 |
+
One --model-path points at onnx/{device}_{precision}/ (flat, manifest-driven):
|
| 13 |
+
text_embed.onnx text_ids[B,T] → text_embeds[B,T,2048] (text_projection∘text_embedding)
|
| 14 |
+
codec_embed.onnx codec_ids[B,T] → codec_embeds[B,T,2048] (talker first-codebook embed)
|
| 15 |
+
talker.onnx inputs_embeds[B,T,2048] + position_ids[3,B,T] + mask → (logits[B,T,V], hidden[B,T,2048])
|
| 16 |
+
code_predictor.onnx talker_hidden[B,2048] + codec_ids[B,16] → group_logits[B,15,vocab] (causal teacher-forced)
|
| 17 |
+
residual_embed.onnx codec_ids[B,16] → step_embed[B,2048] (codec_hiddens.sum — next talker input)
|
| 18 |
+
tok_encoder.onnx audio[B,1,24000] → codes[B,frames,16]
|
| 19 |
+
tok_decoder.onnx codes[B,25,16] → waveform[B,1,L] (FIXED 25 frames)
|
| 20 |
+
|
| 21 |
+
GENERATION (`generate`) mirrors `Qwen3TTSForConditionalGeneration.generate` with
|
| 22 |
+
`non_streaming_mode=True` for the two convertible checkpoints:
|
| 23 |
+
• text-to-speech — text + language
|
| 24 |
+
• voice design — text + instruct (natural-language style) [VoiceDesign]
|
| 25 |
+
• custom voice — text + speaker name (+ optional instruct) [CustomVoice]
|
| 26 |
+
ICL audio-clone (ref_audio/ref_text) is a `base`-model feature (needs the speaker
|
| 27 |
+
encoder, which is absent here) and is intentionally not implemented.
|
| 28 |
+
|
| 29 |
+
The talker is a no-cache forward, so the AR loop re-runs the growing prefix each
|
| 30 |
+
step (correct, O(n²)). MROPE reduces to arange here (`get_rope_index` =
|
| 31 |
+
cumsum(mask)-1, 3 identical rows for an unpadded single sequence).
|
| 32 |
+
|
| 33 |
+
Usage:
|
| 34 |
+
uv run inference.py --model-path onnx/cpu_fp32 --selftest
|
| 35 |
+
uv run inference.py --model-path onnx/cpu_fp32 --tts-dir voicedesign \
|
| 36 |
+
--text "Hello there." --instruct "A calm, low female voice." --out out.wav
|
| 37 |
+
uv run inference.py --model-path onnx/cpu_fp32 --tts-dir customvoice \
|
| 38 |
+
--text "你好。" --speaker ethan --language chinese --out out.wav
|
| 39 |
+
"""
|
| 40 |
+
import argparse
|
| 41 |
+
import json
|
| 42 |
+
import sys
|
| 43 |
+
from pathlib import Path
|
| 44 |
+
|
| 45 |
+
import numpy as np
|
| 46 |
+
|
| 47 |
+
SR = 24000
|
| 48 |
+
DEC_FRAMES = 25 # tok_decoder is exported at a fixed 25-frame length
|
| 49 |
+
N_GROUPS = 16
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def cosine(a, b):
|
| 53 |
+
a, b = a.ravel().astype(np.float64), b.ravel().astype(np.float64)
|
| 54 |
+
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ── sampling helpers (numpy; mirror HF generate logic) ──────────────────────────
|
| 58 |
+
def _apply_repetition_penalty(logits, prev_ids, penalty):
|
| 59 |
+
if penalty == 1.0 or not prev_ids:
|
| 60 |
+
return logits
|
| 61 |
+
idx = np.array(sorted(set(int(i) for i in prev_ids)), dtype=np.int64)
|
| 62 |
+
sc = logits[idx]
|
| 63 |
+
logits[idx] = np.where(sc < 0, sc * penalty, sc / penalty)
|
| 64 |
+
return logits
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _sample(logits, do_sample, top_k, top_p, temperature, rng):
|
| 68 |
+
logits = logits.astype(np.float64)
|
| 69 |
+
if not do_sample or temperature <= 0:
|
| 70 |
+
return int(np.argmax(logits))
|
| 71 |
+
logits = logits / max(temperature, 1e-6)
|
| 72 |
+
if top_k and top_k > 0:
|
| 73 |
+
k = min(top_k, logits.shape[-1])
|
| 74 |
+
kth = np.partition(logits, -k)[-k]
|
| 75 |
+
logits = np.where(logits < kth, -np.inf, logits)
|
| 76 |
+
logits -= logits.max()
|
| 77 |
+
probs = np.exp(logits)
|
| 78 |
+
probs /= probs.sum()
|
| 79 |
+
if top_p and top_p < 1.0:
|
| 80 |
+
order = np.argsort(probs)[::-1]
|
| 81 |
+
csum = np.cumsum(probs[order])
|
| 82 |
+
cut = np.searchsorted(csum, top_p) + 1
|
| 83 |
+
keep = order[:cut]
|
| 84 |
+
mask = np.zeros_like(probs)
|
| 85 |
+
mask[keep] = probs[keep]
|
| 86 |
+
probs = mask / mask.sum()
|
| 87 |
+
return int(rng.choice(len(probs), p=probs))
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class Pipeline:
|
| 91 |
+
"""Manifest-driven loader for the exported Qwen3-TTS ONNX sub-parts."""
|
| 92 |
+
|
| 93 |
+
def __init__(self, model_path: str, tts_dir: str = None):
|
| 94 |
+
import onnxruntime as ort
|
| 95 |
+
|
| 96 |
+
self.root = Path(model_path)
|
| 97 |
+
self.manifest = json.loads((self.root / "manifest.json").read_text())
|
| 98 |
+
sm = self.manifest["sub_models"]
|
| 99 |
+
prov = self.manifest.get("execution_provider", "CPUExecutionProvider")
|
| 100 |
+
avail = ort.get_available_providers()
|
| 101 |
+
if prov not in avail:
|
| 102 |
+
print(f" [warn] manifest EP {prov} unavailable; falling back to CPU", file=sys.stderr)
|
| 103 |
+
prov = "CPUExecutionProvider"
|
| 104 |
+
self.provider = prov
|
| 105 |
+
|
| 106 |
+
so = ort.SessionOptions()
|
| 107 |
+
so.log_severity_level = 3
|
| 108 |
+
|
| 109 |
+
def sess(name):
|
| 110 |
+
if name not in sm:
|
| 111 |
+
return None
|
| 112 |
+
return ort.InferenceSession(str(self.root / sm[name]["filename"]),
|
| 113 |
+
so, providers=[prov])
|
| 114 |
+
|
| 115 |
+
self.text_embed = sess("text_embed")
|
| 116 |
+
self.codec_embed = sess("codec_embed")
|
| 117 |
+
self.talker = sess("talker")
|
| 118 |
+
self.code_predictor = sess("code_predictor")
|
| 119 |
+
self.residual_embed = sess("residual_embed")
|
| 120 |
+
self.tok_encoder = sess("tok_encoder")
|
| 121 |
+
self.tok_decoder = sess("tok_decoder")
|
| 122 |
+
self.speaker_encoder = sess("speaker_encoder") # Base only (voice-clone x-vector)
|
| 123 |
+
self.talker_cache = sess("talker_cache") # optional O(n) KV-cache talker
|
| 124 |
+
if self.talker_cache is not None: # ordered past-input names (flattened)
|
| 125 |
+
self._past_names = [i.name for i in self.talker_cache.get_inputs()][3:]
|
| 126 |
+
|
| 127 |
+
# config + tokenizer (only needed for full generation)
|
| 128 |
+
self.tts_dir = tts_dir
|
| 129 |
+
self._cfg = None
|
| 130 |
+
self._tok = None
|
| 131 |
+
if tts_dir is not None:
|
| 132 |
+
self._cfg = json.loads((Path(tts_dir) / "config.json").read_text())
|
| 133 |
+
|
| 134 |
+
# ── building blocks (each verified against PyTorch in eval_*.py) ──────────
|
| 135 |
+
def embed_text(self, text_ids): # [B,T] int64 → [B,T,2048]
|
| 136 |
+
return self.text_embed.run(None, {"text_ids": np.asarray(text_ids, np.int64)})[0]
|
| 137 |
+
|
| 138 |
+
def embed_codec(self, codec_ids): # [B,T] int64 → [B,T,2048]
|
| 139 |
+
return self.codec_embed.run(None, {"codec_ids": np.asarray(codec_ids, np.int64)})[0]
|
| 140 |
+
|
| 141 |
+
def talker_step(self, inputs_embeds, position_ids, attention_mask):
|
| 142 |
+
"""→ (logits[B,T,V], hidden[B,T,2048]). Talker emits both since the
|
| 143 |
+
code_predictor is conditioned on the talker's last hidden state."""
|
| 144 |
+
return self.talker.run(None, {
|
| 145 |
+
"inputs_embeds": inputs_embeds.astype(np.float32),
|
| 146 |
+
"position_ids": np.asarray(position_ids, np.int64),
|
| 147 |
+
"attention_mask": np.asarray(attention_mask, np.int64)})
|
| 148 |
+
|
| 149 |
+
def talker_cache_step(self, inputs_embeds, position_ids, attention_mask, past):
|
| 150 |
+
"""KV-cache talker: → (logits[B,cur,V], hidden[B,cur,2048], present[list of 56]).
|
| 151 |
+
`past`/`present` are ordered lists of the flattened K/V tensors (layer0_k, layer0_v,
|
| 152 |
+
layer1_k, …). Empty past = prefill; len-1 cur = decode."""
|
| 153 |
+
feed = {"inputs_embeds": inputs_embeds.astype(np.float32),
|
| 154 |
+
"position_ids": np.asarray(position_ids, np.int64),
|
| 155 |
+
"attention_mask": np.asarray(attention_mask, np.int64)}
|
| 156 |
+
for name, t in zip(self._past_names, past):
|
| 157 |
+
feed[name] = t.astype(np.float32)
|
| 158 |
+
out = self.talker_cache.run(None, feed)
|
| 159 |
+
return out[0], out[1], list(out[2:]) # logits, hidden, present
|
| 160 |
+
|
| 161 |
+
def predict_residual(self, talker_hidden, codec_ids): # causal teacher-forced
|
| 162 |
+
return self.code_predictor.run(None, {
|
| 163 |
+
"talker_hidden": talker_hidden.astype(np.float32),
|
| 164 |
+
"codec_ids": np.asarray(codec_ids, np.int64)})[0]
|
| 165 |
+
|
| 166 |
+
def step_embed(self, codec_ids): # [B,16] → [B,2048] (sum of group embeds)
|
| 167 |
+
return self.residual_embed.run(None, {"codec_ids": np.asarray(codec_ids, np.int64)})[0]
|
| 168 |
+
|
| 169 |
+
@staticmethod
|
| 170 |
+
def _load_ref_wav(path):
|
| 171 |
+
"""Load a reference wav → mono float32 @ 24 kHz."""
|
| 172 |
+
import soundfile as sf
|
| 173 |
+
wav, sr = sf.read(path, dtype="float32")
|
| 174 |
+
if wav.ndim > 1:
|
| 175 |
+
wav = wav.mean(axis=1)
|
| 176 |
+
if sr != SR:
|
| 177 |
+
import librosa
|
| 178 |
+
wav = librosa.resample(wav, orig_sr=sr, target_sr=SR)
|
| 179 |
+
return wav.astype(np.float32)
|
| 180 |
+
|
| 181 |
+
def encode_chunked(self, wav): # wav [L] 24k → ref codes [T,16]
|
| 182 |
+
"""tok_encoder is fixed at 1 s (24000 samples); encode in 1 s windows + concat."""
|
| 183 |
+
out = []
|
| 184 |
+
for s in range(0, max(len(wav), 1), SR):
|
| 185 |
+
c = wav[s:s + SR]
|
| 186 |
+
if len(c) < SR:
|
| 187 |
+
c = np.pad(c, (0, SR - len(c)))
|
| 188 |
+
out.append(self.encode(c.reshape(1, 1, SR).astype(np.float32))[0]) # [frames,16]
|
| 189 |
+
return np.concatenate(out, axis=0).astype(np.int64)
|
| 190 |
+
|
| 191 |
+
def encode(self, audio): # [B,1,24000] → [B,frames,16]
|
| 192 |
+
return self.tok_encoder.run(None, {"audio": audio.astype(np.float32)})[0]
|
| 193 |
+
|
| 194 |
+
def decode(self, codes): # [B,F,16] → [B,1,L]
|
| 195 |
+
return self.tok_decoder.run(None, {"audio_codes": np.asarray(codes, np.int64)})[0]
|
| 196 |
+
|
| 197 |
+
def decode_chunked(self, codes):
|
| 198 |
+
"""Decode arbitrary-length codes through the fixed-25-frame decoder by
|
| 199 |
+
tiling each 25-frame chunk; tail is padded by repetition then trimmed."""
|
| 200 |
+
F = codes.shape[1]
|
| 201 |
+
outs = []
|
| 202 |
+
for s in range(0, F, DEC_FRAMES):
|
| 203 |
+
chunk = codes[:, s:s + DEC_FRAMES]
|
| 204 |
+
if chunk.shape[1] < DEC_FRAMES: # pad tail by repeat
|
| 205 |
+
idx = np.arange(DEC_FRAMES) % chunk.shape[1]
|
| 206 |
+
chunk = chunk[:, idx]
|
| 207 |
+
wav = self.decode(chunk)
|
| 208 |
+
keep = int(round(wav.shape[-1] * (F - s) / DEC_FRAMES))
|
| 209 |
+
outs.append(wav[..., :keep]); break
|
| 210 |
+
outs.append(self.decode(chunk))
|
| 211 |
+
return np.concatenate(outs, axis=-1)
|
| 212 |
+
|
| 213 |
+
# ── config / tokenizer accessors ──────────────────────────────────────────
|
| 214 |
+
@property
|
| 215 |
+
def cfg(self):
|
| 216 |
+
if self._cfg is None:
|
| 217 |
+
raise RuntimeError("Pass --tts-dir (the HF model dir) for generation: "
|
| 218 |
+
"config token ids + tokenizer live there.")
|
| 219 |
+
return self._cfg
|
| 220 |
+
|
| 221 |
+
@property
|
| 222 |
+
def model_type(self):
|
| 223 |
+
"""tts_model_type from config: 'voice_design' | 'custom_voice' | 'base'.
|
| 224 |
+
Each model exposes different features (see _check_features)."""
|
| 225 |
+
c = self.cfg
|
| 226 |
+
return (c.get("tts_model_type")
|
| 227 |
+
or c.get("talker_config", {}).get("tts_model_type") or "unknown")
|
| 228 |
+
|
| 229 |
+
def _check_features(self, instruct=None, speaker=None, ref_audio=None):
|
| 230 |
+
"""Gate features by model type so a flag that the loaded model can't honor
|
| 231 |
+
fails loudly instead of silently doing nothing:
|
| 232 |
+
voice_design → instruct (natural-language style); no speaker/ref
|
| 233 |
+
custom_voice → speaker (built-in voices) + optional instruct; no ref
|
| 234 |
+
base → voice cloning (ref_audio/ref_text); no speaker/instruct
|
| 235 |
+
"""
|
| 236 |
+
mt = self.model_type
|
| 237 |
+
if speaker and mt != "custom_voice":
|
| 238 |
+
raise ValueError(f"--speaker is a CustomVoice feature, but this model is '{mt}'. "
|
| 239 |
+
"Use a customvoice checkpoint, or drop --speaker.")
|
| 240 |
+
if instruct and mt not in ("voice_design", "custom_voice"):
|
| 241 |
+
raise ValueError(f"--instruct is a VoiceDesign/CustomVoice feature, but this model "
|
| 242 |
+
f"is '{mt}'. Drop --instruct (Base clones from --ref-audio instead).")
|
| 243 |
+
if ref_audio and mt != "base":
|
| 244 |
+
raise ValueError(f"voice cloning (--ref-audio) is a Base-model feature, but this "
|
| 245 |
+
f"model is '{mt}'. Use a base checkpoint.")
|
| 246 |
+
if mt == "custom_voice" and not speaker:
|
| 247 |
+
print(" [note] CustomVoice with no --speaker → model's default voice.",
|
| 248 |
+
file=sys.stderr)
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def tokenizer(self):
|
| 252 |
+
if self._tok is None:
|
| 253 |
+
from transformers import AutoTokenizer
|
| 254 |
+
self._tok = AutoTokenizer.from_pretrained(self.tts_dir, trust_remote_code=True)
|
| 255 |
+
return self._tok
|
| 256 |
+
|
| 257 |
+
def _ids(self, text):
|
| 258 |
+
enc = self.tokenizer(text, return_tensors="np")
|
| 259 |
+
ids = enc["input_ids"]
|
| 260 |
+
return ids if ids.ndim == 2 else ids[None]
|
| 261 |
+
|
| 262 |
+
# ── full text→speech generation ────────────────────────────────────────────
|
| 263 |
+
def generate(self, text, language="Auto", instruct=None, speaker=None,
|
| 264 |
+
ref_audio=None, ref_text=None,
|
| 265 |
+
max_new_tokens=2048, do_sample=True, top_k=50, top_p=1.0,
|
| 266 |
+
temperature=0.9, repetition_penalty=1.05,
|
| 267 |
+
sub_do_sample=True, sub_top_k=50, sub_top_p=1.0, sub_temperature=0.9,
|
| 268 |
+
seed=0, verbose=True):
|
| 269 |
+
"""Mirror Qwen3TTSForConditionalGeneration.generate (non_streaming_mode=True).
|
| 270 |
+
Features are gated by model type (voice_design=instruct, custom_voice=speaker,
|
| 271 |
+
base=clone). Returns codes [T,16] (int64). Decode with `decode_chunked(codes[None])`.
|
| 272 |
+
"""
|
| 273 |
+
cfg = self.cfg
|
| 274 |
+
self._check_features(instruct=instruct, speaker=speaker, ref_audio=ref_audio)
|
| 275 |
+
if ref_audio:
|
| 276 |
+
return self._generate_clone(text, ref_audio, ref_text, language=language,
|
| 277 |
+
max_new_tokens=max_new_tokens, do_sample=do_sample,
|
| 278 |
+
top_k=top_k, top_p=top_p, temperature=temperature,
|
| 279 |
+
repetition_penalty=repetition_penalty,
|
| 280 |
+
sub_do_sample=sub_do_sample, sub_top_k=sub_top_k,
|
| 281 |
+
sub_top_p=sub_top_p, sub_temperature=sub_temperature,
|
| 282 |
+
seed=seed, verbose=verbose)
|
| 283 |
+
tc = cfg["talker_config"]
|
| 284 |
+
H = tc["hidden_size"]
|
| 285 |
+
rng = np.random.default_rng(seed)
|
| 286 |
+
|
| 287 |
+
# token ids
|
| 288 |
+
tts_bos, tts_eos, tts_pad = (cfg["tts_bos_token_id"], cfg["tts_eos_token_id"],
|
| 289 |
+
cfg["tts_pad_token_id"])
|
| 290 |
+
codec_eos = tc["codec_eos_token_id"]
|
| 291 |
+
codec_pad, codec_bos = tc["codec_pad_id"], tc["codec_bos_id"]
|
| 292 |
+
vocab = tc["vocab_size"]
|
| 293 |
+
|
| 294 |
+
# 0) text → ids (assistant template). role = first 3, trailing = last 5.
|
| 295 |
+
assistant = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
| 296 |
+
input_id = self._ids(assistant) # [1, L]
|
| 297 |
+
if input_id.shape[1] < 9:
|
| 298 |
+
raise ValueError("text tokenized too short for the assistant template")
|
| 299 |
+
|
| 300 |
+
# 1) special text embeds (tts_bos/eos/pad)
|
| 301 |
+
spec = self.embed_text([[tts_bos, tts_eos, tts_pad]]) # [1,3,H]
|
| 302 |
+
bos_e, eos_e, pad_e = spec[:, 0:1], spec[:, 1:2], spec[:, 2:3]
|
| 303 |
+
|
| 304 |
+
# 2) language + codec prefill tags
|
| 305 |
+
lang = (language or "auto").lower()
|
| 306 |
+
if lang == "auto" or lang not in tc.get("codec_language_id", {}):
|
| 307 |
+
language_id = None
|
| 308 |
+
else:
|
| 309 |
+
language_id = tc["codec_language_id"][lang]
|
| 310 |
+
if language_id is None:
|
| 311 |
+
codec_prefill = [[tc["codec_nothink_id"], tc["codec_think_bos_id"],
|
| 312 |
+
tc["codec_think_eos_id"]]]
|
| 313 |
+
else:
|
| 314 |
+
codec_prefill = [[tc["codec_think_id"], tc["codec_think_bos_id"],
|
| 315 |
+
language_id, tc["codec_think_eos_id"]]]
|
| 316 |
+
codec0 = self.embed_codec(codec_prefill) # [1,P,H]
|
| 317 |
+
codec1 = self.embed_codec([[codec_pad, codec_bos]]) # [1,2,H]
|
| 318 |
+
|
| 319 |
+
# speaker (custom voice): speaker name → spk_id → codec table embed
|
| 320 |
+
speaker_embed = None
|
| 321 |
+
if speaker:
|
| 322 |
+
spk_map = tc.get("spk_id", {})
|
| 323 |
+
if speaker.lower() not in spk_map:
|
| 324 |
+
raise ValueError(f"Speaker '{speaker}' not in spk_id {list(spk_map)[:8]}…")
|
| 325 |
+
speaker_embed = self.embed_codec([[spk_map[speaker.lower()]]]) # [1,1,H]
|
| 326 |
+
|
| 327 |
+
if speaker_embed is None:
|
| 328 |
+
codec_input = np.concatenate([codec0, codec1], axis=1)
|
| 329 |
+
else:
|
| 330 |
+
codec_input = np.concatenate([codec0, speaker_embed, codec1], axis=1)
|
| 331 |
+
|
| 332 |
+
# 3) instruct prefix (voice design) — prepended text_projection embeds
|
| 333 |
+
prefix = []
|
| 334 |
+
if instruct:
|
| 335 |
+
instruct_text = f"<|im_start|>user\n{instruct}<|im_end|>\n"
|
| 336 |
+
prefix.append(self.embed_text(self._ids(instruct_text)))
|
| 337 |
+
|
| 338 |
+
# 4) assemble talker prefill (non_streaming_mode=True)
|
| 339 |
+
role = self.embed_text(input_id[:, :3]) # <|im_start|>assistant\n
|
| 340 |
+
pad_block = np.concatenate(
|
| 341 |
+
[np.repeat(pad_e, codec_input.shape[1] - 2, axis=1), bos_e], axis=1)
|
| 342 |
+
talker_in = np.concatenate([role, pad_block + codec_input[:, :-1]], axis=1)
|
| 343 |
+
|
| 344 |
+
body_ids = input_id[:, 3:-5] # pure text tokens
|
| 345 |
+
Ltext = body_ids.shape[1]
|
| 346 |
+
text_body = self.embed_text(body_ids) # [1,Ltext,H]
|
| 347 |
+
block1 = (np.concatenate([text_body, eos_e], axis=1)
|
| 348 |
+
+ self.embed_codec([[codec_pad] * (Ltext + 1)]))
|
| 349 |
+
block2 = pad_e + self.embed_codec([[codec_bos]]) # [1,1,H]
|
| 350 |
+
talker_in = np.concatenate([talker_in, block1, block2], axis=1)
|
| 351 |
+
if prefix:
|
| 352 |
+
talker_in = np.concatenate(prefix + [talker_in], axis=1)
|
| 353 |
+
# trailing_text_hidden is just tts_pad in non_streaming mode → add pad_e each step
|
| 354 |
+
trailing = pad_e[:, 0] # [1,H]
|
| 355 |
+
|
| 356 |
+
return self._ar_loop(talker_in, trailing, vocab, codec_eos, max_new_tokens,
|
| 357 |
+
do_sample, top_k, top_p, temperature, repetition_penalty,
|
| 358 |
+
sub_do_sample, sub_top_k, sub_top_p, sub_temperature, seed, verbose)
|
| 359 |
+
|
| 360 |
+
def _ar_loop(self, talker_in, trailing, vocab, codec_eos, max_new_tokens, do_sample,
|
| 361 |
+
top_k, top_p, temperature, repetition_penalty, sub_do_sample, sub_top_k,
|
| 362 |
+
sub_top_p, sub_temperature, seed, verbose):
|
| 363 |
+
"""AR talker loop (MROPE→arange). Uses the O(n) KV-cache talker if exported,
|
| 364 |
+
else the no-cache O(n²) talker. Shared by all generation paths. Returns codes [T,16]."""
|
| 365 |
+
if self.talker_cache is not None:
|
| 366 |
+
return self._ar_loop_cached(talker_in, trailing, vocab, codec_eos, max_new_tokens,
|
| 367 |
+
do_sample, top_k, top_p, temperature, repetition_penalty,
|
| 368 |
+
sub_do_sample, sub_top_k, sub_top_p, sub_temperature,
|
| 369 |
+
seed, verbose)
|
| 370 |
+
if self.talker is None:
|
| 371 |
+
raise RuntimeError("no talker model found: need talker_cache.onnx (preferred) or "
|
| 372 |
+
"talker.onnx in the model dir.")
|
| 373 |
+
rng = np.random.default_rng(seed)
|
| 374 |
+
suppress = np.array([i for i in range(vocab - 1024, vocab) if i != codec_eos],
|
| 375 |
+
dtype=np.int64)
|
| 376 |
+
all_codes, prev_first = [], []
|
| 377 |
+
for step in range(max_new_tokens):
|
| 378 |
+
T = talker_in.shape[1]
|
| 379 |
+
pos = np.broadcast_to(np.arange(T), (3, 1, T)).copy()
|
| 380 |
+
mask = np.ones((1, T), dtype=np.int64)
|
| 381 |
+
logits, hidden = self.talker_step(talker_in, pos, mask)
|
| 382 |
+
first = logits[0, -1].astype(np.float64).copy()
|
| 383 |
+
first[suppress] = -np.inf
|
| 384 |
+
first = _apply_repetition_penalty(first, prev_first, repetition_penalty)
|
| 385 |
+
code0 = _sample(first, do_sample, top_k, top_p, temperature, rng)
|
| 386 |
+
if code0 == codec_eos:
|
| 387 |
+
break
|
| 388 |
+
prev_first.append(code0)
|
| 389 |
+
th = hidden[0, -1][None].astype(np.float32)
|
| 390 |
+
codes16 = np.zeros((1, N_GROUPS), dtype=np.int64)
|
| 391 |
+
codes16[0, 0] = code0
|
| 392 |
+
for j in range(1, N_GROUPS):
|
| 393 |
+
gl = self.predict_residual(th, codes16)
|
| 394 |
+
codes16[0, j] = _sample(gl[0, j - 1], sub_do_sample, sub_top_k, sub_top_p,
|
| 395 |
+
sub_temperature, rng)
|
| 396 |
+
all_codes.append(codes16[0].copy())
|
| 397 |
+
nxt = self.step_embed(codes16)[:, None] + trailing[:, None]
|
| 398 |
+
talker_in = np.concatenate([talker_in, nxt], axis=1)
|
| 399 |
+
if verbose and (step + 1) % 25 == 0:
|
| 400 |
+
print(f" …{step + 1} frames", file=sys.stderr)
|
| 401 |
+
codes = np.stack(all_codes, axis=0).astype(np.int64) if all_codes \
|
| 402 |
+
else np.zeros((0, N_GROUPS), np.int64)
|
| 403 |
+
if verbose:
|
| 404 |
+
print(f" generated {codes.shape[0]} frames")
|
| 405 |
+
return codes
|
| 406 |
+
|
| 407 |
+
def _ar_loop_cached(self, talker_in, trailing, vocab, codec_eos, max_new_tokens, do_sample,
|
| 408 |
+
top_k, top_p, temperature, repetition_penalty, sub_do_sample, sub_top_k,
|
| 409 |
+
sub_top_p, sub_temperature, seed, verbose):
|
| 410 |
+
"""O(n) KV-cache AR loop: prefill once, then decode one token/step feeding the cache.
|
| 411 |
+
Numerically identical to the no-cache loop (same positions, full causal attention)."""
|
| 412 |
+
rng = np.random.default_rng(seed)
|
| 413 |
+
suppress = np.array([i for i in range(vocab - 1024, vocab) if i != codec_eos], dtype=np.int64)
|
| 414 |
+
past = [np.zeros((1, 8, 0, 128), np.float32) for _ in self._past_names]
|
| 415 |
+
T0 = talker_in.shape[1]
|
| 416 |
+
pos = np.broadcast_to(np.arange(T0), (3, 1, T0)).copy()
|
| 417 |
+
logits, hidden, past = self.talker_cache_step(talker_in, pos, np.ones((1, T0), np.int64), past)
|
| 418 |
+
total = T0
|
| 419 |
+
all_codes, prev_first = [], []
|
| 420 |
+
for step in range(max_new_tokens):
|
| 421 |
+
first = logits[0, -1].astype(np.float64).copy()
|
| 422 |
+
first[suppress] = -np.inf
|
| 423 |
+
first = _apply_repetition_penalty(first, prev_first, repetition_penalty)
|
| 424 |
+
code0 = _sample(first, do_sample, top_k, top_p, temperature, rng)
|
| 425 |
+
if code0 == codec_eos:
|
| 426 |
+
break
|
| 427 |
+
prev_first.append(code0)
|
| 428 |
+
th = hidden[0, -1][None].astype(np.float32)
|
| 429 |
+
codes16 = np.zeros((1, N_GROUPS), dtype=np.int64)
|
| 430 |
+
codes16[0, 0] = code0
|
| 431 |
+
for j in range(1, N_GROUPS):
|
| 432 |
+
gl = self.predict_residual(th, codes16)
|
| 433 |
+
codes16[0, j] = _sample(gl[0, j - 1], sub_do_sample, sub_top_k, sub_top_p,
|
| 434 |
+
sub_temperature, rng)
|
| 435 |
+
all_codes.append(codes16[0].copy())
|
| 436 |
+
nxt = self.step_embed(codes16)[:, None] + trailing[:, None] # [1,1,H]
|
| 437 |
+
pos = np.broadcast_to(np.array([total]), (3, 1, 1)).copy()
|
| 438 |
+
logits, hidden, past = self.talker_cache_step(
|
| 439 |
+
nxt, pos, np.ones((1, total + 1), np.int64), past)
|
| 440 |
+
total += 1
|
| 441 |
+
if verbose and (step + 1) % 25 == 0:
|
| 442 |
+
print(f" …{step + 1} frames (cached)", file=sys.stderr)
|
| 443 |
+
codes = np.stack(all_codes, axis=0).astype(np.int64) if all_codes \
|
| 444 |
+
else np.zeros((0, N_GROUPS), np.int64)
|
| 445 |
+
if verbose:
|
| 446 |
+
print(f" generated {codes.shape[0]} frames (KV-cache)")
|
| 447 |
+
return codes
|
| 448 |
+
|
| 449 |
+
def _generate_clone(self, text, ref_audio, ref_text, language="Auto", max_new_tokens=2048,
|
| 450 |
+
do_sample=True, top_k=50, top_p=1.0, temperature=0.9,
|
| 451 |
+
repetition_penalty=1.05, sub_do_sample=True, sub_top_k=50, sub_top_p=1.0,
|
| 452 |
+
sub_temperature=0.9, seed=0, verbose=True):
|
| 453 |
+
"""Base-model voice cloning (ICL), faithful to generate_icl_prompt (modeling L1968)
|
| 454 |
+
+ the x-vector speaker prompt. Reference audio → codes (tok_encoder) + x-vector
|
| 455 |
+
(speaker_encoder); prompt = role + codec tags(+x-vector) + [ref_text+text+eos / codec_bos
|
| 456 |
+
+ per-frame ref-code sum]. Returns generated codes [T,16]."""
|
| 457 |
+
if self.speaker_encoder is None:
|
| 458 |
+
raise RuntimeError("speaker_encoder.onnx missing — export it for the Base model: "
|
| 459 |
+
"`uv run optimize.py --model base/1.7B --components speaker_encoder`.")
|
| 460 |
+
if not ref_text:
|
| 461 |
+
raise ValueError("voice clone requires --ref-text (transcript of --ref-audio).")
|
| 462 |
+
cfg = self.cfg; tc = cfg["talker_config"]; H = tc["hidden_size"]
|
| 463 |
+
tts_bos, tts_eos, tts_pad = (cfg["tts_bos_token_id"], cfg["tts_eos_token_id"],
|
| 464 |
+
cfg["tts_pad_token_id"])
|
| 465 |
+
codec_eos = tc["codec_eos_token_id"]
|
| 466 |
+
codec_pad, codec_bos = tc["codec_pad_id"], tc["codec_bos_id"]; vocab = tc["vocab_size"]
|
| 467 |
+
|
| 468 |
+
# reference audio → codes (tok_encoder, 1 s windows) + x-vector (speaker_encoder)
|
| 469 |
+
wav = self._load_ref_wav(ref_audio)
|
| 470 |
+
ref_code = self.encode_chunked(wav) # [T_ref,16]
|
| 471 |
+
spk = self.speaker_encoder.run(None, {"audio": wav[None].astype(np.float32)})[0]
|
| 472 |
+
spk = spk.reshape(1, 1, H) # x-vector [1,1,H]
|
| 473 |
+
|
| 474 |
+
assistant = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
| 475 |
+
input_id = self._ids(assistant)
|
| 476 |
+
ref_id = self._ids(f"<|im_start|>assistant\n{ref_text}<|im_end|>\n")[:, 3:-2]
|
| 477 |
+
text_id = input_id[:, 3:-5]
|
| 478 |
+
|
| 479 |
+
spec = self.embed_text([[tts_bos, tts_eos, tts_pad]])
|
| 480 |
+
bos_e, eos_e, pad_e = spec[:, 0:1], spec[:, 1:2], spec[:, 2:3]
|
| 481 |
+
lang = (language or "auto").lower()
|
| 482 |
+
language_id = (tc["codec_language_id"][lang]
|
| 483 |
+
if lang != "auto" and lang in tc.get("codec_language_id", {}) else None)
|
| 484 |
+
codec_prefill = ([[tc["codec_nothink_id"], tc["codec_think_bos_id"], tc["codec_think_eos_id"]]]
|
| 485 |
+
if language_id is None else
|
| 486 |
+
[[tc["codec_think_id"], tc["codec_think_bos_id"], language_id,
|
| 487 |
+
tc["codec_think_eos_id"]]])
|
| 488 |
+
codec0 = self.embed_codec(codec_prefill)
|
| 489 |
+
codec1 = self.embed_codec([[codec_pad, codec_bos]])
|
| 490 |
+
codec_input = np.concatenate([codec0, spk, codec1], axis=1) # x-vector injected
|
| 491 |
+
|
| 492 |
+
role = self.embed_text(input_id[:, :3])
|
| 493 |
+
pad_block = np.concatenate([np.repeat(pad_e, codec_input.shape[1] - 2, axis=1), bos_e], axis=1)
|
| 494 |
+
base = np.concatenate([role, pad_block + codec_input[:, :-1]], axis=1)
|
| 495 |
+
|
| 496 |
+
# ICL block (generate_icl_prompt, non_streaming): the per-frame ref-code sum IS step_embed
|
| 497 |
+
text_embed = np.concatenate([self.embed_text(np.concatenate([ref_id, text_id], axis=1)),
|
| 498 |
+
eos_e], axis=1) # [1,T1,H]
|
| 499 |
+
T1 = text_embed.shape[1]
|
| 500 |
+
codec_embed = np.concatenate([self.embed_codec([[codec_bos]]),
|
| 501 |
+
self.step_embed(ref_code)[None]], axis=1) # [1,1+T_ref,H]
|
| 502 |
+
icl = text_embed + self.embed_codec([[codec_pad] * T1])
|
| 503 |
+
icl = np.concatenate([icl, codec_embed + pad_e], axis=1)
|
| 504 |
+
talker_in = np.concatenate([base, icl], axis=1)
|
| 505 |
+
trailing = pad_e[:, 0]
|
| 506 |
+
if verbose:
|
| 507 |
+
print(f" [clone] ref {ref_code.shape[0]} frames + ref_text {ref_id.shape[1]} toks "
|
| 508 |
+
f"+ text {text_id.shape[1]} toks → prefill {talker_in.shape[1]}", file=sys.stderr)
|
| 509 |
+
return self._ar_loop(talker_in, trailing, vocab, codec_eos, max_new_tokens, do_sample,
|
| 510 |
+
top_k, top_p, temperature, repetition_penalty, sub_do_sample,
|
| 511 |
+
sub_top_k, sub_top_p, sub_temperature, seed, verbose)
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def selftest(pipe: Pipeline):
|
| 515 |
+
"""Codec round-trip + building-block smoke test on the loaded EP."""
|
| 516 |
+
print(f"Provider: {pipe.provider}")
|
| 517 |
+
rng = np.random.default_rng(0)
|
| 518 |
+
t = np.arange(SR) / SR
|
| 519 |
+
audio = (0.6 * np.sin(2 * np.pi * (180 + 300 * t) * t)
|
| 520 |
+
+ 0.01 * rng.standard_normal(SR)).astype(np.float32)[None, None, :]
|
| 521 |
+
|
| 522 |
+
codes = pipe.encode(audio)
|
| 523 |
+
print(f" encode : audio{audio.shape} -> codes{codes.shape}")
|
| 524 |
+
wav = pipe.decode_chunked(codes)
|
| 525 |
+
print(f" decode : codes{codes.shape} -> wav{wav.shape}")
|
| 526 |
+
|
| 527 |
+
te = pipe.embed_text(rng.integers(0, 1000, (1, 8)))
|
| 528 |
+
ce = pipe.embed_codec(rng.integers(0, 2048, (1, 8)))
|
| 529 |
+
print(f" text_embed{te.shape} codec_embed{ce.shape}")
|
| 530 |
+
th = rng.standard_normal((1, 2048)).astype(np.float32)
|
| 531 |
+
gl = pipe.predict_residual(th, rng.integers(0, 2048, (1, N_GROUPS)))
|
| 532 |
+
print(f" code_predictor group_logits{gl.shape}")
|
| 533 |
+
if pipe.residual_embed is not None:
|
| 534 |
+
se = pipe.step_embed(rng.integers(0, 2048, (1, N_GROUPS)))
|
| 535 |
+
print(f" residual_embed step_embed{se.shape}")
|
| 536 |
+
else:
|
| 537 |
+
print(" [warn] residual_embed.onnx missing — re-export for generate()")
|
| 538 |
+
print("OK — codec path + building blocks run on this EP.")
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def main():
|
| 542 |
+
ap = argparse.ArgumentParser(description="Qwen3-TTS ONNX inference")
|
| 543 |
+
ap.add_argument("--model-path", required=True, help="onnx/{device}_{precision} dir")
|
| 544 |
+
ap.add_argument("--tts-dir", help="HF model dir (config + tokenizer) — needed for --text")
|
| 545 |
+
ap.add_argument("--selftest", action="store_true", help="codec round-trip + block smoke test")
|
| 546 |
+
ap.add_argument("--text", help="text to synthesize (full generation)")
|
| 547 |
+
ap.add_argument("--instruct", help="voice-design style instruction (VoiceDesign only)")
|
| 548 |
+
ap.add_argument("--speaker", help="built-in speaker name (CustomVoice only)")
|
| 549 |
+
ap.add_argument("--ref-audio", help="reference wav for voice cloning (Base only)")
|
| 550 |
+
ap.add_argument("--ref-text", help="transcript of --ref-audio (Base clone)")
|
| 551 |
+
ap.add_argument("--language", default="Auto", help="language (default Auto)")
|
| 552 |
+
ap.add_argument("--out", default="out.wav", help="output wav path for --text")
|
| 553 |
+
ap.add_argument("--max-new-tokens", type=int, default=2048)
|
| 554 |
+
ap.add_argument("--greedy", action="store_true", help="disable sampling (argmax)")
|
| 555 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 556 |
+
ap.add_argument("--save-wav", help="write the self-test reconstruction to this path")
|
| 557 |
+
args = ap.parse_args()
|
| 558 |
+
|
| 559 |
+
pipe = Pipeline(args.model_path, tts_dir=args.tts_dir)
|
| 560 |
+
|
| 561 |
+
if args.text is not None:
|
| 562 |
+
print(f" model_type={pipe.model_type}", file=sys.stderr)
|
| 563 |
+
codes = pipe.generate(
|
| 564 |
+
args.text, language=args.language, instruct=args.instruct,
|
| 565 |
+
speaker=args.speaker, ref_audio=args.ref_audio, ref_text=args.ref_text,
|
| 566 |
+
max_new_tokens=args.max_new_tokens,
|
| 567 |
+
do_sample=not args.greedy, sub_do_sample=not args.greedy, seed=args.seed)
|
| 568 |
+
if codes.shape[0] == 0:
|
| 569 |
+
print(" [warn] no frames generated (immediate EOS)"); return
|
| 570 |
+
wav = pipe.decode_chunked(codes[None]).reshape(-1)
|
| 571 |
+
import soundfile as sf
|
| 572 |
+
sf.write(args.out, wav, SR)
|
| 573 |
+
print(f" wrote {args.out} ({wav.shape[0] / SR:.2f}s)")
|
| 574 |
+
return
|
| 575 |
+
|
| 576 |
+
if args.selftest:
|
| 577 |
+
selftest(pipe)
|
| 578 |
+
if args.save_wav:
|
| 579 |
+
import soundfile as sf
|
| 580 |
+
rng = np.random.default_rng(0)
|
| 581 |
+
t = np.arange(SR) / SR
|
| 582 |
+
audio = (0.6 * np.sin(2 * np.pi * (180 + 300 * t) * t)).astype(np.float32)[None, None, :]
|
| 583 |
+
sf.write(args.save_wav, pipe.decode_chunked(pipe.encode(audio)).reshape(-1), SR)
|
| 584 |
+
print(f" wrote {args.save_wav}")
|
| 585 |
+
return
|
| 586 |
+
|
| 587 |
+
ap.error("nothing to do: pass --selftest or --text ...")
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
if __name__ == "__main__":
|
| 591 |
+
main()
|
info.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
keywords:
|
| 2 |
+
- olive-ai
|
| 3 |
+
- tts
|
| 4 |
+
- speech-synthesis
|
| 5 |
+
recipes:
|
| 6 |
+
- name: Qwen3-TTS-VoiceDesign
|
| 7 |
+
file: optimize.py
|
| 8 |
+
eps:
|
| 9 |
+
- CPUExecutionProvider
|
| 10 |
+
devices:
|
| 11 |
+
- cpu
|
optimize.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "transformers==4.57.3",
|
| 5 |
+
# "torch",
|
| 6 |
+
# "torchvision",
|
| 7 |
+
# "torchaudio",
|
| 8 |
+
# "olive-ai",
|
| 9 |
+
# "onnx",
|
| 10 |
+
# "onnxruntime>=1.20",
|
| 11 |
+
# "onnxruntime-genai",
|
| 12 |
+
# "safetensors",
|
| 13 |
+
# "numpy",
|
| 14 |
+
# "tabulate",
|
| 15 |
+
# "huggingface_hub",
|
| 16 |
+
# "accelerate",
|
| 17 |
+
# "librosa",
|
| 18 |
+
# "numba>=0.60.0",
|
| 19 |
+
# "llvmlite>=0.43.0",
|
| 20 |
+
# "soundfile",
|
| 21 |
+
# ]
|
| 22 |
+
# ///
|
| 23 |
+
# numba/llvmlite pinned >=0.60/0.43: librosa otherwise resolves to numba 0.53.1 →
|
| 24 |
+
# llvmlite 0.36, which fails to build on Python 3.12 ("only versions <3.10 supported").
|
| 25 |
+
"""Qwen3-TTS → ONNX sub-parts. Run with: uv run optimize.py --model <name|path> ...
|
| 26 |
+
|
| 27 |
+
Self-contained uv script: pins transformers==4.57.3 (the version the vendored
|
| 28 |
+
qwen_tts modeling needs) in an isolated env, leaving the shared 5.10.2 venv alone.
|
| 29 |
+
|
| 30 |
+
Handles three model kinds (auto-detected from config.json `model_type`):
|
| 31 |
+
qwen3_tts → TTS : talker (+ code_predictor) [+ embedded tokenizer]
|
| 32 |
+
qwen3_tts_tokenizer_12hz → codec: tok_encoder + tok_decoder
|
| 33 |
+
|
| 34 |
+
Outputs flat sub-parts under onnx/{device}_{precision}/ + manifest.json.
|
| 35 |
+
|
| 36 |
+
--device {cpu,cuda} --precision {int4,fp16,fp32}
|
| 37 |
+
|
| 38 |
+
The talker is a custom Qwen3+MROPE LLM (dual codec/text embedding, codec_head) and is
|
| 39 |
+
NOT onnxruntime-genai ModelBuilder-compatible (MROPE + dual embedding don't map to a
|
| 40 |
+
stock Qwen3) — it is exported via Olive like the rest. (ModelBuilder remap is attempted
|
| 41 |
+
only when the talker is detectably stock; otherwise Olive is used.)
|
| 42 |
+
|
| 43 |
+
Usage:
|
| 44 |
+
uv run optimize.py --model Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --device cpu --precision int4
|
| 45 |
+
uv run optimize.py --model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --device cpu --precision fp16
|
| 46 |
+
uv run optimize.py --model Qwen/Qwen3-TTS-Tokenizer-12Hz --device cpu --precision fp32
|
| 47 |
+
uv run optimize.py --model voicedesign --skip-download # use local dir
|
| 48 |
+
"""
|
| 49 |
+
import argparse
|
| 50 |
+
import json
|
| 51 |
+
import shutil
|
| 52 |
+
import subprocess
|
| 53 |
+
import sys
|
| 54 |
+
from pathlib import Path
|
| 55 |
+
|
| 56 |
+
HERE = Path(__file__).parent
|
| 57 |
+
PROVIDER = {"cpu": "CPUExecutionProvider", "cuda": "CUDAExecutionProvider"}
|
| 58 |
+
OLIVE_DEV = {"cpu": "cpu", "cuda": "gpu"}
|
| 59 |
+
|
| 60 |
+
# component → (loader, io_config, dummy_inputs, model_subdir or "")
|
| 61 |
+
TTS_COMPONENTS = {
|
| 62 |
+
"text_embed": ("get_text_embed_model", "get_text_embed_io_config", "get_text_embed_dummy_inputs", ""),
|
| 63 |
+
"codec_embed": ("get_codec_embed_model", "get_codec_embed_io_config", "get_codec_embed_dummy_inputs", ""),
|
| 64 |
+
"talker": ("get_talker_model", "get_talker_io_config", "get_talker_dummy_inputs", ""),
|
| 65 |
+
"talker_cache": ("get_talker_cache_model", "get_talker_cache_io_config", "get_talker_cache_dummy_inputs", ""),
|
| 66 |
+
"code_predictor": ("get_code_predictor_model", "get_code_predictor_io_config", "get_code_predictor_dummy_inputs", ""),
|
| 67 |
+
"residual_embed": ("get_residual_embed_model", "get_residual_embed_io_config", "get_residual_embed_dummy_inputs", ""),
|
| 68 |
+
"tok_encoder": ("get_tok_encoder_model", "get_tok_encoder_io_config", "get_tok_encoder_dummy_inputs", "speech_tokenizer"),
|
| 69 |
+
"tok_decoder": ("get_tok_decoder_model", "get_tok_decoder_io_config", "get_tok_decoder_dummy_inputs", "speech_tokenizer"),
|
| 70 |
+
"speaker_encoder": ("get_speaker_encoder_model", "get_speaker_encoder_io_config", "get_speaker_encoder_dummy_inputs", ""),
|
| 71 |
+
}
|
| 72 |
+
TOKENIZER_COMPONENTS = {
|
| 73 |
+
"tok_encoder": ("get_tok_encoder_model", "get_tok_encoder_io_config", "get_tok_encoder_dummy_inputs", ""),
|
| 74 |
+
"tok_decoder": ("get_tok_decoder_model", "get_tok_decoder_io_config", "get_tok_decoder_dummy_inputs", ""),
|
| 75 |
+
}
|
| 76 |
+
# audio codecs + speaker encoder stay fp32 (DAC/conv int4/fp16 too lossy; ECAPA small)
|
| 77 |
+
FP32_ONLY = {"tok_encoder", "tok_decoder", "speaker_encoder"}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def output_root(device, precision):
|
| 81 |
+
return HERE / "onnx" / f"{device}_{precision}"
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def resolve_model(model: str, skip_download: bool) -> Path:
|
| 85 |
+
"""Local dir (relative or absolute) or HF id → local checkpoint dir."""
|
| 86 |
+
p = Path(model)
|
| 87 |
+
if p.is_dir():
|
| 88 |
+
return p
|
| 89 |
+
local = HERE / model.split("/")[-1].lower()
|
| 90 |
+
if local.is_dir():
|
| 91 |
+
return local
|
| 92 |
+
if skip_download:
|
| 93 |
+
raise FileNotFoundError(f"{model} not found locally and --skip-download set")
|
| 94 |
+
from huggingface_hub import snapshot_download
|
| 95 |
+
dst = HERE / model.split("/")[-1]
|
| 96 |
+
print(f" Downloading {model} → {dst}")
|
| 97 |
+
snapshot_download(repo_id=model, local_dir=str(dst))
|
| 98 |
+
return dst
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def detect_type(model_dir: Path) -> str:
|
| 102 |
+
cfg = json.loads((model_dir / "config.json").read_text())
|
| 103 |
+
return cfg.get("model_type", "")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def build_component(name, funcs, model_dir, out_dir, device, precision):
|
| 107 |
+
from olive import run
|
| 108 |
+
loader, io_cfg, dummy, subdir = funcs
|
| 109 |
+
src = model_dir / subdir if subdir else model_dir
|
| 110 |
+
prec = "fp32" if name in FP32_ONLY else precision
|
| 111 |
+
passes = {"c": {"type": "OnnxConversion", "target_opset": 20}}
|
| 112 |
+
# Transformers using create_causal_mask aren't torch.onnx(TorchScript)-traceable
|
| 113 |
+
# → dynamo exporter (codec parts + the MROPE talker).
|
| 114 |
+
if name in FP32_ONLY or name in ("talker", "talker_cache"):
|
| 115 |
+
passes["c"]["use_dynamo_exporter"] = True
|
| 116 |
+
if prec == "int4":
|
| 117 |
+
passes["q"] = {"type": "OnnxBlockwiseRtnQuantization", "bits": 4,
|
| 118 |
+
"block_size": 32, "is_symmetric": False}
|
| 119 |
+
elif prec == "fp16":
|
| 120 |
+
passes["h"] = {"type": "OnnxFloatToFloat16"}
|
| 121 |
+
|
| 122 |
+
tmp = (out_dir / f"_{name}_tmp").resolve()
|
| 123 |
+
cfg = {
|
| 124 |
+
"input_model": {"type": "PyTorchModel", "model_path": str(src.resolve()),
|
| 125 |
+
"model_loader": loader,
|
| 126 |
+
"model_script": str((HERE / "user_script.py").resolve()),
|
| 127 |
+
"io_config": io_cfg, "dummy_inputs_func": dummy},
|
| 128 |
+
"systems": {"local_system": {"type": "LocalSystem", "accelerators": [
|
| 129 |
+
{"device": OLIVE_DEV[device], "execution_providers": [PROVIDER[device]]}]}},
|
| 130 |
+
"passes": passes, "target": "local_system", "log_severity_level": 2,
|
| 131 |
+
"output_dir": str(tmp), "cache_dir": str((out_dir / "_ocache").resolve()),
|
| 132 |
+
"no_artifacts": True,
|
| 133 |
+
}
|
| 134 |
+
tmp_json = out_dir / f"_{name}.json"
|
| 135 |
+
tmp_json.write_text(json.dumps(cfg, indent=2))
|
| 136 |
+
print(f" Olive export {name} (precision={prec}) → {out_dir}/{name}.onnx")
|
| 137 |
+
run(str(tmp_json))
|
| 138 |
+
tmp_json.unlink(missing_ok=True)
|
| 139 |
+
moved_data = False
|
| 140 |
+
for f in tmp.glob("model.onnx*"):
|
| 141 |
+
dst = out_dir / f.name.replace("model.onnx", f"{name}.onnx")
|
| 142 |
+
if dst.exists():
|
| 143 |
+
dst.unlink()
|
| 144 |
+
shutil.move(str(f), str(dst))
|
| 145 |
+
if dst.name.endswith(".onnx.data"):
|
| 146 |
+
moved_data = True
|
| 147 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 148 |
+
shutil.rmtree(out_dir / "_ocache", ignore_errors=True)
|
| 149 |
+
if moved_data:
|
| 150 |
+
relink_external_data(out_dir / f"{name}.onnx", f"{name}.onnx.data")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def relink_external_data(onnx_path, new_data_name):
|
| 154 |
+
"""Point a renamed model's external_data references at the renamed .data file.
|
| 155 |
+
|
| 156 |
+
Olive writes `model.onnx` + `model.onnx.data`; we flatten to `{name}.onnx[.data]`
|
| 157 |
+
but the proto still references `model.onnx.data`, so onnxruntime fails to load
|
| 158 |
+
(`External data path does not exist`). Rewrite each tensor's `location` entry.
|
| 159 |
+
Proto-only (load_external_data=False) so we never pull the multi-GB blob into RAM.
|
| 160 |
+
"""
|
| 161 |
+
import onnx
|
| 162 |
+
m = onnx.load(str(onnx_path), load_external_data=False)
|
| 163 |
+
n = 0
|
| 164 |
+
for t in m.graph.initializer:
|
| 165 |
+
if t.HasField("data_location") and t.data_location == onnx.TensorProto.EXTERNAL:
|
| 166 |
+
for kv in t.external_data:
|
| 167 |
+
if kv.key == "location":
|
| 168 |
+
kv.value = new_data_name
|
| 169 |
+
n += 1
|
| 170 |
+
onnx.save(m, str(onnx_path)) # proto only; .data already on disk
|
| 171 |
+
print(f" relinked {n} external-data refs → {new_data_name}")
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def write_manifest(out_dir, model, device, precision, model_kind, built):
|
| 175 |
+
# include every sub-part present on disk (robust to incremental builds)
|
| 176 |
+
present = sorted({p.stem for p in out_dir.glob("*.onnx")})
|
| 177 |
+
manifest = {
|
| 178 |
+
"model_id": model, "model_kind": model_kind, "device": device,
|
| 179 |
+
"precision": precision, "execution_provider": PROVIDER[device],
|
| 180 |
+
"sub_models": {n: {"filename": f"{n}.onnx"} for n in present},
|
| 181 |
+
}
|
| 182 |
+
(out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
| 183 |
+
print(f" Wrote {out_dir / 'manifest.json'}")
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def main():
|
| 187 |
+
ap = argparse.ArgumentParser(description="Qwen3-TTS → ONNX sub-parts")
|
| 188 |
+
ap.add_argument("--model", required=True, help="HF id or local dir")
|
| 189 |
+
ap.add_argument("--device", choices=["cpu", "cuda"], default="cpu")
|
| 190 |
+
ap.add_argument("--precision", choices=["int4", "fp16", "fp32"], default="int4")
|
| 191 |
+
ap.add_argument("--components", nargs="*", default=None, help="subset to build")
|
| 192 |
+
ap.add_argument("--skip-download", action="store_true")
|
| 193 |
+
ap.add_argument("--no-isolate", action="store_true",
|
| 194 |
+
help="build everything in ONE process (may OOM on the talker). Default: "
|
| 195 |
+
"each component runs in its own subprocess for fresh memory.")
|
| 196 |
+
ap.add_argument("--_child", action="store_true", help=argparse.SUPPRESS) # internal
|
| 197 |
+
args = ap.parse_args()
|
| 198 |
+
|
| 199 |
+
model_dir = resolve_model(args.model, args.skip_download)
|
| 200 |
+
mtype = detect_type(model_dir)
|
| 201 |
+
if mtype == "qwen3_tts":
|
| 202 |
+
kind, comp_map = "tts", TTS_COMPONENTS
|
| 203 |
+
elif mtype.startswith("qwen3_tts_tokenizer"):
|
| 204 |
+
kind, comp_map = "tokenizer", TOKENIZER_COMPONENTS
|
| 205 |
+
else:
|
| 206 |
+
raise ValueError(f"Unknown model_type '{mtype}' in {model_dir}")
|
| 207 |
+
|
| 208 |
+
if args.components:
|
| 209 |
+
components = [c for c in args.components if c in comp_map]
|
| 210 |
+
else:
|
| 211 |
+
components = list(comp_map)
|
| 212 |
+
# speaker_encoder exists only in `base` checkpoints — skip it by default elsewhere
|
| 213 |
+
# (it raises in the wrapper otherwise). Still buildable if requested explicitly.
|
| 214 |
+
tts_type = json.loads((model_dir / "config.json").read_text()).get("tts_model_type")
|
| 215 |
+
if "speaker_encoder" in components and tts_type != "base":
|
| 216 |
+
components.remove("speaker_encoder")
|
| 217 |
+
# talker and talker_cache hold the SAME transformer weights (no-cache vs KV-cache
|
| 218 |
+
# forward). Shipping both duplicates ~870 MB. Default to talker_cache only (it does
|
| 219 |
+
# prefill+decode, is faster O(n), and is what inference auto-uses); the plain no-cache
|
| 220 |
+
# `talker` is still buildable explicitly if a simpler graph is wanted.
|
| 221 |
+
if "talker" in components and "talker_cache" in components:
|
| 222 |
+
components.remove("talker")
|
| 223 |
+
out_dir = output_root(args.device, args.precision)
|
| 224 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 225 |
+
print(f"Model {args.model} [{kind}] | {args.device}/{args.precision} → {out_dir}")
|
| 226 |
+
print(f"Components: {components}\n")
|
| 227 |
+
|
| 228 |
+
# ── memory-isolation: re-invoke this script once per component in a fresh subprocess.
|
| 229 |
+
# The talker's big dynamo/float16 pass commits a lot of virtual memory; stacking it on
|
| 230 |
+
# top of others in one process OOM/pagefile-kills on Windows. One process per component
|
| 231 |
+
# releases everything between builds. (Reuses sys.executable = the resolved uv env.)
|
| 232 |
+
if not args._child and not args.no_isolate and len(components) > 1:
|
| 233 |
+
print(f"Isolating {len(components)} components in subprocesses (avoids OOM)…\n")
|
| 234 |
+
ok, failed = [], []
|
| 235 |
+
for comp in components:
|
| 236 |
+
cmd = [sys.executable, str(Path(__file__).resolve()),
|
| 237 |
+
"--model", args.model, "--device", args.device,
|
| 238 |
+
"--precision", args.precision, "--components", comp, "--_child"]
|
| 239 |
+
if args.skip_download:
|
| 240 |
+
cmd.append("--skip-download")
|
| 241 |
+
print(f"=== [subprocess] {comp} ===")
|
| 242 |
+
rc = subprocess.run(cmd).returncode
|
| 243 |
+
(ok if rc == 0 else failed).append(comp)
|
| 244 |
+
if rc != 0:
|
| 245 |
+
print(f" [warn] component '{comp}' failed (rc {rc}) — continuing")
|
| 246 |
+
print()
|
| 247 |
+
write_manifest(out_dir, args.model, args.device, args.precision, kind, ok)
|
| 248 |
+
print(f"\nDone → {out_dir} built={ok}" + (f" FAILED={failed}" if failed else ""))
|
| 249 |
+
return
|
| 250 |
+
|
| 251 |
+
# direct build (single component, --_child subprocess, or --no-isolate)
|
| 252 |
+
built = []
|
| 253 |
+
for name in components:
|
| 254 |
+
print(f"=== {name} ===")
|
| 255 |
+
build_component(name, comp_map[name], model_dir, out_dir, args.device, args.precision)
|
| 256 |
+
built.append(name); print()
|
| 257 |
+
if not args._child: # children skip manifest; parent writes it
|
| 258 |
+
write_manifest(out_dir, args.model, args.device, args.precision, kind, built)
|
| 259 |
+
print(f"\nDone → {out_dir}")
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
if __name__ == "__main__":
|
| 263 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen3-TTS-VoiceDesign → ONNX (CPU INT4) build/run/eval dependencies
|
| 2 |
+
olive-ai
|
| 3 |
+
onnxruntime>=1.20
|
| 4 |
+
onnxruntime-genai # ModelBuilder (create_model) for the Qwen3 LLM sub-part
|
| 5 |
+
torch
|
| 6 |
+
transformers>=5.10
|
| 7 |
+
safetensors
|
| 8 |
+
numpy
|
| 9 |
+
soundfile
|
| 10 |
+
librosa
|