inoryQwQ commited on
Commit
0c723b3
·
verified ·
1 Parent(s): a84e9a3

Release 1.0.0: Chinese TTS (Inflect-Micro-v2 arch + BigVGAN), ONNX + AX650 axmodel + checkpoints

Browse files
.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 偏短)