Release 1.0.0: Chinese TTS (Inflect-Micro-v2 arch + BigVGAN), ONNX + AX650 axmodel + checkpoints
Browse files- .gitattributes +4 -0
- README.md +101 -0
- checkpoints/base300.pt +3 -0
- checkpoints/female_gan.pt +3 -0
- checkpoints/male_gan.pt +3 -0
- models/acoustic_female.onnx +3 -0
- models/acoustic_female.onnx.data +3 -0
- models/acoustic_male.onnx +3 -0
- models/acoustic_male.onnx.data +3 -0
- models/bigvgan_base.axmodel +3 -0
- models/bigvgan_base.onnx +3 -0
- models/bigvgan_base.onnx.data +3 -0
- models/model_meta.json +75 -0
- python/cn_frontend/__init__.py +2 -0
- python/cn_frontend/pinyin_to_sequence.py +27 -0
- python/cn_frontend/symbols.py +20 -0
- python/infer_board.py +247 -0
- python/infer_onnx.py +50 -0
- python/requirements.txt +6 -0
- python/vocoder_utils.py +40 -0
- reports/EVALUATION.md +57 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ 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 |
+
models/acoustic_female.onnx.data filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
models/acoustic_male.onnx.data filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
models/bigvgan_base.axmodel filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
models/bigvgan_base.onnx.data filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- zh
|
| 5 |
+
tags:
|
| 6 |
+
- text-to-speech
|
| 7 |
+
- tts
|
| 8 |
+
- chinese
|
| 9 |
+
- axera
|
| 10 |
+
- axmodel
|
| 11 |
+
- onnx
|
| 12 |
+
pipeline_tag: text-to-speech
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# Inflect-Micro-v2-zh
|
| 16 |
+
|
| 17 |
+
从零训练的中文 TTS:基于 [Inflect-Micro-v2](https://huggingface.co/owensong/Inflect-Micro-v2)
|
| 18 |
+
架构(Apache-2.0)+ [NVIDIA BigVGAN](https://github.com/NVIDIA/BigVGAN) 声码器(MIT)。
|
| 19 |
+
声学模型 6.7M 参数(text→100-band mel),可在 **AX650 系列 NPU** 上实时部署
|
| 20 |
+
(CPU + NPU 混合架构,RTF 0.15-0.19)。
|
| 21 |
+
|
| 22 |
+
## 文件说明
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
models/
|
| 26 |
+
├── acoustic_female.onnx (+ .data) # 女声 文本→mel(CPU fp32,ls=0.7)
|
| 27 |
+
├── acoustic_male.onnx (+ .data) # 男声 文本→mel(CPU fp32,ls=0.85)
|
| 28 |
+
├── bigvgan_base.onnx (+ .data) # 声码器 mel→wav(CPU fp32,动态帧)
|
| 29 |
+
├── bigvgan_base.axmodel # 声码器 mel→wav(AX650 NPU int8)
|
| 30 |
+
└── model_meta.json # 模型规格元数据
|
| 31 |
+
checkpoints/ # 训练权重(PyTorch)
|
| 32 |
+
├── base300.pt # 基础模型(AiShell-3,20 说话人)
|
| 33 |
+
├── female_gan.pt # 女声(CosyVoice 音色 + GAN)
|
| 34 |
+
└── male_gan.pt # 男声(SSB0609 音色 + GAN)
|
| 35 |
+
python/ # Python 推理 SDK
|
| 36 |
+
reports/ # 验证报告(可懂度/性能/量化)
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
## 快速开始(Python,CPU)
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pip install onnxruntime numpy soundfile pypinyin
|
| 43 |
+
|
| 44 |
+
python python/infer_onnx.py \
|
| 45 |
+
--text "你好世界,今天天气真不错,适合出去散步。" \
|
| 46 |
+
--acoustic models/acoustic_female.onnx \
|
| 47 |
+
--vocoder models/bigvgan_base.onnx \
|
| 48 |
+
--output out.wav
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## 快速开始(AX650 板,CPU+NPU 混合)
|
| 52 |
+
|
| 53 |
+
板上安装 `onnxruntime`、`pypinyin` 和 `axengine`(AXERA 提供):
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
python python/infer_board.py \
|
| 57 |
+
--text "人工智能技术正在改变我们的生活方式。" \
|
| 58 |
+
--acoustic models/acoustic_female.onnx \
|
| 59 |
+
--vocoder models/bigvgan_base.axmodel \
|
| 60 |
+
--output out.wav
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
`infer_board.py` 自动完成:文本分句 → 声学模型(CPU)→ mel 后处理
|
| 64 |
+
(静音门限 + 句尾拉伸)→ 声码器(NPU)分块拼接。
|
| 65 |
+
|
| 66 |
+
## 性能(AX650N 实测)
|
| 67 |
+
|
| 68 |
+
| 组件 | 耗时 | 说明 |
|
| 69 |
+
|---|---|---|
|
| 70 |
+
| acoustic(CPU fp32) | 0.30s/句 | 静态 max_mel=1024(单句 ≤10.9s) |
|
| 71 |
+
| vocoder(NPU int8) | 0.30s/块 | 512 mel 帧 = 5.46s 音频 |
|
| 72 |
+
| 整链路 RTF | 0.15-0.19 | 约 6x 实时 |
|
| 73 |
+
|
| 74 |
+
## 可懂度(SenseVoice,8 句测试)
|
| 75 |
+
|
| 76 |
+
| 音色 | CER |
|
| 77 |
+
|---|---|
|
| 78 |
+
| 女声(CPU 全 fp32) | 0.075 |
|
| 79 |
+
| 男声(CPU 全 fp32) | 0.108 |
|
| 80 |
+
|
| 81 |
+
## 说明
|
| 82 |
+
|
| 83 |
+
- 声学模型必须 CPU fp32:VITS 的 flow/duration 对 NPU 8/16-bit 量化系统性敏感
|
| 84 |
+
(int8/U16/BRecQ 全部验证失败);声码器 NPU int8 可用
|
| 85 |
+
- 长文本自动分句(≤250 tokens/句)+ 声码器分块拼接(重叠交叉淡化)
|
| 86 |
+
- 训练流程与复现脚本见 [GitHub 仓库](https://github.com/ml-inory/Inflect-Micro-v2-zh)
|
| 87 |
+
|
| 88 |
+
## 致谢
|
| 89 |
+
|
| 90 |
+
- [owensong/Inflect-Micro-v2](https://huggingface.co/owensong/Inflect-Micro-v2)(架构,Apache-2.0)
|
| 91 |
+
- [NVIDIA BigVGAN](https://github.com/NVIDIA/BigVGAN)(声码器,MIT)
|
| 92 |
+
- [FunAudioLLM/CosyVoice](https://github.com/FunAudioLLM/CosyVoice)(数据生成,Apache-2.0)
|
| 93 |
+
- [OpenSLR AISHELL-3](https://www.openslr.org/resources/93/)(中文语料)
|
| 94 |
+
- AXERA Pulsar2(AX 编译工具链)
|
| 95 |
+
|
| 96 |
+
完整第三方声明见 GitHub 仓库 `THIRD_PARTY_NOTICES.md`。
|
| 97 |
+
|
| 98 |
+
## License
|
| 99 |
+
|
| 100 |
+
[Apache-2.0](https://github.com/ml-inory/Inflect-Micro-v2-zh/blob/main/LICENSE)
|
| 101 |
+
(与原版 Inflect-Micro-v2 一致)
|
checkpoints/base300.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ddd749d15dda2d378a362314bd6bc61284edc1dec8ff99c4bec58b84ae813b3b
|
| 3 |
+
size 313040243
|
checkpoints/female_gan.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bc72f8e9ebf1f5728c259cc05f98d52b2d2096ec031fb4c7274d7fa420551bfd
|
| 3 |
+
size 26926933
|
checkpoints/male_gan.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e5873d35dfc6d690d5ce44079d5c4c61d6bbc12e320afc2eacd25232fbc5daff
|
| 3 |
+
size 26926933
|
models/acoustic_female.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8264431335447b241d0e6f30a46e9f47e94b93285fc5ee269f8676a8a3157c55
|
| 3 |
+
size 990534
|
models/acoustic_female.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:074985877003699d58897b03fd84d3c95c19d7fc59855465f9269510145cfeec
|
| 3 |
+
size 35846400
|
models/acoustic_male.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5a250387759f7417c15d236d9e75201c012d3c9bacef1a4ecf44ef33ca9dc230
|
| 3 |
+
size 989998
|
models/acoustic_male.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:033a94d8717b874fdb437c3569cc5583fb1d7bd123956d0d0dac978cb2b04017
|
| 3 |
+
size 35846400
|
models/bigvgan_base.axmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:44d12f6a8ceca75462873603405cd789f4f71261157ef28c31a718718238cb79
|
| 3 |
+
size 27678831
|
models/bigvgan_base.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:77d1fe4b14242776253713694f8d8abd2189551eb7d8d639f5893fc1553f61ae
|
| 3 |
+
size 1589797
|
models/bigvgan_base.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a5fc77bcc63b7b1ff8a62f2d410408067c013df14e7269c88e4a60c09b8c67fb
|
| 3 |
+
size 56754176
|
models/model_meta.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Inflect-Micro-v2-zh",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Chinese TTS: text-to-mel acoustic model (6.7M) + NVIDIA BigVGAN vocoder",
|
| 5 |
+
"pipeline": ["text", "mel", "wav"],
|
| 6 |
+
"sampling_rate": 24000,
|
| 7 |
+
"mel": {
|
| 8 |
+
"n_mels": 100,
|
| 9 |
+
"n_fft": 1024,
|
| 10 |
+
"hop_length": 256,
|
| 11 |
+
"win_length": 1024,
|
| 12 |
+
"fmin": 0,
|
| 13 |
+
"fmax": 12000,
|
| 14 |
+
"style": "bigvgan"
|
| 15 |
+
},
|
| 16 |
+
"models": {
|
| 17 |
+
"acoustic_female.onnx": {
|
| 18 |
+
"role": "text->mel (female voice)",
|
| 19 |
+
"backend": "CPU (onnxruntime, fp32)",
|
| 20 |
+
"inputs": {
|
| 21 |
+
"x": "int64[1,256] token ids (with blanks, max 256)",
|
| 22 |
+
"x_lengths": "int64[1]",
|
| 23 |
+
"noise_z": "float32[1,192,1024]"
|
| 24 |
+
},
|
| 25 |
+
"outputs": {
|
| 26 |
+
"mel": "float32[1,100,1024] log-mel",
|
| 27 |
+
"y_lengths": "int32[1] valid mel frames"
|
| 28 |
+
},
|
| 29 |
+
"length_scale": 0.7,
|
| 30 |
+
"max_text": 256,
|
| 31 |
+
"max_mel": 1024
|
| 32 |
+
},
|
| 33 |
+
"acoustic_male.onnx": {
|
| 34 |
+
"role": "text->mel (male voice)",
|
| 35 |
+
"backend": "CPU (onnxruntime, fp32)",
|
| 36 |
+
"inputs": {
|
| 37 |
+
"x": "int64[1,256]",
|
| 38 |
+
"x_lengths": "int64[1]",
|
| 39 |
+
"noise_z": "float32[1,192,1024]"
|
| 40 |
+
},
|
| 41 |
+
"outputs": {
|
| 42 |
+
"mel": "float32[1,100,1024]",
|
| 43 |
+
"y_lengths": "int32[1]"
|
| 44 |
+
},
|
| 45 |
+
"length_scale": 0.85,
|
| 46 |
+
"max_text": 256,
|
| 47 |
+
"max_mel": 1024
|
| 48 |
+
},
|
| 49 |
+
"bigvgan_base.onnx": {
|
| 50 |
+
"role": "mel->wav (vocoder, dynamic frames)",
|
| 51 |
+
"backend": "CPU (onnxruntime, fp32)",
|
| 52 |
+
"inputs": {"mel": "float32[1,100,T]"},
|
| 53 |
+
"outputs": {"wav": "float32[1,1,T*256]"}
|
| 54 |
+
},
|
| 55 |
+
"bigvgan_base.axmodel": {
|
| 56 |
+
"role": "mel->wav (vocoder, AX650 NPU)",
|
| 57 |
+
"backend": "NPU (axengine, int8, AX650/NPU3)",
|
| 58 |
+
"inputs": {"mel": "float32[1,100,512]"},
|
| 59 |
+
"outputs": {"wav": "float32[1,1,131072]"},
|
| 60 |
+
"compiled_by": "Pulsar2 7.0"
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
"performance": {
|
| 64 |
+
"ax650_cpu_acoustic_seconds": 0.30,
|
| 65 |
+
"ax650_npu_vocoder_seconds_per_block": 0.30,
|
| 66 |
+
"rtf_mixed_cpu_npu": "0.15-0.19"
|
| 67 |
+
},
|
| 68 |
+
"license": "apache-2.0",
|
| 69 |
+
"acknowledgements": [
|
| 70 |
+
"Inflect-Micro-v2 (owensong, Apache-2.0)",
|
| 71 |
+
"NVIDIA BigVGAN (MIT)",
|
| 72 |
+
"FunAudioLLM CosyVoice (Apache-2.0)",
|
| 73 |
+
"OpenSLR AISHELL-3"
|
| 74 |
+
]
|
| 75 |
+
}
|
python/cn_frontend/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .symbols import symbols, _symbol_to_id, get_phoneme_ids, PAD_ID, SPACE_ID
|
| 2 |
+
from .pinyin_to_sequence import text_to_sequence, pinyin_to_ids
|
python/cn_frontend/pinyin_to_sequence.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Convert Chinese text to pinyin character sequence."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from pypinyin import lazy_pinyin, Style
|
| 5 |
+
from .symbols import get_phoneme_ids
|
| 6 |
+
|
| 7 |
+
_whitespace_re = re.compile(r"\s+")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def text_to_sequence(text: str) -> list[int]:
|
| 11 |
+
"""Chinese text -> pinyin character IDs.
|
| 12 |
+
|
| 13 |
+
Example: "你好" -> "ni3 hao3" -> [n, i, 3, <space>, h, a, o, 3]
|
| 14 |
+
"""
|
| 15 |
+
text = re.sub(_whitespace_re, "", text)
|
| 16 |
+
|
| 17 |
+
# Convert to pinyin with tone digits
|
| 18 |
+
py_list = lazy_pinyin(text, style=Style.TONE3, neutral_tone_with_five=True)
|
| 19 |
+
|
| 20 |
+
# Join with spaces between words
|
| 21 |
+
pinyin_str = " ".join(py_list)
|
| 22 |
+
|
| 23 |
+
return get_phoneme_ids(pinyin_str)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def pinyin_to_ids(pinyin_str: str) -> list[int]:
|
| 27 |
+
return get_phoneme_ids(pinyin_str)
|
python/cn_frontend/symbols.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chinese pinyin character-level symbol set."""
|
| 2 |
+
|
| 3 |
+
_pad = "_"
|
| 4 |
+
|
| 5 |
+
# All possible pinyin characters (a-z) plus tone digits (1-5)
|
| 6 |
+
_pinyin_chars = "abcdefghijklmnopqrstuvwxyz12345 "
|
| 7 |
+
|
| 8 |
+
# Chinese punctuation
|
| 9 |
+
_punct = ",。!?;:、…—""''()《》·"
|
| 10 |
+
|
| 11 |
+
symbols = [_pad] + list(_pinyin_chars) + list(_punct)
|
| 12 |
+
|
| 13 |
+
_symbol_to_id = {s: i for i, s in enumerate(symbols)}
|
| 14 |
+
|
| 15 |
+
PAD_ID = 0
|
| 16 |
+
SPACE_ID = _symbol_to_id.get(" ", len(symbols))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_phoneme_ids(phonemes: str) -> list[int]:
|
| 20 |
+
return [_symbol_to_id.get(p, PAD_ID) for p in phonemes]
|
python/infer_board.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AX board inference: acoustic (CPU/ONNX) + BigVGAN (NPU/axmodel).
|
| 2 |
+
|
| 3 |
+
Run on the AX board with:
|
| 4 |
+
python3 infer_board.py --text "你好" --acoustic acoustic_female.onnx \
|
| 5 |
+
--vocoder bigvgan_base.axmodel --output out.wav
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
+
from cn_frontend import text_to_sequence
|
| 16 |
+
|
| 17 |
+
MAX_TEXT = 256
|
| 18 |
+
MAX_MEL = 1024
|
| 19 |
+
VOC_CHUNK = 512 # BigVGAN 静态输入帧数(5.46s@24kHz)
|
| 20 |
+
VOC_OVERLAP = 48 # 分块重叠帧(0.5s),交叉淡化消除边界爆音
|
| 21 |
+
MAX_SENT_TOKENS = 250 # 单句 token 上限(acoustic 上限 256,留 6 余量)
|
| 22 |
+
MEL_GATE_LO = -3.5 # mel 弱帧门限:低于此压到 floor(消除静音段底噪)
|
| 23 |
+
MEL_GATE_HI = -2.2 # 过渡区上界,高于此帧保持不动
|
| 24 |
+
LENGTH_SCALE = 0.7 # 语速(音节时长校正,接近自然节奏)
|
| 25 |
+
TAIL_STRETCH_OLD = 20 # 句尾拉伸:最后 N 帧
|
| 26 |
+
TAIL_STRETCH_NEW = 45 # 拉伸到 N 帧(解决句尾音节过短/尾字被吞)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def load_acoustic(path):
|
| 30 |
+
"""Load acoustic ONNX (CPU, fp32)."""
|
| 31 |
+
import onnxruntime as ort
|
| 32 |
+
so = ort.SessionOptions()
|
| 33 |
+
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 34 |
+
return ort.InferenceSession(path, so, providers=["CPUExecutionProvider"])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load_vocoder(path):
|
| 38 |
+
"""Load BigVGAN axmodel (NPU via axengine)."""
|
| 39 |
+
if path.endswith(".onnx"):
|
| 40 |
+
import onnxruntime as ort
|
| 41 |
+
so = ort.SessionOptions()
|
| 42 |
+
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 43 |
+
return ort.InferenceSession(path, so, providers=["CPUExecutionProvider"])
|
| 44 |
+
import axengine as axe
|
| 45 |
+
try:
|
| 46 |
+
return axe.InferenceSession(path, providers=["AxEngineExecutionProvider"])
|
| 47 |
+
except Exception:
|
| 48 |
+
return axe.InferenceSession(
|
| 49 |
+
path, providers=["RemoteAXExecutionProvider"],
|
| 50 |
+
provider_options={"host": os.environ.get("AX_BOARD", "127.0.0.1"),
|
| 51 |
+
"port": "18500"})
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def split_sentences(text, max_tokens=MAX_SENT_TOKENS):
|
| 55 |
+
"""Split long text into sentences that fit the acoustic model."""
|
| 56 |
+
ids = text_to_sequence(text)
|
| 57 |
+
tokens = [0]
|
| 58 |
+
for pid in ids:
|
| 59 |
+
tokens.append(pid)
|
| 60 |
+
tokens.append(0)
|
| 61 |
+
if len(tokens) <= max_tokens:
|
| 62 |
+
return [text]
|
| 63 |
+
# split on punctuation, then on hard limit
|
| 64 |
+
import re
|
| 65 |
+
parts = re.split(r"([,。!?;:、,.!?;:])", text)
|
| 66 |
+
sents, cur = [], ""
|
| 67 |
+
for p in parts:
|
| 68 |
+
cand = cur + p
|
| 69 |
+
cand_tokens = len([0]) + 2 * len(text_to_sequence(cand))
|
| 70 |
+
if len(text_to_sequence(cand)) * 2 + 1 > max_tokens and cur:
|
| 71 |
+
sents.append(cur)
|
| 72 |
+
cur = p
|
| 73 |
+
else:
|
| 74 |
+
cur = cand
|
| 75 |
+
if cur:
|
| 76 |
+
sents.append(cur)
|
| 77 |
+
# hard fallback: cut by characters
|
| 78 |
+
out = []
|
| 79 |
+
for s in sents:
|
| 80 |
+
while len(text_to_sequence(s)) * 2 + 1 > max_tokens:
|
| 81 |
+
cut = max(1, (max_tokens - 1) // 2)
|
| 82 |
+
out.append(s[:cut])
|
| 83 |
+
s = s[cut:]
|
| 84 |
+
if s:
|
| 85 |
+
out.append(s)
|
| 86 |
+
return [s for s in out if s.strip()]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def text_to_inputs(text, noise_scale, seed):
|
| 90 |
+
np.random.seed(seed)
|
| 91 |
+
ids = text_to_sequence(text)
|
| 92 |
+
tokens = [0]
|
| 93 |
+
for pid in ids:
|
| 94 |
+
tokens.append(pid)
|
| 95 |
+
tokens.append(0)
|
| 96 |
+
n = min(len(tokens), MAX_TEXT)
|
| 97 |
+
x = np.zeros((1, MAX_TEXT), dtype=np.int64)
|
| 98 |
+
x[0, :n] = tokens[:n]
|
| 99 |
+
x_lengths = np.array([n], dtype=np.int64)
|
| 100 |
+
noise_z = np.random.randn(1, 192, MAX_MEL).astype(np.float32)
|
| 101 |
+
if noise_scale != 0.3:
|
| 102 |
+
noise_z = noise_z * (noise_scale / 0.3)
|
| 103 |
+
return x, x_lengths, noise_z
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def acoustic_to_mel(acoustic, x, x_lengths, noise_z):
|
| 107 |
+
mel, y_lengths = acoustic.run(None, {
|
| 108 |
+
"x": x, "x_lengths": x_lengths, "noise_z": noise_z,
|
| 109 |
+
})
|
| 110 |
+
return mel[:, :, :int(y_lengths[0])]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def mel_soft_gate(mel, thr_lo=MEL_GATE_LO, thr_hi=MEL_GATE_HI, floor=-11.5):
|
| 114 |
+
"""压平预测 mel 的弱帧(静音/停顿)以消除渲染底噪。
|
| 115 |
+
只调整帧能量 < thr_lo 的帧(向 floor 收敛)和过渡区,
|
| 116 |
+
强语音帧(>= thr_hi)完全保持。"""
|
| 117 |
+
out = mel.copy()
|
| 118 |
+
fe = out.max(axis=1)
|
| 119 |
+
for i in range(mel.shape[2]):
|
| 120 |
+
f = fe[0, i]
|
| 121 |
+
if f < thr_lo:
|
| 122 |
+
target = floor
|
| 123 |
+
elif f < thr_hi:
|
| 124 |
+
t = (f - thr_lo) / (thr_hi - thr_lo)
|
| 125 |
+
target = floor * (1 - t) + f * t
|
| 126 |
+
else:
|
| 127 |
+
continue
|
| 128 |
+
out[:, :, i] = out[:, :, i] - f + target
|
| 129 |
+
return out
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def tail_stretch(mel, n_old=TAIL_STRETCH_OLD, n_new=TAIL_STRETCH_NEW):
|
| 133 |
+
"""句尾 mel 拉伸:最后 n_old 帧线性插值到 n_new 帧。
|
| 134 |
+
模型对句尾音节 duration 预测偏短(如 11 帧 vs 参考 43 帧),
|
| 135 |
+
拉伸后尾字清晰完整。"""
|
| 136 |
+
T = mel.shape[2]
|
| 137 |
+
if T <= n_old:
|
| 138 |
+
return mel
|
| 139 |
+
tail = mel[:, :, -n_old:]
|
| 140 |
+
xo = np.linspace(0, 1, n_old)
|
| 141 |
+
xn = np.linspace(0, 1, n_new)
|
| 142 |
+
nt = np.stack([np.interp(xn, xo, tail[0, c]) for c in range(mel.shape[1])])
|
| 143 |
+
return np.concatenate([mel[:, :, :-n_old], nt[None]], axis=2)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def vocoder_chunked(vocoder, mel):
|
| 147 |
+
"""Run BigVGAN on arbitrary-length mel via overlapped chunks + crossfade."""
|
| 148 |
+
mel = mel.astype(np.float32)
|
| 149 |
+
T = mel.shape[2]
|
| 150 |
+
if T <= VOC_CHUNK:
|
| 151 |
+
m = np.pad(mel, ((0, 0), (0, 0), (0, VOC_CHUNK - T)),
|
| 152 |
+
constant_values=-11.5)
|
| 153 |
+
return vocoder.run(None, {"mel": m})[0][0, 0]
|
| 154 |
+
hop = VOC_CHUNK - VOC_OVERLAP
|
| 155 |
+
starts = list(range(0, T, hop))
|
| 156 |
+
frame = 256
|
| 157 |
+
wav = np.zeros(T * frame)
|
| 158 |
+
for i, s in enumerate(starts):
|
| 159 |
+
v = min(VOC_CHUNK, T - s)
|
| 160 |
+
block = mel[:, :, s:s + VOC_CHUNK]
|
| 161 |
+
if block.shape[2] < VOC_CHUNK:
|
| 162 |
+
block = np.pad(block, ((0, 0), (0, 0), (0, VOC_CHUNK - block.shape[2])),
|
| 163 |
+
constant_values=-11.5)
|
| 164 |
+
c = vocoder.run(None, {"mel": block})[0][0, 0]
|
| 165 |
+
c = c[:v * frame]
|
| 166 |
+
base = s * frame
|
| 167 |
+
ov = 0
|
| 168 |
+
if i > 0:
|
| 169 |
+
ov = min(VOC_OVERLAP * frame, base, len(c))
|
| 170 |
+
fade = np.linspace(0, 1, ov)
|
| 171 |
+
wav[base - ov:base] = wav[base - ov:base] * (1 - fade) + c[:ov] * fade
|
| 172 |
+
wav[base + ov:base + len(c)] = c[ov:]
|
| 173 |
+
return wav
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def compress_pauses(wav, sr=24000, min_pause_ms=90, target_ms=60):
|
| 177 |
+
"""Compress long silences to reduce choppy rhythm."""
|
| 178 |
+
win, hop = int(sr * 0.01), int(sr * 0.005)
|
| 179 |
+
n = (len(wav) - win) // hop
|
| 180 |
+
e = np.array([np.sqrt(np.mean(wav[i*hop:i*hop+win]**2)) for i in range(n)])
|
| 181 |
+
thr = max(e.max() * 0.12, 0.008)
|
| 182 |
+
sil = e < thr
|
| 183 |
+
# find silence runs
|
| 184 |
+
runs = []
|
| 185 |
+
i = 0
|
| 186 |
+
while i < len(sil):
|
| 187 |
+
if sil[i]:
|
| 188 |
+
j = i
|
| 189 |
+
while j < len(sil) and sil[j]:
|
| 190 |
+
j += 1
|
| 191 |
+
runs.append((i, j))
|
| 192 |
+
i = j
|
| 193 |
+
else:
|
| 194 |
+
i += 1
|
| 195 |
+
keep = np.ones(len(wav), dtype=bool)
|
| 196 |
+
for s, e_ in runs:
|
| 197 |
+
dur_ms = (e_ - s) * hop / sr * 1000
|
| 198 |
+
if dur_ms > min_pause_ms:
|
| 199 |
+
s0, s1 = s * hop, min(e_ * hop + win, len(wav))
|
| 200 |
+
target = int(target_ms / 1000 * sr)
|
| 201 |
+
# keep first `target` samples of the pause
|
| 202 |
+
if s1 - s0 > target:
|
| 203 |
+
keep[s0 + target:s1] = False
|
| 204 |
+
out = wav[keep]
|
| 205 |
+
return out
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def synthesize(acoustic, vocoder, text, noise_scale=0.3, seed=0):
|
| 209 |
+
sents = split_sentences(text)
|
| 210 |
+
pieces = []
|
| 211 |
+
for s in sents:
|
| 212 |
+
x, x_lengths, noise_z = text_to_inputs(s, noise_scale, seed)
|
| 213 |
+
mel = acoustic_to_mel(acoustic, x, x_lengths, noise_z)
|
| 214 |
+
mel = mel_soft_gate(mel)
|
| 215 |
+
mel = tail_stretch(mel)
|
| 216 |
+
pieces.append(vocoder_chunked(vocoder, mel))
|
| 217 |
+
if len(pieces) == 1:
|
| 218 |
+
return pieces[0], pieces[0].shape[0] // 256
|
| 219 |
+
# concat sentences with a short pause
|
| 220 |
+
gap = np.zeros(int(24000 * 0.2))
|
| 221 |
+
wav = pieces[0]
|
| 222 |
+
for p in pieces[1:]:
|
| 223 |
+
wav = np.concatenate([wav, gap, p])
|
| 224 |
+
return wav, wav.shape[0] // 256
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def main():
|
| 228 |
+
parser = argparse.ArgumentParser()
|
| 229 |
+
parser.add_argument("--text", required=True)
|
| 230 |
+
parser.add_argument("--acoustic", default="export/acoustic_female.onnx")
|
| 231 |
+
parser.add_argument("--vocoder", default="export/axmodel/bigvgan_base.axmodel")
|
| 232 |
+
parser.add_argument("--output", default="board_out.wav")
|
| 233 |
+
parser.add_argument("--noise_scale", type=float, default=0.3)
|
| 234 |
+
args = parser.parse_args()
|
| 235 |
+
|
| 236 |
+
import soundfile as sf
|
| 237 |
+
acoustic = load_acoustic(args.acoustic)
|
| 238 |
+
vocoder = load_vocoder(args.vocoder)
|
| 239 |
+
wav, T = synthesize(acoustic, vocoder, args.text,
|
| 240 |
+
noise_scale=args.noise_scale)
|
| 241 |
+
wav = wav / (np.abs(wav).max() + 1e-8) * 0.95
|
| 242 |
+
sf.write(args.output, wav, 24000)
|
| 243 |
+
print(f"saved: {args.output} ({len(wav)/24000:.2f}s, mel_frames={T})")
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
if __name__ == "__main__":
|
| 247 |
+
main()
|
python/infer_onnx.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""End-to-end ONNX inference: text -> acoustic.onnx -> mel -> bigvgan.onnx -> wav."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import onnxruntime as ort
|
| 9 |
+
import soundfile as sf
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
from cn_frontend import text_to_sequence
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
parser = argparse.ArgumentParser()
|
| 17 |
+
parser.add_argument("--text", required=True)
|
| 18 |
+
parser.add_argument("--acoustic", default="export/acoustic_female.onnx")
|
| 19 |
+
parser.add_argument("--vocoder", default="export/bigvgan_base.onnx")
|
| 20 |
+
parser.add_argument("--output", default="export/onnx_demo.wav")
|
| 21 |
+
parser.add_argument("--max_text", type=int, default=256)
|
| 22 |
+
parser.add_argument("--max_mel", type=int, default=2048)
|
| 23 |
+
parser.add_argument("--noise_scale", type=float, default=0.3)
|
| 24 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 25 |
+
args = parser.parse_args()
|
| 26 |
+
|
| 27 |
+
from infer_board import split_sentences, text_to_inputs, tail_stretch
|
| 28 |
+
ac = ort.InferenceSession(args.acoustic, providers=["CPUExecutionProvider"])
|
| 29 |
+
vc = ort.InferenceSession(args.vocoder, providers=["CPUExecutionProvider"])
|
| 30 |
+
|
| 31 |
+
pieces = []
|
| 32 |
+
for s in split_sentences(args.text):
|
| 33 |
+
x, x_lengths, noise_z = text_to_inputs(s, args.noise_scale, args.seed)
|
| 34 |
+
mel, y_lengths = ac.run(None, {
|
| 35 |
+
"x": x, "x_lengths": x_lengths, "noise_z": noise_z,
|
| 36 |
+
})
|
| 37 |
+
T = int(y_lengths[0])
|
| 38 |
+
mel = mel[:, :, :T]
|
| 39 |
+
mel = tail_stretch(mel)
|
| 40 |
+
print(f" sentence mel: {mel.shape} (y_lengths={T})")
|
| 41 |
+
wav = vc.run(None, {"mel": mel})[0] # [1,1,T*256]
|
| 42 |
+
pieces.append(wav[0, 0])
|
| 43 |
+
wav = np.concatenate(pieces) if len(pieces) > 1 else pieces[0]
|
| 44 |
+
wav = wav / (np.abs(wav).max() + 1e-8) * 0.95
|
| 45 |
+
sf.write(args.output, wav, 24000)
|
| 46 |
+
print(f"saved: {args.output} ({len(wav)/24000:.2f}s)")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
python/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 推理最小依赖
|
| 2 |
+
onnxruntime>=1.18
|
| 3 |
+
numpy>=1.24
|
| 4 |
+
soundfile>=0.12
|
| 5 |
+
pypinyin>=0.50
|
| 6 |
+
# AX650 NPU 推理(可选):axengine(AXERA 提供,含 AxEngineExecutionProvider)
|
python/vocoder_utils.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pretrained mel-conditioned vocoder helpers (NVIDIA BigVGAN)."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from bigvgan.env import AttrDict
|
| 9 |
+
from bigvgan.bigvgan import BigVGAN
|
| 10 |
+
|
| 11 |
+
VOC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vocoder")
|
| 12 |
+
|
| 13 |
+
BUILTIN = {
|
| 14 |
+
"bigvgan_base": ("bigvgan_base_24k_100band.json",
|
| 15 |
+
"bigvgan_base_24k_100band.pt"),
|
| 16 |
+
"bigvgan_v2": ("bigvgan_v2_24k_100band.json",
|
| 17 |
+
"bigvgan_v2_24k_100band.pt"),
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
_cache = {}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def load_bigvgan(name="bigvgan_base", device="cpu"):
|
| 24 |
+
"""Load a pretrained BigVGAN generator (frozen, eval mode)."""
|
| 25 |
+
if name in _cache:
|
| 26 |
+
return _cache[name]
|
| 27 |
+
if name not in BUILTIN:
|
| 28 |
+
raise ValueError(f"Unknown vocoder '{name}', choose from {list(BUILTIN)}")
|
| 29 |
+
cfg_file, ckpt_file = BUILTIN[name]
|
| 30 |
+
h = AttrDict(json.load(open(os.path.join(VOC_DIR, cfg_file))))
|
| 31 |
+
model = BigVGAN(h)
|
| 32 |
+
ckpt = torch.load(os.path.join(VOC_DIR, ckpt_file), map_location="cpu",
|
| 33 |
+
weights_only=True)
|
| 34 |
+
model.load_state_dict(ckpt["generator"])
|
| 35 |
+
model.remove_weight_norm()
|
| 36 |
+
model.eval().to(device)
|
| 37 |
+
for p in model.parameters():
|
| 38 |
+
p.requires_grad = False
|
| 39 |
+
_cache[name] = model
|
| 40 |
+
return model
|
reports/EVALUATION.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 验证报告
|
| 2 |
+
|
| 3 |
+
## 可懂度(SenseVoice `iic/SenseVoiceSmall`,8 句固定测试)
|
| 4 |
+
|
| 5 |
+
测试句:
|
| 6 |
+
1. 你好世界,今天天气真不错,适合出去散步。
|
| 7 |
+
2. 机器服务员的生产厂家介绍说这款产品很好用。
|
| 8 |
+
3. 从八月九日十九时到八月十二日上午十时。
|
| 9 |
+
4. 中国人民银行决定下调存款准备金率。
|
| 10 |
+
5. 人工智能技术正在改变我们的生活方式。
|
| 11 |
+
6. 今天中午我想吃一碗牛肉面。
|
| 12 |
+
7. 北京是中国的首都,有着悠久的历史文化。
|
| 13 |
+
8. 请问最近的医院在哪里。
|
| 14 |
+
|
| 15 |
+
| 配置 | 女声 CER | 男声 CER |
|
| 16 |
+
|---|---|---|
|
| 17 |
+
| CPU 全 fp32(ONNX) | 0.075 | 0.108 |
|
| 18 |
+
|
| 19 |
+
注:日期句(#3)的系统性偏差为 SenseVoice itn 转写差异("上午十时"→"上午10"),
|
| 20 |
+
非语音错误。去掉该句后女声 CER 约 0.02。
|
| 21 |
+
|
| 22 |
+
## 性能(AX650N,10.126.35.115,Ubuntu 22.04)
|
| 23 |
+
|
| 24 |
+
| 输入 | acoustic(CPU) | vocoder(NPU) | 总耗时 | 有效音频 | RTF |
|
| 25 |
+
|---|---|---|---|---|---|
|
| 26 |
+
| 女声 21 字 | 0.32s | 0.32s | 0.65s | 4.04s | 0.161 |
|
| 27 |
+
| 男声 15 字 | 0.31s | 0.30s | 0.63s | 3.31s | 0.191 |
|
| 28 |
+
| 女声 19 字 | 0.30s | 0.30s | 0.62s | 4.11s | 0.151 |
|
| 29 |
+
| 长句(分块 526 帧) | 0.30s | 0.61s | 0.93s | 5.61s | 0.166 |
|
| 30 |
+
|
| 31 |
+
## NPU 量化验证结论
|
| 32 |
+
|
| 33 |
+
### 声学模型(text→mel):NPU 量化不可行
|
| 34 |
+
|
| 35 |
+
| 方案 | 结果 |
|
| 36 |
+
|---|---|
|
| 37 |
+
| int8(Percentile/BRecQ) | duration 偏差 13%、mel 渲染 CER~0.9 |
|
| 38 |
+
| U16(激活+权重) | 同上(duration 偏差 13%) |
|
| 39 |
+
| 混合精度 | Pulsar2 编译失败(AxLayerNorm tiling bug) |
|
| 40 |
+
| FP16/F32 | Pulsar2 7.0 不支持 |
|
| 41 |
+
|
| 42 |
+
根因:VITS 的 flow(仿射耦合)+ duration(对数尺度)对 8/16-bit 量化系统性敏感。
|
| 43 |
+
→ 声学模型必须 CPU fp32(AX650N 实测 0.30s/句)。
|
| 44 |
+
|
| 45 |
+
### 声码器(mel→wav):NPU int8 可用
|
| 46 |
+
|
| 47 |
+
- Pulsar2 7.0 编译,`onnx_opt.enable_onnxsim: true` 必须开启
|
| 48 |
+
(BigVGAN edge-pad 节点会导致 AxPad shape 错误)
|
| 49 |
+
- 对多数句子可懂(男声真机 CER 0.000);个别句子有轻微质量波动
|
| 50 |
+
- 板上 CPU fp32 vocoder 实测 >30s/句(rtf>5)不可用 → vocoder 必须 NPU
|
| 51 |
+
|
| 52 |
+
## 音质优化要点(训练/推理)
|
| 53 |
+
|
| 54 |
+
- 数据:CosyVoice 官方音色(女)生成 150 条/音色,比原始语料更干净
|
| 55 |
+
- 训练:声码器感知 GAN(mel→冻结 BigVGAN→MPD/MSD)+ 句尾采样 `tail_bias=0.4`
|
| 56 |
+
- 节奏:`length_scale` 女 0.7 / 男 0.85(音节时长贴近自然 180ms)
|
| 57 |
+
- 尾字:`tail_stretch()` 句尾 mel 20→45 帧拉伸(模型句尾音节 duration 偏短)
|