vits-csmsc / README.md
iiiting's picture
Add files using upload-large-folder tool
4d6c834 verified
|
Raw History Blame Contribute Delete
14 kB
---
license: apache-2.0
pipeline_tag: text-to-speech
language:
- zh
tags:
- vits
- text-to-speech
- tts
- csmsc
- paddlespeech
- chinese
- robotics
- IB-Robot
- ascend
- edge-deployment
---
# Model Card for VITS-CSMSC (IB-Robot, hybrid CPU + Ascend 310P)
PaddleSpeech VITS-CSMSC β€” single-speaker Mandarin text-to-speech, 22.05 kHz β€”
packaged for the [IB-Robot](https://atomgit.com/openeuler/IB_Robot) framework
as a **hybrid deployment**: the text encoder and duration predictor run as ONNX
on CPU, and the flow decoder (which contains the vocoder and dominates compute)
runs as an `.om` on Ascend 310P1.
This is the speech-output stage of the robot pipeline. Accepts arbitrary
Chinese text; the voice is the fixed CSMSC speaker.
## Deployments
| deployment | backend | flow artifact | frames | notes |
|---|---|---|---:|---|
| `ascend_310p` | ONNX CPU + Ascend ACL | `vits_flow_decoder_dynamic_fp16.om` | variable | **recommended**, any utterance length |
| `torch_cpu` | ONNX Runtime CPU | `artifacts/onnx/vits_flow_decoder.onnx` | variable | full CPU reference path |
Two fixed-shape flow decoders also ship in the repo without their own
deployment entry, matching the convention across this organisation that one
hardware target carries one deployment:
| file | frames | audio | notes |
|---|---:|---:|---|
| `vits_flow_decoder_t639_fp16.om` | 639 | 7.42 s | ~20 % lower flow latency than dynamic, 44 % of its size |
| `vits_flow_decoder_t681_fp16.om` | 681 | 7.91 s | second fixed-shape build |
Both are declared in `bundle.files` and load by pointing an ACL session at the
file directly. They accept **exactly** their frame count, so an utterance has
to be padded or trimmed to fit; the dynamic build takes any length, which is
why it is the one wired to `ascend_310p`. Accuracy for all three is in
[Validation](#validation) β€” they are within 1e-5 cosine of each other.
The `ascend_310p` row is a **composite deployment** in the manifest: there is no
single `runtime_profile`, because there is no single backend. `role_identities`
and `role_runtime_profiles` name the backend per role β€” `onnx` on CPU for
`text_encoder` and `duration`, `ascend`/`acl` for `flow_decoder`. `torch_cpu`
runs all three roles on ONNX Runtime and so keeps one shared profile.
The two host-side steps between duration and flow (`duration_expand_path`,
`sample_z_p`) are listed in each deployment's `metadata.host_stages`; they are
not model roles and have no artifact.
Shared contract: `tensor_model/vits/synthesize`
(`tts.phone_ids [1,-1] -> tts.audio [1,-1]`), preprocessing
`paddlespeech-zh-frontend-jieba-pypinyin-phoneid-v1`, output
`mono-float32-pcm-22050hz-v1`, logical revision
`paddlespeech-vits-csmsc@onnx-export-v1`.
`ascend_310p` uses the dynamic-shape flow decoder, so no padding or trimming
is needed. For latency-sensitive use with a known utterance length, load one of
the two fixed-shape builds above directly.
## Pipeline
```text
Chinese text
β†’ PaddleSpeech frontend / pypinyin β†’ phone ids
β†’ text encoder (ONNX, CPU) β†’ x, m_p, logs_p (192-d), x_mask
β†’ duration predictor (ONNX, CPU) β†’ logw
β†’ host: alignment expansion + z_p (see below)
β†’ flow decoder (.om, Ascend 310P1) β†’ waveform
β†’ 22.05 kHz mono wav
```
### Host stage (between duration and flow)
This is not a thin glue layer β€” it is part of the model's semantics and must be
reproduced exactly. See `assets/reference_impl/run_vits_hybrid_310p.py`:
```python
dur = ceil(exp(logw) * float32(x_mask)) # per-phone frame counts
path = generate_path(dur, ...) # [1, T_text, T_frames] alignment
m = (path.transpose @ m_p.transpose).transpose # expand to frames
logs = (path.transpose @ logs_p.transpose).transpose
y_mask = ones([1, 1, T_frames])
z_p = m + randn_like(m) * exp(logs) * 0.667 # noise_scale 0.667
```
The reference uses **seed 1234** for reproducibility; the sampling of `z_p` is
genuinely stochastic, so runs with different seeds produce different (equally
valid) audio, and bit-exact comparison against a reference requires pinning it.
## Tensor contract
```text
text encoder text int64 [1, -1] phone ids
text_lengths int64 [1]
β†’ x float32 [1, 192, -1] outputs are positional and
m_p float32 [1, 192, -1] come back in this order
logs_p float32 [1, 192, -1]
x_mask bool [1, 1, -1]
duration x float32 [1, 192, -1]
x_mask bool [1, 1, -1] bool, not float
β†’ logw float32 [1, 1, -1]
flow decoder z_p float32 [1, 192, T] T = 639 / 681 / -1
y_mask float32 [1, 1, T]
β†’ wav float32 [1, T*256] 256 samples per frame
```
Sample rate 22050 Hz, hop 256 β†’ one frame β‰ˆ 11.61 ms.
## Repository Structure
- `inference_manifest.json` β€” deployment routing (schema v3)
- `assets/adapter.json` β€” adapter identity (`vits` / `synthesize`)
- `assets/phone_id_map.txt` — 268-entry phone→id map used by the frontend
- `assets/snapshot_iter_150000.pdz` β€” the upstream Paddle training checkpoint
(1.1 GB), byte-identical to the one in `vits_csmsc_ckpt_1.4.0.zip`
- `assets/default.yaml` β€” that checkpoint's own training config
- `assets/reference_impl/run_vits_hybrid_310p.py` β€” the hybrid runner, and the
normative source for the host stage above
- `assets/reference_impl/synthesize_vits_310p.py` β€” text β†’ wav CLI
- `artifacts/onnx/*.onnx` β€” the three ONNX graphs (CPU stages + CPU reference)
- `artifacts/ascend_310p/*.om` β€” three flow decoder builds
## Source Model
Upstream is **PaddleSpeech VITS-CSMSC**, released checkpoint
**`vits_csmsc_ckpt_1.4.0`**. The manifest's logical revision is
`paddlespeech-vits-csmsc@onnx-export-v1` β€” it names the ONNX export this bundle
runs, not the upstream release tag, which is `1.4.0`.
```bash
wget https://paddlespeech.cdn.bcebos.com/Parakeet/released_models/vits/\
vits_csmsc_ckpt_1.4.0.zip
# 1,027,863,302 bytes
# sha256 994340036777b24f3bf02967bf37f570d40e5514b54ff0001c5b4732921ae064
```
The archive holds exactly three files, and **all three are shipped here
byte-for-byte** (sha256 verified against a fresh download and extraction):
| shipped as | in archive | bytes | sha256 |
|---|---|---:|---|
| `assets/snapshot_iter_150000.pdz` | `snapshot_iter_150000.pdz` | 1,116,898,380 | `ef0faad8186cdd1aa278b61e0b2a3cb7dee9216d1b84f1bc760ec887bcd935c7` |
| `assets/default.yaml` | `default.yaml` | 7,835 | `7eebdd044a663f2485b6abd54f6179e90da64c0c7c109778295f9a23be6dcbfc` |
| `assets/phone_id_map.txt` | `phone_id_map.txt` | 2,128 | `ca00d60618a00a2dc1e42002faaa9024c9e5057e04b1e834325950111cee59c3` |
`phone_id_map.txt` pins the phone inventory, and therefore the text frontend
contract, to this exact release. `default.yaml` is the checkpoint's own training
config and independently confirms the numbers this bundle's tensor contract
depends on: `fs: 22050`, `n_shift: 256` (the 256 samples per frame above) and
`hidden_channels: 192` (the 192-d `z_p`).
Shipping the checkpoint follows the convention in the rest of this
organisation, where the upstream weights live under `assets/`. Note that it is
**not** the artifact any deployment loads β€” every deployment runs the ONNX
graphs below. It is there so the bundle carries its own provenance.
The three ONNX graphs were exported from that checkpoint in separate passes,
which is why their opsets differ (12 / 13 / 11):
| file | bytes | opset | sha256 |
|---|---:|---:|---|
| `vits_text_encoder_clean.onnx` | 55,329,745 | 12 | `15c5fb01538ce38c4748c9bc5892fc0af9bee7c65b228cd116651237a3f72536` |
| `vits_duration.onnx` | 2,644,862 | 13 | `f75fd325801a62976ecea0f52ba03a384197f574fedf0486598e5c0fc840c89c` |
| `vits_flow_decoder.onnx` | 86,596,889 | 11 | `3cb9dfe036b843abce71b867626f23d60745c0f0f35f29787ecec7fbfaf0f7fa` |
| `vits_flow_decoder_t639_fp16.om` | 91,708,818 | β€” | `b7da515895536b7baf75314b908344164efb916ffc8cc2246c6dac0e4c4cf91b` |
| `vits_flow_decoder_t681_fp16.om` | 91,706,422 | β€” | `df192baf48cefb732f52945cd411d3c88bbd310cac784dc51e11a8a7ed0d2b61` |
| `vits_flow_decoder_dynamic_fp16.om` | 210,986,822 | β€” | `c8b26160a0bc7e4de987424fab228bcc8dfd3fb053bd910737443999412c40d8` |
Shipping the checkpoint does **not** make the ONNX byte-reproducible from it:
the export script is not part of this bundle, and PaddleSpeech static-model
export behaviour changed with PaddlePaddle β‰₯ 2.5. What *is* established is the
accuracy of the export β€” cosine `0.9999999999998159` against the Paddle
reference, below β€” plus the exact checkpoint and config it came from.
## Validation
All measured on a real Ascend 310P1, against ONNX Runtime CPU fp32.
**ONNX export vs Paddle reference** (text encoder + duration) β€” the export
itself is lossless:
```text
cosine 0.9999999999998159
max_abs 4.291534423828125e-06
```
**Flow decoder OM vs ONNX**, on the flow subgraph alone:
| build | cosine | max_abs | latency mean | p95 | subgraph RTF |
|---|---:|---:|---:|---:|---:|
| t639 | 0.9999842557218173 | 0.01094835 | 96.17 ms | 96.38 ms | 0.01296 |
| t681 | 0.9999692545131995 | 0.01450025 | 102.72 ms | β€” | 0.01299 |
Those are the conversion-time numbers. They have since been **independently
reproduced on the shipped artifacts** β€” same board, same `artifacts/onnx/
vits_flow_decoder.onnx` reference, but with `z_p` built through the real
encoder β†’ duration β†’ host-alignment path rather than synthetic tensors
(`_tools/board/verify_vits_flow_om_vs_onnx.py`):
| build | frames | cosine | max_abs | rel_l2 | latency mean | p95 | subgraph RTF |
|---|---:|---:|---:|---:|---:|---:|---:|
| t639 | 639 | 0.99998847 | 0.008860 | 0.00480 | 95.89 ms | 96.00 ms | 0.01292 |
| t681 | 681 | 0.99998923 | 0.008666 | 0.00464 | 102.47 ms | 102.71 ms | 0.01296 |
| dynamic | 399 | 0.99998542 | 0.006929 | 0.00540 | 77.32 ms | 77.90 ms | 0.01669 |
Latency matches the conversion report to within 0.3 ms on every build, and
accuracy comes out slightly *better* on realistic `z_p` than on the synthetic
input used at conversion time. The dynamic build's higher RTF at 399 frames is
the fixed per-call overhead spread over less audio, not a slower kernel β€” at
639 frames it lands within ~40 ms of the fixed build (see below).
**End-to-end hybrid pipeline**, 20 runs, 7.42 s of output audio. Measured with
the fixed **t639** build, which is what the reference runner loads
(`assets/reference_impl/run_vits_hybrid_310p.py:15`); the same pipeline on the
`ascend_310p` dynamic build is the `baseline` row two tables down, 367.63 ms:
```text
cosine_vs_onnx 0.9999853236698978
max_abs 0.008778512477874756
latency mean 327.38 ms p50 323.81 ms p95 355.89 ms
RTF 0.0441
```
Per-stage breakdown (mean, ms):
| stage | backend | ms | share |
|---|---|---:|---:|
| text encoder | ONNX CPU | 48.71 | 15% |
| duration | ONNX CPU | 25.38 | 8% |
| host alignment + z_p | numpy CPU | 156.64 | 48% |
| flow decoder | Ascend OM | 96.53 | 29% |
The host alignment stage is now the single largest cost β€” larger than the NPU
work it feeds. It is pure numpy (`generate_path` plus two matmuls) and is the
obvious next optimisation target; moving it onto the NPU or vectorising it
would cut end-to-end latency by roughly a third.
**Dynamic build, variable text length:**
| sample | phones | frames | audio | latency mean | RTF |
|---|---:|---:|---:|---:|---:|
| baseline | 101 | 639 | 7.4188 s | 367.63 ms | 0.04955 |
| new_text | 144 | 915 | 10.6231 s | 414.97 ms | 0.03906 |
Latency grows sub-linearly with length, so RTF improves on longer utterances.
The dynamic build costs ~40 ms over the fixed t639 build at the same length.
## Usage
```python
from inference_manifest import load_inference_manifest
validated = load_inference_manifest("models/vits-csmsc", "ascend_310p")
```
The hybrid path needs both an ONNX Runtime CPU session (encoder, duration) and
an ACL session (flow), plus the host stage between them β€” see
`assets/reference_impl/run_vits_hybrid_310p.py`, which is the normative
implementation.
## Known Limitations
- **Not a pure-NPU TTS.** Text encoder and duration remain on CPU; only the
flow decoder is compiled to OM.
- **Chinese only.** English and other languages are not a validation target.
- **Fixed speaker.** Single-speaker CSMSC voice, no speaker conditioning.
- **The two extra fixed-shape OMs require exact frame counts** (639 / 681).
The `ascend_310p` deployment uses the dynamic build and has no such limit;
the fixed builds are for callers who know their length and want the ~40 ms.
- Output is stochastic (`noise_scale = 0.667`) unless the RNG seed is pinned.
## Environment
Validated on: Ascend 310P1 with CANN and `ais_bench`, Python 3.11.6,
ONNX Runtime 1.23.2.
## License
Code and packaging: Apache-2.0. Model weights follow the upstream
[PaddleSpeech](https://github.com/PaddlePaddle/PaddleSpeech) license
(Apache-2.0); the CSMSC corpus is subject to its own terms.
## Citation
@inproceedings{vits2021,
title = {Conditional Variational Autoencoder with Adversarial Learning for End-to-End Text-to-Speech},
author = {Kim, Jaehyeon and Kong, Jungil and Son, Juhee},
booktitle = {arXiv preprint arXiv:2106.06103},
year = {2021}
}
@article{paddlespeech2022,
title = {PaddleSpeech: An Easy-to-Use All-in-One Speech Toolkit},
author = {Zhang, Hui and Yuan, Tian and Chen, Junkun and Li, Xintong and Zheng, Renjie and Huang, Yuxin and Chen, Xiaojie and Gong, Enlei and Chen, Zeyu and Hu, Xiaoguang and Yu, Dianhai and Ma, Yanjun and Huang, Liang},
journal = {arXiv preprint arXiv:2205.12007},
year = {2022}
}
@software{ib_robot,
title = {IB-Robot: Intelligence Boom Robot},
url = {https://atomgit.com/openeuler/IB_Robot},
license = {Apache-2.0}
}