HY-2012 commited on
Commit
4b8c0f4
·
verified ·
1 Parent(s): 796f7d7

update: use static320 FP32 decode path and add config.json

Browse files

Promote the quality-equivalent static320 FP32 ONNX decode model to the default path. Update README/runtime scripts and remove legacy package layout files.

Files changed (35) hide show
  1. .gitattributes +12 -0
  2. .gitignore +4 -0
  3. MODEL_SHA256SUMS +15 -0
  4. README.md +162 -89
  5. models/axmodels/tts_local_decoder.axmodel → bin/moss_tts_ax650/libonnxruntime.so +2 -2
  6. models/axmodels/tts_decode_step_u16.axmodel → bin/moss_tts_ax650/libonnxruntime.so.1 +2 -2
  7. models/axmodels/codec_encode.axmodel → bin/moss_tts_ax650/libonnxruntime.so.1.23.2 +2 -2
  8. bin/moss_tts_ax650/libonnxruntime_providers_shared.so +0 -0
  9. bin/moss_tts_ax650/moss_tts_ax650 +3 -0
  10. {configs → config}/browser_poc_manifest.json +0 -0
  11. {configs → config}/codec_browser_onnx_meta.json +0 -0
  12. {configs → config}/tokenizer.model +0 -0
  13. {configs → config}/tts_browser_onnx_meta.json +0 -0
  14. configuration.json +15 -0
  15. infer_board_axmodel_decode.py +0 -112
  16. models/{axmodels → axmodels_650}/codec_decode.axmodel +2 -2
  17. models/{axmodels → axmodels_650}/tts_decode_step.axmodel +2 -2
  18. models/{axmodels → axmodels_650}/tts_local_fixed_sampled_frame.axmodel +2 -2
  19. models/{axmodels → axmodels_650}/tts_prefill.axmodel +2 -2
  20. models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.data +3 -0
  21. models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.onnx +3 -0
  22. models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.data +3 -0
  23. models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.onnx +3 -0
  24. models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx +3 -0
  25. models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx.data +3 -0
  26. models/onnxmodels/moss_tts_decode_step.data +3 -0
  27. models/onnxmodels/moss_tts_decode_step.onnx +3 -0
  28. models/onnxmodels/tts_decode_step.onnx +0 -3
  29. infer_board_onnx_decode.py → python/infer_moss_tts.py +98 -14
  30. python/prepare_request.py +146 -0
  31. requirements-hybrid.txt +2 -0
  32. requirements.txt +0 -3
  33. run_ax650.sh +69 -0
  34. scripts/tts_runtime.py +148 -33
  35. verify_package.sh +39 -0
.gitattributes CHANGED
@@ -40,3 +40,15 @@ models/axmodels/tts_decode_step_u16.axmodel filter=lfs diff=lfs merge=lfs -text
40
  models/axmodels/tts_local_decoder.axmodel filter=lfs diff=lfs merge=lfs -text
41
  models/axmodels/tts_local_fixed_sampled_frame.axmodel filter=lfs diff=lfs merge=lfs -text
42
  models/axmodels/tts_prefill.axmodel filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  models/axmodels/tts_local_decoder.axmodel filter=lfs diff=lfs merge=lfs -text
41
  models/axmodels/tts_local_fixed_sampled_frame.axmodel filter=lfs diff=lfs merge=lfs -text
42
  models/axmodels/tts_prefill.axmodel filter=lfs diff=lfs merge=lfs -text
43
+ bin/moss_tts_ax650/libonnxruntime.so filter=lfs diff=lfs merge=lfs -text
44
+ bin/moss_tts_ax650/libonnxruntime.so.1 filter=lfs diff=lfs merge=lfs -text
45
+ bin/moss_tts_ax650/libonnxruntime.so.1.23.2 filter=lfs diff=lfs merge=lfs -text
46
+ bin/moss_tts_ax650/moss_tts_ax650 filter=lfs diff=lfs merge=lfs -text
47
+ models/axmodels_650/codec_decode.axmodel filter=lfs diff=lfs merge=lfs -text
48
+ models/axmodels_650/tts_decode_step.axmodel filter=lfs diff=lfs merge=lfs -text
49
+ models/axmodels_650/tts_local_fixed_sampled_frame.axmodel filter=lfs diff=lfs merge=lfs -text
50
+ models/axmodels_650/tts_prefill.axmodel filter=lfs diff=lfs merge=lfs -text
51
+ models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.data filter=lfs diff=lfs merge=lfs -text
52
+ models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.data filter=lfs diff=lfs merge=lfs -text
53
+ models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx.data filter=lfs diff=lfs merge=lfs -text
54
+ models/onnxmodels/moss_tts_decode_step.data filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ outputs/
2
+ __pycache__/
3
+ .cache/
4
+ *.pyc
MODEL_SHA256SUMS ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 27c474ec1cf819d72ebaa0ed33ae8d308eef508da74bb6860d99e916904fcc30 models/axmodels_650/codec_decode.axmodel
2
+ f64eda392b2186b61830aece478dd6d415f7d584a71ab12f45f28127f5a43e35 models/axmodels_650/tts_decode_step.axmodel
3
+ 4d929e9c05a26ec0b638e84fc8b4fc3476a7f3a578a09b679067a4ffec8d82f7 models/axmodels_650/tts_local_fixed_sampled_frame.axmodel
4
+ dfe5ae88a2549f708ab9134d1c6dbd8c53e49aff76fedff8a902459a7c4b6597 models/axmodels_650/tts_prefill.axmodel
5
+ 31294cf854687c937f5769baf44b796b055e559bc74bb0450af327c51c8f6969 models/onnxmodels/moss_tts_decode_step.onnx
6
+ 710ca7f6123372488a1641c579159908d8e6461569ae0b83fc071de8eb3de34f models/onnxmodels/moss_tts_decode_step.data
7
+ 35f309211297e504ed42b57e3011234023b78a453b8c5ba1e1d690ccb3612136 models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.onnx
8
+ 9269ab372a4f59d4db869c44da21b228abf2bf1db7059b1144d0a536c5cec985 models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.data
9
+ 0a59338df1984be8bcd4b49020eae5572c3eff03c4fdda75ead8c03c02e6e8b8 models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx
10
+ 7232d99b91b6be4b0bc86486b7df1c17f05bf6fae93c8629552274ab1a61cd91 models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx.data
11
+ 31294cf854687c937f5769baf44b796b055e559bc74bb0450af327c51c8f6969 models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.onnx
12
+ 710ca7f6123372488a1641c579159908d8e6461569ae0b83fc071de8eb3de34f models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.data
13
+ eddd60c9cbe74805d94342c032143f1a7d7af91f02af21eb749b6f919804e9be bin/moss_tts_ax650/moss_tts_ax650
14
+ ab8c4363e06ac80b3d1279ea55ebea44e906c5b131ba783ff684a067540c0e94 bin/moss_tts_ax650/libonnxruntime.so.1.23.2
15
+ e229c8e7b2abc2a50e550b3ed099178bdbe1948cf3a40447f753a01720aa9052 bin/moss_tts_ax650/libonnxruntime_providers_shared.so
README.md CHANGED
@@ -1,131 +1,204 @@
1
- # Moss-TTS-Nano.AXERA
 
 
 
 
 
 
 
 
2
 
3
- MOSS-TTS-Nano AX650 板端推理demo,包含板端 Python 推理脚本、配置文件、ONNX 对照模型和 AX650 可执行的 axmodel。
4
 
5
- [模型导出](https://github.com/AXERA-TECH/Moss-TTS-Nano.AXERA)
 
 
6
 
 
7
 
8
- ## 当前状态
 
 
 
 
9
 
10
- - 支持内置音色。
11
- - 当前推理未加入音色克隆。原因是量化后音色克隆链路损失较大。
12
- - 推理稳定性有待验证,当前板端推理 RTF 约为 3,实时率偏大,流式step输出效果无法对齐,RTF也在1.5-2左右,后续加入音色克隆模型推理耗时会更多。
13
- - 当前音频解码实际使用全量 codec decode 一次性还原。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- ## 目录结构
16
 
17
  ```text
18
- Moss-TTS-Nano.AXERA/
19
- ├── configs/
20
- ├── models/
21
- │ ├── axmodels/
22
- │ └── onnxmodels/
23
- ├── scripts/
24
- ├── infer_board_axmodel_decode.py
25
- ├── infer_board_onnx_decode.py
26
- ├── requirements.txt
27
- └── README.md
28
  ```
29
 
30
- ## 环境
 
31
 
32
- ### 安装 pyaxengine
33
- [pyaxengine Releases](https://github.com/AXERA-TECH/pyaxengine/releases/latest) 下载对应版本的 .whl 文件,然后安装:
 
 
34
  ```
35
- pip install axengine-x.x.x-py3-none-any.whl
 
 
 
 
 
 
 
 
36
  ```
37
 
38
- ### Python 依赖:
 
39
 
40
- ```bash
41
- pip install -r requirements.txt
 
 
 
 
 
 
42
  ```
43
 
44
- ## 推理
 
 
 
 
 
45
 
46
- ### 中文
 
 
 
47
 
48
  ```bash
49
- python3 infer_board_axmodel_decode.py \
50
- --text "你好,今天是美好的一天" \
51
- --voice Junhao \
52
- --seed 1234 \
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  --max-new-frames 128 \
54
- --output-audio-path outputs/zh.wav
 
55
  ```
56
 
57
- 结果:
58
 
 
 
 
 
 
59
  ```
60
- =======================================================
61
- 板端全 AXModel 推理结果
62
- =======================================================
63
- 输出: /path/to/Moss-TTS-Nano.AXERA/outputs/zh.wav
64
- 音频时长: 3.20s
65
- 推理耗时: 9.34s
66
- RTF: 2.9180
67
- =======================================================
 
 
 
 
 
 
 
68
  ```
69
 
70
- ### 英文
71
 
72
  ```bash
73
- python3 infer_board_axmodel_decode.py \
74
- --text "hello,today is a good day." \
75
- --voice Ava \
76
- --seed 1234 \
77
- --max-new-frames 128 \
78
- --output-audio-path outputs/en.wav
79
  ```
80
 
81
- 结果:
 
 
 
82
 
 
 
 
 
 
 
 
83
  ```
84
- =======================================================
85
- 板端全 AXModel 推理结果
86
- =======================================================
87
- 输出: /path/to/Moss-TTS-Nano.AXERA/outputs/en.wav
88
- 音频时长: 3.04s
89
- 推理耗时: 8.77s
90
- RTF: 2.8861
91
- =======================================================
 
 
 
 
 
 
 
92
  ```
93
- --voice 可选18种(Junhao、Zhiming、等)
94
 
95
- ## 内置音色
96
 
97
- | 名称 | 描述 | 类型 |
98
- |------|------|------|
99
- | Junhao | CN 欢迎关注模思智能 | Chinese Male |
100
- | Zhiming | CN 京味胡同闲聊 | Chinese Male |
101
- | Weiguo | CN 说书 | Chinese Male |
102
- | Xiaoyu | CN 明星 | Chinese Female |
103
- | Yuewen | CN 机车 | Chinese Female |
104
- | Lingyu | CN 深夜电台 | Chinese Female |
105
- | Trump | EN Trump | English Male |
106
- | Ava | EN The Bitter Lesson | English Female |
107
- | Bella | EN A Gentle Reminder | English Female |
108
- | Adam | EN English News | English Male |
109
- | Nathan | EN The Quiet Motion of the World | English Male |
110
- | Soyo | JP Soyo | Japanese Female |
111
- | Saki | JP Saki | Japanese Female |
112
- | Mortis | JP Mortis | Japanese Female |
113
- | Umiri | JP Umiri | Japanese Female |
114
- | Mei | JP Togawa | Japanese Female |
115
- | Anon | JP Anon | Japanese Female |
116
- | Arisa | JP Arisa | Japanese Female |
117
 
118
- ## ONNX decode_step 对比推理
 
 
 
119
 
120
- 注:`infer_board_onnx_decode.py` `infer_board_axmodel_decode.py` 的差别是 `decode_step` 使用了 ONNX 模型(效果好一些),其余模型仍然使用 axmodel
121
 
122
- 执行命令:
123
 
124
  ```bash
125
- python3 infer_board_onnx_decode.py \
126
- --text "你好,今天是美好的一天" \
127
- --voice Junhao \
128
- --seed 1234 \
129
- --max-new-frames 128 \
130
- --output-audio-path outputs/onnx_decode_step.wav
131
  ```
 
1
+ ---
2
+ library_name: axengine
3
+ pipeline_tag: text-to-speech
4
+ tags:
5
+ - axera
6
+ - ax650
7
+ - moss-tts
8
+ license: apache-2.0
9
+ ---
10
 
11
+ # MOSS-TTS-Nano.AXERA_HUG
12
 
13
+ AX650 量化模型和板端推理包。该仓库只发布量化 `.axmodel`、质量路径所需的 decode
14
+ ONNX、最小 Python 推理代码、AX650 AArch64 可执行文件及 ONNX Runtime 运行库,不包含
15
+ C++ 源码或浮点 checkpoint。
16
 
17
+ 源码、模型导出、校准和 Pulsar2 量化工程见:
18
 
19
+ ```text
20
+ https://github.com/AXERA-TECH/Moss-TTS-Nano.AXERA
21
+ ```
22
+
23
+ ## 发布内容
24
 
25
+ ```text
26
+ models/axmodels_650/
27
+ ├── tts_prefill.axmodel
28
+ ├── tts_decode_step.axmodel # 量化模型诊断/低延迟实验用
29
+ ├── tts_local_fixed_sampled_frame.axmodel
30
+ └── codec_decode.axmodel
31
+
32
+ models/onnxmodels/
33
+ ├── moss_tts_decode_step.onnx # 默认质量路径:static320 FP32
34
+ ├── moss_tts_decode_step.data
35
+ └── candidates/
36
+ ├── default_512_fp32/ # 原始 512-KV FP32 备份
37
+ ├── static320_fp32/ # 默认模型的来源副本
38
+ └── static320_int8_qdq/ # 可选速度候选,静态 KV=320
39
+
40
+ bin/moss_tts_ax650/
41
+ ├── moss_tts_ax650
42
+ ├── libonnxruntime.so.1.23.2
43
+ └── libonnxruntime_providers_shared.so
44
+ ```
45
 
46
+ ## 推荐质量路径
47
 
48
  ```text
49
+ tts_prefill.axmodel
50
+ tts_local_fixed_sampled_frame.axmodel
51
+ moss_tts_decode_step.onnx(ONNX Runtime CPU)
52
+ codec_decode.axmodel
 
 
 
 
 
 
53
  ```
54
 
55
+ 这是当前默认的质量和速度平衡路径。它使用 static320 FP32 decode ONNX,板端多句测试中
56
+ 与原始 512-KV FP32 路径生成的 Token 和 WAV 完全一致。稳态结果:
57
 
58
+ ```text
59
+ Total RTF: 1.10–1.13
60
+ Generation RTF: 1.01–1.04
61
+ Decode: 69–70 ms/frame
62
  ```
63
+
64
+ 以上数值来自板端空闲、关闭其他 NPU/CPU 服务后的多句稳态测试;单句冷启动或板端同时运行
65
+ `axllm serve` 等任务时,ONNX CPU decode 会受到资源争用影响,RTF 可能显著升高。模型加载
66
+ 时间不计入 RTF,但首帧运行时初始化和系统负载仍会影响实测值。
67
+
68
+ 模型加载时间不计入 RTF。默认 static320 路径要求:
69
+
70
+ ```text
71
+ 实际 prefill 长度 + max_new_frames <= 320
72
  ```
73
 
74
+ 当前脚本默认 `max_new_frames=110`、decode warmup 为 32 帧;长文本请按标点分段。全 AXModel 路径速度约 `RTF 0.44–0.48`,但长句存在提前停止
75
+ 或尾部噪音风险,因此仅保留为诊断选项。
76
 
77
+ ## 可选速度候选:static320 INT8 QDQ
78
+
79
+ `models/onnxmodels/candidates/static320_int8_qdq/` 是经过主机和板端测试的 ONNX Runtime
80
+ INT8 候选,只量化常量权重 Linear,注意力、LayerNorm、Softmax 和 KV 保持 FP32。它的静态
81
+ KV 容量为 320,必须满足:
82
+
83
+ ```text
84
+ 实际 prefill 长度 + max_new_frames <= 320
85
  ```
86
 
87
+ 板端多句测试在一次模型已缓存、32 帧 decode warmup 的运行中得到:
88
+
89
+ ```text
90
+ Total RTF:0.8462
91
+ Generation RTF:0.7547
92
+ ```
93
 
94
+ 这是最佳观测值,不是空闲/繁忙状态下都能保证的固定值;同一板端后台服务运行时曾测得
95
+ `RTF 1.8502`,并出现过 AXEngine 句柄资源不足。因此候选只作为实验速度路径,默认发布
96
+ 路径仍保持 FP32 ONNX。它的 Token 流与默认 FP32 质量路径也不是逐 token 一致,使用前必须
97
+ 在目标板端试听确认。示例:
98
 
99
  ```bash
100
+ bash run_ax650.sh \
101
+ --text "你好,今天是美好的一天" \
102
+ --decode-backend onnx \
103
+ --decode-onnx models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx \
104
+ --max-new-frames 110 \
105
+ --decode-warmup-frames 32 \
106
+ --output-wav outputs/static320_int8_qdq.wav
107
+ ```
108
+
109
+ ���始 512-KV FP32 模型保存在 `models/onnxmodels/candidates/default_512_fp32/`,仅用于
110
+ 回归对比。若需使用原始路径:
111
+
112
+ ```bash
113
+ bash run_ax650.sh \
114
+ --text "你好,今天是美好的一天。" \
115
+ --decode-backend onnx \
116
+ --decode-onnx models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.onnx \
117
  --max-new-frames 128 \
118
+ --decode-warmup-frames 32 \
119
+ --output-wav outputs/default_512_fp32.wav
120
  ```
121
 
122
+ ## Python 推理
123
 
124
+ 板端安装 Python 依赖和匹配的 `pyaxengine/axengine`:
125
+
126
+ ```bash
127
+ python3 -m pip install -r requirements.txt
128
+ python3 -c "import axengine, numpy, sentencepiece; print('Python runtime OK')"
129
  ```
130
+
131
+ 运行 Python 参考链路:
132
+
133
+ ```bash
134
+ python3 python/infer_moss_tts.py \
135
+ --config-dir config \
136
+ --axmodel-dir models/axmodels_650 \
137
+ --onnx-dir models/onnxmodels \
138
+ --text "明天上午九点,我们一起去公园散步,然后喝一杯热咖啡。" \
139
+ --voice Junhao \
140
+ --sample-mode fixed \
141
+ --local-fixed-backend axmodel \
142
+ --prefill-backend axmodel \
143
+ --decode-backend onnx \
144
+ --output-audio-path outputs/python_decode_onnx.wav
145
  ```
146
 
147
+ 若要在 Python 中测试全 AXModel 诊断路径:
148
 
149
  ```bash
150
+ python3 python/infer_moss_tts.py \
151
+ --config-dir config --axmodel-dir models/axmodels_650 \
152
+ --onnx-dir models/onnxmodels \
153
+ --text "你好,今天是美好的一天。" --voice Junhao \
154
+ --decode-backend axmodel \
155
+ --output-audio-path outputs/python_all_axmodel.wav
156
  ```
157
 
158
+ ## C++ AX650 推理
159
+
160
+ `bin/moss_tts_ax650/moss_tts_ax650` 已交叉编译,无需在板端编译。运行库位于同目录,
161
+ 脚本会自动设置 `LD_LIBRARY_PATH`:
162
 
163
+ ```bash
164
+ bash run_ax650.sh \
165
+ --text "明天上午九点,我们一起去公园散步,然后喝一杯热咖啡。" \
166
+ --voice Junhao \
167
+ --decode-backend onnx \
168
+ --output-wav outputs/cpp_decode_onnx.wav \
169
+ --output-log outputs/cpp_decode_onnx.log
170
  ```
171
+
172
+ 只用于诊断量化 AXModel
173
+
174
+ ```bash
175
+ bash run_ax650.sh \
176
+ --text "你好,今天是美好的一天。" \
177
+ --decode-backend axmodel \
178
+ --output-wav outputs/cpp_all_axmodel.wav
179
+ ```
180
+
181
+ 直接运行二进制时,必须从包根目录执行:
182
+
183
+ ```bash
184
+ export LD_LIBRARY_PATH="$PWD/bin/moss_tts_ax650:${LD_LIBRARY_PATH:-}"
185
+ ./bin/moss_tts_ax650/moss_tts_ax650 --help
186
  ```
 
187
 
188
+ ## 工具链说明
189
 
190
+ 源码仓库记录的本机工具链路径为:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
+ ```text
193
+ GCC:/data/shared/huyuan/toolchains/gcc-arm-9.2-2019.12-x86_64-aarch64-none-linux-gnu
194
+ AX650 BSP:/data/shared/huyuan/toolchains/ax650n_bsp_sdk/msp/out
195
+ ```
196
 
197
+ 这些工具链不上传到 Hugging Face。C++ 源码和交叉编译方法见配套 GitHub 仓库
198
 
199
+ ## 完整性检查
200
 
201
  ```bash
202
+ chmod +x run_ax650.sh verify_package.sh bin/moss_tts_ax650/moss_tts_ax650
203
+ bash verify_package.sh
 
 
 
 
204
  ```
models/axmodels/tts_local_decoder.axmodel → bin/moss_tts_ax650/libonnxruntime.so RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:d189cb4cab14401e95b787cf829eaf205dff238c4ccd26fbf52681019600da4e
3
- size 84574470
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ab8c4363e06ac80b3d1279ea55ebea44e906c5b131ba783ff684a067540c0e94
3
+ size 18693384
models/axmodels/tts_decode_step_u16.axmodel → bin/moss_tts_ax650/libonnxruntime.so.1 RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9cb39e7c48d398a57120dec59f29a7ba9bf09d9102c03fc1acb3baacbe2c5502
3
- size 144441291
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ab8c4363e06ac80b3d1279ea55ebea44e906c5b131ba783ff684a067540c0e94
3
+ size 18693384
models/axmodels/codec_encode.axmodel → bin/moss_tts_ax650/libonnxruntime.so.1.23.2 RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6ca92f2ca6f20147f87b8aa6af9ebb0c4b6eebe6c4e0d9192c50931989029798
3
- size 102540526
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ab8c4363e06ac80b3d1279ea55ebea44e906c5b131ba783ff684a067540c0e94
3
+ size 18693384
bin/moss_tts_ax650/libonnxruntime_providers_shared.so ADDED
Binary file (9.92 kB). View file
 
bin/moss_tts_ax650/moss_tts_ax650 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eddd60c9cbe74805d94342c032143f1a7d7af91f02af21eb749b6f919804e9be
3
+ size 124096
{configs → config}/browser_poc_manifest.json RENAMED
File without changes
{configs → config}/codec_browser_onnx_meta.json RENAMED
File without changes
{configs → config}/tokenizer.model RENAMED
File without changes
{configs → config}/tts_browser_onnx_meta.json RENAMED
File without changes
configuration.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "moss-tts-nano",
3
+ "library_name": "axengine",
4
+ "pipeline_tag": "text-to-speech",
5
+ "target_hardware": ["AX650"],
6
+ "source_code": "https://github.com/AXERA-TECH/Moss-TTS-Nano.AXERA",
7
+ "source_code_standardized": "https://github.com/AXERA-TECH/Moss-TTS-Nano.AXERA",
8
+ "quality_path": "prefill.axmodel -> local_fixed_sampled_frame.axmodel -> static320 FP32 decode ONNX CPU -> codec_decode.axmodel",
9
+ "rtf_reference": 1.10,
10
+ "rtf_generation_reference": 1.01,
11
+ "decode_kv_capacity": 320,
12
+ "decode_warmup_frames": 32,
13
+ "onnxruntime_version": "1.23.2",
14
+ "compiler": "AX650 GCC ARM 9.2 / AX650 BSP"
15
+ }
infer_board_axmodel_decode.py DELETED
@@ -1,112 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import logging
5
- import sys
6
- from pathlib import Path
7
- from typing import Optional, Sequence
8
-
9
- _HERE = Path(__file__).resolve().parent
10
-
11
-
12
- def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
13
- p = argparse.ArgumentParser(
14
- description=(
15
- "MOSS-TTS-Nano:"
16
- "prefill(axmodel) -> decode_step(axmodel) -> local_decoder(axmodel) -> codec_decode(axmodel)。"
17
- )
18
- )
19
- p.add_argument("--config-dir", default=str(_HERE / "configs"),
20
- help="config directory (default: ./configs)")
21
- p.add_argument("--axmodel-dir", default=str(_HERE / "models" / "axmodels"),
22
- help="axmodel directory (default: ./models/axmodels)")
23
- p.add_argument("--output-audio-path",
24
- default=str(_HERE / "outputs" / "board_axmodel_decode.wav"),
25
- help="output WAV path")
26
-
27
- text_group = p.add_mutually_exclusive_group(required=True)
28
- text_group.add_argument("--text", help="待合成文本")
29
- text_group.add_argument("--text-file", help="UTF-8 文本文件路径")
30
-
31
- p.add_argument("--voice", default="Junhao", help="内置音色名称")
32
- p.add_argument("--prompt-audio-path", "--reference-audio-path",
33
- dest="prompt_audio_path", default=None,
34
- help="参考音频路径(语音克隆,覆盖 --voice)")
35
- p.add_argument("--sample-mode", choices=("greedy", "fixed", "full"), default="full")
36
- p.add_argument("--do-sample", type=int, default=0, choices=[0, 1],
37
- help="是否采样 (0=argmax, 1=sample)")
38
- p.add_argument("--streaming", type=int, default=0, choices=[0, 1],
39
- help="按文本 chunk 增量写 WAV (1);不改变 codec 全量解码方式")
40
- p.add_argument("--max-new-frames", type=int, default=150,
41
- help="最大生成帧数")
42
- p.add_argument("--voice-clone-max-text-tokens", type=int, default=75,
43
- help="语音克隆每段最大 token 数")
44
- p.add_argument("--seed", type=int, default=None, help="随机种子")
45
- p.add_argument("--debug", action="store_true", help="启用 debug 日志")
46
- return p.parse_args(argv)
47
-
48
-
49
- def main(argv: Optional[Sequence[str]] = None) -> dict:
50
- args = parse_args(argv)
51
-
52
- logging.basicConfig(
53
- format="%(asctime)s %(levelname)s %(name)s: %(message)s",
54
- level=logging.DEBUG if args.debug else logging.INFO,
55
- stream=sys.stderr,
56
- )
57
-
58
- raw_text = (
59
- str(args.text)
60
- if args.text is not None
61
- else Path(args.text_file).read_text(encoding="utf-8")
62
- )
63
-
64
- logging.info("config_dir: %s", args.config_dir)
65
- logging.info("axmodel_dir: %s", args.axmodel_dir)
66
- logging.info("text: %r", raw_text[:60])
67
-
68
- from scripts.tts_runtime import AxTtsRuntime
69
-
70
- runtime = AxTtsRuntime(
71
- config_dir=args.config_dir,
72
- axmodel_dir=args.axmodel_dir,
73
- use_onnx_decode=False,
74
- max_new_frames=args.max_new_frames,
75
- do_sample=bool(args.do_sample),
76
- sample_mode=args.sample_mode,
77
- )
78
-
79
- logging.info("decode_step backend: AXModel (NPU)")
80
- logging.info("chain: prefill(axmodel) -> decode_step(axmodel) -> local_decoder(axmodel) -> codec_decode(axmodel)")
81
- if args.prompt_audio_path:
82
- logging.info("参考音频: %s", args.prompt_audio_path)
83
- else:
84
- logging.info("内置音色: %s", args.voice)
85
-
86
- result = runtime.synthesize(
87
- text=raw_text,
88
- voice=args.voice,
89
- prompt_audio_path=args.prompt_audio_path,
90
- output_audio_path=args.output_audio_path,
91
- sample_mode=args.sample_mode,
92
- do_sample=bool(args.do_sample),
93
- streaming=bool(args.streaming),
94
- max_new_frames=args.max_new_frames,
95
- voice_clone_max_text_tokens=args.voice_clone_max_text_tokens,
96
- seed=args.seed,
97
- )
98
- t = result["timing"]
99
- print(f"\n{'='*55}")
100
- print(f" 板端全 AXModel 推理结果")
101
- print(f"{'='*55}")
102
- print(f" 输出: {result['audio_path']}")
103
- print(f" 音频时长: {t['audio_duration_sec']:.2f}s")
104
- print(f" 推理耗时: {t['total_infer_time_sec']:.2f}s")
105
- print(f" RTF: {t['rtf']:.4f}")
106
- print(f"{'='*55}\n")
107
-
108
- return result
109
-
110
-
111
- if __name__ == "__main__":
112
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/{axmodels → axmodels_650}/codec_decode.axmodel RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:edcadd065f5c2d114b955805a6045afaac347c33a83e5da13f104516383db0ce
3
- size 52534674
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27c474ec1cf819d72ebaa0ed33ae8d308eef508da74bb6860d99e916904fcc30
3
+ size 52017863
models/{axmodels → axmodels_650}/tts_decode_step.axmodel RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:effb33389b8447097931036d8d289ce88dc95a1c9361ea3f51910f4f64a113de
3
- size 441548285
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f64eda392b2186b61830aece478dd6d415f7d584a71ab12f45f28127f5a43e35
3
+ size 144663502
models/{axmodels → axmodels_650}/tts_local_fixed_sampled_frame.axmodel RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:bb532a0e1367d1607288b36b3e5fc546942b51e7ea4870fee51272120a9010f4
3
- size 385673439
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4d929e9c05a26ec0b638e84fc8b4fc3476a7f3a578a09b679067a4ffec8d82f7
3
+ size 215139340
models/{axmodels → axmodels_650}/tts_prefill.axmodel RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:a27d6de7e122bf487d8a34ce5a1558267e6b7dc245908157b8e7909a3fb3c29b
3
- size 146156002
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfe5ae88a2549f708ab9134d1c6dbd8c53e49aff76fedff8a902459a7c4b6597
3
+ size 147742699
models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9269ab372a4f59d4db869c44da21b228abf2bf1db7059b1144d0a536c5cec985
3
+ size 881631240
models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:35f309211297e504ed42b57e3011234023b78a453b8c5ba1e1d690ccb3612136
3
+ size 178765
models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:710ca7f6123372488a1641c579159908d8e6461569ae0b83fc071de8eb3de34f
3
+ size 440816136
models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31294cf854687c937f5769baf44b796b055e559bc74bb0450af327c51c8f6969
3
+ size 151626
models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a59338df1984be8bcd4b49020eae5572c3eff03c4fdda75ead8c03c02e6e8b8
3
+ size 638590
models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7232d99b91b6be4b0bc86486b7df1c17f05bf6fae93c8629552274ab1a61cd91
3
+ size 186012168
models/onnxmodels/moss_tts_decode_step.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:710ca7f6123372488a1641c579159908d8e6461569ae0b83fc071de8eb3de34f
3
+ size 440816136
models/onnxmodels/moss_tts_decode_step.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31294cf854687c937f5769baf44b796b055e559bc74bb0450af327c51c8f6969
3
+ size 151626
models/onnxmodels/tts_decode_step.onnx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7ea885af519913fb9a79b99523b9a9ed7bc4c1d9673077d6124e00edbad15a65
3
- size 440954551
 
 
 
 
infer_board_onnx_decode.py → python/infer_moss_tts.py RENAMED
@@ -6,6 +6,10 @@ import sys
6
  from pathlib import Path
7
  from typing import Optional, Sequence
8
 
 
 
 
 
9
  _HERE = Path(__file__).resolve().parent
10
 
11
 
@@ -13,17 +17,37 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
13
  p = argparse.ArgumentParser(
14
  description=(
15
  "MOSS-TTS-Nano:"
16
- "prefill(axmodel) -> decode_step(ONNX CPU) -> local_decoder(axmodel) -> codec_decode(axmodel)。"
 
 
17
  )
18
  )
19
- p.add_argument("--config-dir", default=str(_HERE / "configs"),
20
  help="config directory (default: ./configs)")
21
- p.add_argument("--axmodel-dir", default=str(_HERE / "models" / "axmodels"),
22
  help="axmodel directory (default: ./models/axmodels)")
23
- p.add_argument("--onnx-dir", default=str(_HERE / "models" / "onnxmodels"),
24
- help="ONNX model directory (default: ./models/onnxmodels)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  p.add_argument("--output-audio-path",
26
- default=str(_HERE / "outputs" / "board_onnx_decode.wav"),
27
  help="output WAV path")
28
 
29
  text_group = p.add_mutually_exclusive_group(required=True)
@@ -34,9 +58,19 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
34
  p.add_argument("--prompt-audio-path", "--reference-audio-path",
35
  dest="prompt_audio_path", default=None,
36
  help="参考音频路径(语音克隆,覆盖 --voice)")
37
- p.add_argument("--sample-mode", choices=("greedy", "fixed", "full"), default="full")
38
- p.add_argument("--do-sample", type=int, default=0, choices=[0, 1],
39
- help="是否采样 (0=argmax, 1=sample)")
 
 
 
 
 
 
 
 
 
 
40
  p.add_argument("--streaming", type=int, default=0, choices=[0, 1],
41
  help="按文本 chunk 增量写 WAV (1);不改变 codec 全量解码方式")
42
  p.add_argument("--max-new-frames", type=int, default=150,
@@ -44,8 +78,23 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
44
  p.add_argument("--voice-clone-max-text-tokens", type=int, default=75,
45
  help="语音克隆每段最大 token 数")
46
  p.add_argument("--seed", type=int, default=None, help="随机种子")
 
 
 
 
 
 
 
 
 
 
 
 
47
  p.add_argument("--debug", action="store_true", help="启用 debug 日志")
48
- return p.parse_args(argv)
 
 
 
49
 
50
 
51
  def main(argv: Optional[Sequence[str]] = None) -> dict:
@@ -74,14 +123,27 @@ def main(argv: Optional[Sequence[str]] = None) -> dict:
74
  config_dir=args.config_dir,
75
  axmodel_dir=args.axmodel_dir,
76
  onnx_dir=args.onnx_dir,
77
- use_onnx_decode=True,
 
 
78
  max_new_frames=args.max_new_frames,
79
  do_sample=bool(args.do_sample),
80
  sample_mode=args.sample_mode,
81
  )
82
 
83
- logging.info("decode_step backend: ONNX (CPU)")
84
- logging.info("chain: prefill(axmodel) -> decode_step(ONNX) -> local_decoder(axmodel) -> codec_decode(axmodel)")
 
 
 
 
 
 
 
 
 
 
 
85
  if args.prompt_audio_path:
86
  logging.info("参考音频: %s", args.prompt_audio_path)
87
  else:
@@ -98,15 +160,37 @@ def main(argv: Optional[Sequence[str]] = None) -> dict:
98
  max_new_frames=args.max_new_frames,
99
  voice_clone_max_text_tokens=args.voice_clone_max_text_tokens,
100
  seed=args.seed,
 
 
101
  )
 
 
 
 
102
  t = result["timing"]
103
  print(f"\n{'='*55}")
104
- print(f" 板端 ONNX decode_step 推理结果")
 
 
 
 
 
 
 
 
 
105
  print(f"{'='*55}")
106
  print(f" 输出: {result['audio_path']}")
 
107
  print(f" 音频时长: {t['audio_duration_sec']:.2f}s")
108
  print(f" 推理耗时: {t['total_infer_time_sec']:.2f}s")
109
  print(f" RTF: {t['rtf']:.4f}")
 
 
 
 
 
 
110
  print(f"{'='*55}\n")
111
 
112
  return result
 
6
  from pathlib import Path
7
  from typing import Optional, Sequence
8
 
9
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
10
+
11
+ import numpy as np
12
+
13
  _HERE = Path(__file__).resolve().parent
14
 
15
 
 
17
  p = argparse.ArgumentParser(
18
  description=(
19
  "MOSS-TTS-Nano:"
20
+ "默认走全 AXModel 快速路径;也可仅将 "
21
+ "local_fixed_sampled_frame 切换为 ONNX Runtime CPU,"
22
+ "用于验证量化后的离散 Token 分叉。"
23
  )
24
  )
25
+ p.add_argument("--config-dir", default=str(_HERE.parent / "config"),
26
  help="config directory (default: ./configs)")
27
+ p.add_argument("--axmodel-dir", default=str(_HERE.parent / "models" / "axmodels_650"),
28
  help="axmodel directory (default: ./models/axmodels)")
29
+ p.add_argument("--onnx-dir", default=str(_HERE.parent / "models" / "onnxmodels"),
30
+ help="ONNX directory (default: ./models/onnxmodels)")
31
+ p.add_argument(
32
+ "--prefill-backend",
33
+ choices=("axmodel", "onnx"),
34
+ default="axmodel",
35
+ help="prefill backend (default: axmodel)",
36
+ )
37
+ p.add_argument(
38
+ "--decode-backend",
39
+ choices=("axmodel", "onnx"),
40
+ default="axmodel",
41
+ help="decode_step backend (default: axmodel)",
42
+ )
43
+ p.add_argument(
44
+ "--local-fixed-backend",
45
+ choices=("axmodel", "onnx"),
46
+ default="axmodel",
47
+ help="local_fixed_sampled_frame backend (default: axmodel)",
48
+ )
49
  p.add_argument("--output-audio-path",
50
+ default=str(_HERE / "outputs" / "board_axmodel_decode.wav"),
51
  help="output WAV path")
52
 
53
  text_group = p.add_mutually_exclusive_group(required=True)
 
58
  p.add_argument("--prompt-audio-path", "--reference-audio-path",
59
  dest="prompt_audio_path", default=None,
60
  help="参考音频路径(语音克隆,覆盖 --voice)")
61
+ p.add_argument(
62
+ "--sample-mode",
63
+ choices=("greedy", "fixed", "full"),
64
+ default="fixed",
65
+ help="fixed=单次 local-fixed 快速路径;full/greedy=逐 codebook local-decoder 慢路径",
66
+ )
67
+ p.add_argument(
68
+ "--do-sample",
69
+ type=int,
70
+ default=1,
71
+ choices=[0, 1],
72
+ help="兼容参数;默认与 fixed 快速路径配置一致",
73
+ )
74
  p.add_argument("--streaming", type=int, default=0, choices=[0, 1],
75
  help="按文本 chunk 增量写 WAV (1);不改变 codec 全量解码方式")
76
  p.add_argument("--max-new-frames", type=int, default=150,
 
78
  p.add_argument("--voice-clone-max-text-tokens", type=int, default=75,
79
  help="语音克隆每段最大 token 数")
80
  p.add_argument("--seed", type=int, default=None, help="随机种子")
81
+ p.add_argument(
82
+ "--greedy-prefix-frames",
83
+ type=int,
84
+ default=4,
85
+ help="前 N 帧使用 top-1 音频采样,之后恢复随机采样",
86
+ )
87
+ p.add_argument(
88
+ "--assistant-random-u",
89
+ type=float,
90
+ default=None,
91
+ help="可选:固定每帧继续/停止采样值,取值范围 [0, 1)",
92
+ )
93
  p.add_argument("--debug", action="store_true", help="启用 debug 日志")
94
+ args = p.parse_args(argv)
95
+ if args.assistant_random_u is not None and not 0.0 <= args.assistant_random_u < 1.0:
96
+ p.error("--assistant-random-u must be in [0, 1)")
97
+ return args
98
 
99
 
100
  def main(argv: Optional[Sequence[str]] = None) -> dict:
 
123
  config_dir=args.config_dir,
124
  axmodel_dir=args.axmodel_dir,
125
  onnx_dir=args.onnx_dir,
126
+ use_onnx_prefill=args.prefill_backend == "onnx",
127
+ use_onnx_decode=args.decode_backend == "onnx",
128
+ use_onnx_local_fixed=args.local_fixed_backend == "onnx",
129
  max_new_frames=args.max_new_frames,
130
  do_sample=bool(args.do_sample),
131
  sample_mode=args.sample_mode,
132
  )
133
 
134
+ logging.info("decode_step backend: %s", args.decode_backend.upper())
135
+ logging.info("prefill backend: %s", args.prefill_backend.upper())
136
+ local_stage = (
137
+ f"local_fixed_sampled_frame({args.local_fixed_backend}, fast)"
138
+ if args.sample_mode == "fixed"
139
+ else "local_decoder(axmodel, slow x17/frame)"
140
+ )
141
+ logging.info(
142
+ "chain: prefill(%s) -> %s -> decode_step(%s) -> codec_decode(axmodel)",
143
+ args.prefill_backend,
144
+ local_stage,
145
+ args.decode_backend,
146
+ )
147
  if args.prompt_audio_path:
148
  logging.info("参考音频: %s", args.prompt_audio_path)
149
  else:
 
160
  max_new_frames=args.max_new_frames,
161
  voice_clone_max_text_tokens=args.voice_clone_max_text_tokens,
162
  seed=args.seed,
163
+ greedy_prefix_frames=args.greedy_prefix_frames,
164
+ assistant_random_u=args.assistant_random_u,
165
  )
166
+ token_path = Path(args.output_audio_path).expanduser().resolve().with_suffix(".tokens.npy")
167
+ token_path.parent.mkdir(parents=True, exist_ok=True)
168
+ np.save(token_path, np.asarray(result["audio_token_ids"], dtype=np.int32))
169
+ result["audio_token_path"] = str(token_path)
170
  t = result["timing"]
171
  print(f"\n{'='*55}")
172
+ result_kind = (
173
+ "板端混合 ONNX/AXModel 推理结果"
174
+ if (
175
+ args.local_fixed_backend == "onnx"
176
+ or args.prefill_backend == "onnx"
177
+ or args.decode_backend == "onnx"
178
+ )
179
+ else "板端全 AXModel 推理结果"
180
+ )
181
+ print(f" {result_kind}")
182
  print(f"{'='*55}")
183
  print(f" 输出: {result['audio_path']}")
184
+ print(f" Token: {result['audio_token_path']}")
185
  print(f" 音频时长: {t['audio_duration_sec']:.2f}s")
186
  print(f" 推理耗时: {t['total_infer_time_sec']:.2f}s")
187
  print(f" RTF: {t['rtf']:.4f}")
188
+ print(f" 生成模型 RTF: {t['generation_model_rtf']:.4f}")
189
+ print(
190
+ " local calls: "
191
+ f"fixed={t['per_model_calls'].get('local_fixed_sampled_frame', 0)} "
192
+ f"decoder={t['per_model_calls'].get('local_decoder', 0)}"
193
+ )
194
  print(f"{'='*55}\n")
195
 
196
  return result
python/prepare_request.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ import sentencepiece as spm
10
+
11
+
12
+ def build_text_rows(token_ids: list[int], cfg: dict) -> list[list[int]]:
13
+ row_width = int(cfg["n_vq"]) + 1
14
+ rows = []
15
+ for tid in token_ids:
16
+ row = [int(cfg["audio_pad_token_id"])] * row_width
17
+ row[0] = int(tid)
18
+ rows.append(row)
19
+ return rows
20
+
21
+
22
+ def build_audio_prefix_rows(prompt_audio_codes: list[list[int]], cfg: dict) -> list[list[int]]:
23
+ row_width = int(cfg["n_vq"]) + 1
24
+ rows = []
25
+ for code_row in prompt_audio_codes:
26
+ row = [int(cfg["audio_pad_token_id"])] * row_width
27
+ row[0] = int(cfg["audio_user_slot_token_id"])
28
+ for i in range(min(len(code_row), int(cfg["n_vq"]))):
29
+ row[i + 1] = int(code_row[i])
30
+ rows.append(row)
31
+ return rows
32
+
33
+
34
+ def main() -> None:
35
+ ap = argparse.ArgumentParser(description="Prepare C++ MOSS-TTS request rows with Python tokenizer.")
36
+ ap.add_argument("--config-dir", default="configs")
37
+ group = ap.add_mutually_exclusive_group(required=True)
38
+ group.add_argument("--text")
39
+ group.add_argument("--token-ids", help="comma/space separated SentencePiece token ids")
40
+ ap.add_argument("--voice", default="Junhao")
41
+ ap.add_argument("--output", default="cpp/cpp_request.txt")
42
+ ap.add_argument("--seed", type=int, default=1234)
43
+ ap.add_argument("--max-new-frames", type=int, default=128)
44
+ ap.add_argument("--greedy-prefix-frames", type=int, default=4)
45
+ ap.add_argument(
46
+ "--assistant-random-u",
47
+ type=float,
48
+ default=None,
49
+ help="fix the continue/end sampling value; 0.5 selects the more probable decision",
50
+ )
51
+ args = ap.parse_args()
52
+ if args.max_new_frames <= 0:
53
+ raise SystemExit("--max-new-frames must be positive")
54
+ if args.greedy_prefix_frames < 0:
55
+ raise SystemExit("--greedy-prefix-frames must be non-negative")
56
+ if args.assistant_random_u is not None and not 0.0 <= args.assistant_random_u < 1.0:
57
+ raise SystemExit("--assistant-random-u must be in [0, 1)")
58
+
59
+ config_dir = Path(args.config_dir)
60
+ manifest = json.loads((config_dir / "browser_poc_manifest.json").read_text("utf-8"))
61
+ tts_meta = json.loads((config_dir / "tts_browser_onnx_meta.json").read_text("utf-8"))
62
+ cfg = manifest["tts_config"]
63
+ templates = manifest["prompt_templates"]
64
+
65
+ if args.token_ids:
66
+ token_ids = [int(x) for x in args.token_ids.replace(",", " ").split() if x.strip()]
67
+ else:
68
+ sp = spm.SentencePieceProcessor(model_file=str(config_dir / manifest["model_files"]["tokenizer_model"]))
69
+ token_ids = [int(x) for x in sp.encode(str(args.text), out_type=int)]
70
+
71
+ voice = None
72
+ for item in manifest["builtin_voices"]:
73
+ if item["voice"] == args.voice:
74
+ voice = item
75
+ break
76
+ if voice is None:
77
+ raise SystemExit(f"voice not found: {args.voice}")
78
+
79
+ prefix_ids = [
80
+ *templates["user_prompt_prefix_token_ids"],
81
+ int(cfg["audio_start_token_id"]),
82
+ ]
83
+ suffix_ids = [
84
+ int(cfg["audio_end_token_id"]),
85
+ *templates["user_prompt_after_reference_token_ids"],
86
+ *token_ids,
87
+ *templates["assistant_prompt_prefix_token_ids"],
88
+ int(cfg["audio_start_token_id"]),
89
+ ]
90
+ rows = [
91
+ *build_text_rows(prefix_ids, cfg),
92
+ *build_audio_prefix_rows(voice["prompt_audio_codes"], cfg),
93
+ *build_text_rows(suffix_ids, cfg),
94
+ ]
95
+ actual_len = len(rows)
96
+ static_len = int(tts_meta["static_shapes"]["prefill_seq"])
97
+ row_width = int(cfg["n_vq"]) + 1
98
+ if actual_len > static_len:
99
+ raise SystemExit(f"request too long: actual={actual_len}, static={static_len}")
100
+ pad_row = [int(cfg["audio_pad_token_id"])] * row_width
101
+ pad_row[0] = int(cfg["pad_token_id"])
102
+ rows = rows + [pad_row[:] for _ in range(static_len - actual_len)]
103
+ mask = [1] * actual_len + [0] * (static_len - actual_len)
104
+
105
+ # Serialize NumPy PCG64 draws so C++ and Python feed exactly the same
106
+ # four-input local-fixed sampler. Prefix frames consume their draws first,
107
+ # then replace audio u with zero (top-1), matching the Python runtime.
108
+ sampler_steps = int(args.max_new_frames)
109
+ rng = np.random.default_rng(int(args.seed))
110
+ assistant_random_u: list[np.float32] = []
111
+ audio_random_u: list[list[np.float32]] = []
112
+ for step in range(sampler_steps):
113
+ sampled_assistant_u = np.float32(rng.random())
114
+ if args.assistant_random_u is not None:
115
+ sampled_assistant_u = np.float32(args.assistant_random_u)
116
+ assistant_random_u.append(sampled_assistant_u)
117
+ audio_row = np.asarray(rng.random(int(cfg["n_vq"])), dtype=np.float32)
118
+ if step < int(args.greedy_prefix_frames):
119
+ audio_row.fill(0.0)
120
+ audio_random_u.append([np.float32(value) for value in audio_row])
121
+
122
+ out = Path(args.output)
123
+ out.parent.mkdir(parents=True, exist_ok=True)
124
+ with out.open("w", encoding="utf-8") as f:
125
+ f.write(f"actual_len {actual_len}\n")
126
+ f.write(f"prefill_seq {static_len}\n")
127
+ f.write(f"row_width {row_width}\n")
128
+ f.write("input_ids\n")
129
+ for row in rows:
130
+ f.write(" ".join(str(int(x)) for x in row) + "\n")
131
+ f.write("attention_mask\n")
132
+ f.write(" ".join(str(int(x)) for x in mask) + "\n")
133
+ f.write(f"sampler_steps {sampler_steps}\n")
134
+ f.write("assistant_random_u\n")
135
+ f.write(" ".join(format(float(value), ".9g") for value in assistant_random_u) + "\n")
136
+ f.write("audio_random_u\n")
137
+ for row in audio_random_u:
138
+ f.write(" ".join(format(float(value), ".9g") for value in row) + "\n")
139
+ print(
140
+ f"wrote {out} actual_len={actual_len} text_tokens={len(token_ids)} "
141
+ f"sampler_steps={sampler_steps} greedy_prefix_frames={args.greedy_prefix_frames}"
142
+ )
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
requirements-hybrid.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ -r requirements.txt
2
+ onnxruntime
requirements.txt CHANGED
@@ -1,5 +1,2 @@
1
  numpy
2
  sentencepiece
3
- onnxruntime>=1.16.0
4
- soundfile
5
- scipy
 
1
  numpy
2
  sentencepiece
 
 
 
run_ax650.sh ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PYTHON_BIN="${PYTHON_BIN:-python3}"
6
+ TEXT="你好,今天是美好的一天。"
7
+ VOICE="Junhao"
8
+ SEED=1234
9
+ MAX_NEW_FRAMES=110
10
+ GREEDY_PREFIX_FRAMES=4
11
+ DECODE_BACKEND="onnx"
12
+ DECODE_ONNX_THREADS=0
13
+ DECODE_WARMUP_FRAMES=32
14
+ DECODE_ONNX="$ROOT_DIR/models/onnxmodels/moss_tts_decode_step.onnx"
15
+ OUTPUT_WAV="$ROOT_DIR/outputs/moss_tts_ax650.wav"
16
+ OUTPUT_LOG="$ROOT_DIR/outputs/moss_tts_ax650.log"
17
+
18
+ usage() {
19
+ echo "usage: $0 --text TEXT [--voice NAME] [--decode-backend onnx|axmodel]"
20
+ echo " [--decode-onnx FILE] [--decode-onnx-threads N] [--decode-warmup-frames N]"
21
+ echo " [--seed N] [--max-new-frames N] [--output-wav FILE] [--output-log FILE]"
22
+ }
23
+
24
+ while [[ $# -gt 0 ]]; do
25
+ case "$1" in
26
+ --text) TEXT="$2"; shift 2 ;;
27
+ --voice) VOICE="$2"; shift 2 ;;
28
+ --seed) SEED="$2"; shift 2 ;;
29
+ --max-new-frames) MAX_NEW_FRAMES="$2"; shift 2 ;;
30
+ --greedy-prefix-frames) GREEDY_PREFIX_FRAMES="$2"; shift 2 ;;
31
+ --decode-backend) DECODE_BACKEND="$2"; shift 2 ;;
32
+ --decode-onnx) DECODE_ONNX="$2"; shift 2 ;;
33
+ --decode-onnx-threads) DECODE_ONNX_THREADS="$2"; shift 2 ;;
34
+ --decode-warmup-frames) DECODE_WARMUP_FRAMES="$2"; shift 2 ;;
35
+ --output-wav) OUTPUT_WAV="$2"; shift 2 ;;
36
+ --output-log) OUTPUT_LOG="$2"; shift 2 ;;
37
+ -h|--help) usage; exit 0 ;;
38
+ *) echo "unknown arg: $1" >&2; usage >&2; exit 2 ;;
39
+ esac
40
+ done
41
+
42
+ mkdir -p "$(dirname "$OUTPUT_WAV")" "$(dirname "$OUTPUT_LOG")"
43
+ export LD_LIBRARY_PATH="$ROOT_DIR/bin/moss_tts_ax650:${LD_LIBRARY_PATH:-}"
44
+
45
+ if [[ "$DECODE_BACKEND" == "onnx" ]]; then
46
+ [[ -f "$DECODE_ONNX" ]] || { echo "missing decode ONNX: $DECODE_ONNX" >&2; exit 1; }
47
+ EXTRA=(--decode-backend onnx --decode-onnx "$DECODE_ONNX"
48
+ --decode-onnx-threads "$DECODE_ONNX_THREADS"
49
+ --decode-warmup-frames "$DECODE_WARMUP_FRAMES")
50
+ elif [[ "$DECODE_BACKEND" == "axmodel" ]]; then
51
+ EXTRA=(--decode-backend axmodel --decode-axmodel "$ROOT_DIR/models/axmodels_650/tts_decode_step.axmodel")
52
+ else
53
+ echo "--decode-backend must be onnx or axmodel" >&2
54
+ exit 2
55
+ fi
56
+
57
+ REQUEST="$ROOT_DIR/outputs/request.txt"
58
+ "$PYTHON_BIN" "$ROOT_DIR/python/prepare_request.py" \
59
+ --config-dir "$ROOT_DIR/config" --text "$TEXT" --voice "$VOICE" \
60
+ --seed "$SEED" --max-new-frames "$MAX_NEW_FRAMES" \
61
+ --greedy-prefix-frames "$GREEDY_PREFIX_FRAMES" --output "$REQUEST"
62
+
63
+ "$ROOT_DIR/bin/moss_tts_ax650/moss_tts_ax650" \
64
+ --axmodel-dir "$ROOT_DIR/models/axmodels_650" \
65
+ --onnx-dir "$ROOT_DIR/models/onnxmodels" \
66
+ "${EXTRA[@]}" --request "$REQUEST" \
67
+ --output-wav "$OUTPUT_WAV" --max-new-frames "$MAX_NEW_FRAMES" \
68
+ --seed "$SEED" --greedy-prefix-frames "$GREEDY_PREFIX_FRAMES" \
69
+ 2>&1 | tee "$OUTPUT_LOG"
scripts/tts_runtime.py CHANGED
@@ -85,7 +85,9 @@ _AXMODEL_FILE_MAP: dict[str, str] = {
85
  }
86
 
87
  _ONNX_FILE_MAP: dict[str, str] = {
88
- "decode": "tts_decode_step.onnx",
 
 
89
  }
90
 
91
 
@@ -463,14 +465,18 @@ class AxTtsRuntime:
463
  axmodel_dir: str | Path,
464
  *,
465
  onnx_dir: str | Path | None = None,
 
466
  use_onnx_decode: bool = False,
 
467
  max_new_frames: int | None = None,
468
  do_sample: bool = True,
469
  sample_mode: str | None = None,
470
  ) -> None:
471
  self.config_dir = Path(config_dir).expanduser().resolve()
472
  self.axmodel_dir = Path(axmodel_dir).expanduser().resolve()
 
473
  self.use_onnx_decode = bool(use_onnx_decode)
 
474
  if onnx_dir is not None:
475
  self.onnx_dir = Path(onnx_dir).expanduser().resolve()
476
  else:
@@ -553,18 +559,34 @@ class AxTtsRuntime:
553
  sessions: dict[str, Any] = {}
554
  self._session_input_names: dict[str, list[str]] = {}
555
 
556
- if self.use_onnx_decode:
557
- onnx_name = _ONNX_FILE_MAP["decode"]
558
  onnx_path = self.onnx_dir / onnx_name
559
  if not onnx_path.exists():
560
  raise FileNotFoundError(f"ONNX not found: {onnx_path}")
561
  if not _HAVE_ORT:
562
- raise RuntimeError("onnxruntime not installed; cannot load ONNX decode_step.")
563
- sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
564
- sessions["decode"] = sess
565
- self._model_display_names["decode"] = f"{onnx_path.name} [onnxruntime CPU]"
566
- self._session_input_names["decode"] = [inp.name for inp in sess.get_inputs()]
567
- logging.info("loaded decode → %s (onnxruntime CPU)", onnx_path.name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  else:
569
  axmodel_name = _AXMODEL_FILE_MAP["decode"]
570
  axmodel_path = self.axmodel_dir / axmodel_name
@@ -577,7 +599,15 @@ class AxTtsRuntime:
577
  self._session_input_names["decode"] = [inp.name for inp in sess.get_inputs()]
578
  logging.info("loaded decode → %s (axmodel)", axmodel_path.name)
579
 
 
 
 
 
 
 
580
  for key in ("prefill", "local_decoder", "local_fixed_sampled_frame", "codec_decode", "codec_encode"):
 
 
581
  if key not in _AXMODEL_FILE_MAP:
582
  continue
583
  axmodel_name = _AXMODEL_FILE_MAP[key]
@@ -934,6 +964,8 @@ class AxTtsRuntime:
934
  global_hidden: np.ndarray,
935
  *,
936
  previous_token_sets_by_channel: list[set[int]],
 
 
937
  ) -> tuple[bool, list[int]]:
938
  n_vq = int(self.manifest["tts_config"]["n_vq"])
939
  codebook_size = int(self.tts_meta["model_config"]["audio_codebook_sizes"][0])
@@ -950,13 +982,17 @@ class AxTtsRuntime:
950
  }
951
  asst_u: float | None = None
952
  if "assistant_random_u" in session_input_names:
953
- asst_u = float(self.rng.random())
 
 
 
 
954
  input_feed["assistant_random_u"] = np.array([asst_u], dtype=np.float32)
955
  if "audio_random_u" in session_input_names:
956
- input_feed["audio_random_u"] = np.asarray(
957
- [[float(self.rng.random()) for _ in range(n_vq)]],
958
- dtype=np.float32,
959
- )
960
  outputs = self._run_session("local_fixed_sampled_frame", input_feed)
961
  out_names = [o.name for o in sess.get_outputs()]
962
  named = dict(zip(out_names, outputs, strict=True))
@@ -970,12 +1006,21 @@ class AxTtsRuntime:
970
  )
971
  return should_continue, [int(x) for x in frame_ids]
972
 
973
- def _can_use_local_fixed_sampled_frame(self) -> bool:
974
  sess = self.sessions.get("local_fixed_sampled_frame")
975
  if sess is None:
976
- return False
977
  session_input_names = {inp.name for inp in sess.get_inputs()}
978
- return "audio_random_u" in session_input_names
 
 
 
 
 
 
 
 
 
979
 
980
  def run_local_decoder(
981
  self,
@@ -1076,15 +1121,42 @@ class AxTtsRuntime:
1076
  consecutive_pad_frames = 0
1077
  _MAX_CONSECUTIVE_PAD_FRAMES = 10 # 0.8s silence → force stop (guard against quantized AXModel)
1078
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1079
  for step in range(int(generation_defaults["max_new_frames"])):
1080
  frame: list[int] = []
1081
 
1082
- if (
1083
- generation_defaults["sample_mode"] == SAMPLE_MODE_FIXED
1084
- and self._can_use_local_fixed_sampled_frame()
1085
- ):
1086
  should_continue, frame = self.run_local_fixed_sampled_frame(
1087
- global_hidden, previous_token_sets_by_channel=prev_tok_sets
 
 
 
 
 
1088
  )
1089
  if not should_continue:
1090
  logging.info("[gen] frame %d: should_continue=False → 停止生成", step)
@@ -1093,14 +1165,6 @@ class AxTtsRuntime:
1093
  prev_tokens[ci].append(tok)
1094
  prev_tok_sets[ci].add(tok)
1095
  elif "local_decoder" in self.sessions:
1096
- if (
1097
- generation_defaults["sample_mode"] == SAMPLE_MODE_FIXED
1098
- and "local_fixed_sampled_frame" in self.sessions
1099
- and step == 0
1100
- ):
1101
- logging.warning(
1102
- "local_fixed_sampled_frame 缺少 audio_random_u 输入,回退到 local_decoder 随机采样路径"
1103
- )
1104
  local_text_logits, _ = self.run_local_decoder(global_hidden, 0, [])
1105
  next_text = _sample_assistant_text_token(
1106
  local_text_logits, self.manifest, generation_defaults, self.rng
@@ -1319,6 +1383,8 @@ class AxTtsRuntime:
1319
  max_new_frames: int | None = None,
1320
  voice_clone_max_text_tokens: int = 75,
1321
  seed: int | None = None,
 
 
1322
  ) -> dict[str, Any]:
1323
  self._reset_timing_stats()
1324
  gen = self.manifest["generation_defaults"]
@@ -1333,6 +1399,14 @@ class AxTtsRuntime:
1333
  normalized_mode = self._normalize_sample_mode(sample_mode, do_sample)
1334
  gen["sample_mode"] = normalized_mode
1335
  gen["do_sample"] = normalized_mode != SAMPLE_MODE_GREEDY
 
 
 
 
 
 
 
 
1336
  if seed is not None:
1337
  self.rng = np.random.default_rng(int(seed))
1338
 
@@ -1415,6 +1489,29 @@ class AxTtsRuntime:
1415
  )
1416
  rtf = (total_infer_time / audio_duration_sec) if audio_duration_sec > 0 else float("inf")
1417
  model_time_total = sum(self._model_time_stats.values())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1418
  audio_path = out_path if streaming else _write_waveform_to_wav(out_path, final_waveform, sample_rate)
1419
  logging.info("已保存 %s sample_rate=%s frames=%s", audio_path, sample_rate, len(all_generated_frames))
1420
  logging.info(
@@ -1424,14 +1521,26 @@ class AxTtsRuntime:
1424
  rtf,
1425
  model_time_total,
1426
  )
 
 
 
 
 
 
 
 
1427
  for key in sorted(self._model_display_names):
1428
  model_name = self._model_display_names[key]
 
 
1429
  logging.info(
1430
- "[timing] session=%s model=%s calls=%d total=%.3fs",
1431
  key,
1432
  model_name,
1433
- self._model_call_stats.get(key, 0),
1434
- self._model_time_stats.get(key, 0.0),
 
 
1435
  )
1436
  return {
1437
  "audio_path": str(audio_path),
@@ -1447,8 +1556,14 @@ class AxTtsRuntime:
1447
  "audio_duration_sec": audio_duration_sec,
1448
  "rtf": rtf,
1449
  "model_time_total_sec": model_time_total,
 
 
 
 
 
1450
  "per_model_time_sec": dict(self._model_time_stats),
1451
  "per_model_calls": dict(self._model_call_stats),
 
1452
  "per_model_display_name": dict(self._model_display_names),
1453
  "used_model_keys": list(self._used_model_keys),
1454
  },
 
85
  }
86
 
87
  _ONNX_FILE_MAP: dict[str, str] = {
88
+ "prefill": "patched_moss_tts_prefill_finite_mask.onnx",
89
+ "decode": "moss_tts_decode_step.onnx",
90
+ "local_fixed_sampled_frame": "moss_tts_local_fixed_sampled_frame.onnx",
91
  }
92
 
93
 
 
465
  axmodel_dir: str | Path,
466
  *,
467
  onnx_dir: str | Path | None = None,
468
+ use_onnx_prefill: bool = False,
469
  use_onnx_decode: bool = False,
470
+ use_onnx_local_fixed: bool = False,
471
  max_new_frames: int | None = None,
472
  do_sample: bool = True,
473
  sample_mode: str | None = None,
474
  ) -> None:
475
  self.config_dir = Path(config_dir).expanduser().resolve()
476
  self.axmodel_dir = Path(axmodel_dir).expanduser().resolve()
477
+ self.use_onnx_prefill = bool(use_onnx_prefill)
478
  self.use_onnx_decode = bool(use_onnx_decode)
479
+ self.use_onnx_local_fixed = bool(use_onnx_local_fixed)
480
  if onnx_dir is not None:
481
  self.onnx_dir = Path(onnx_dir).expanduser().resolve()
482
  else:
 
559
  sessions: dict[str, Any] = {}
560
  self._session_input_names: dict[str, list[str]] = {}
561
 
562
+ def load_onnx_cpu(key: str) -> Any:
563
+ onnx_name = _ONNX_FILE_MAP[key]
564
  onnx_path = self.onnx_dir / onnx_name
565
  if not onnx_path.exists():
566
  raise FileNotFoundError(f"ONNX not found: {onnx_path}")
567
  if not _HAVE_ORT:
568
+ raise RuntimeError(
569
+ f"onnxruntime not installed; cannot load ONNX session {key}."
570
+ )
571
+ available_providers = ort.get_available_providers()
572
+ if "CPUExecutionProvider" not in available_providers:
573
+ raise RuntimeError(
574
+ "Hybrid inference requires ONNX Runtime CPUExecutionProvider; "
575
+ f"available providers: {available_providers}"
576
+ )
577
+ sess = ort.InferenceSession(
578
+ str(onnx_path), providers=["CPUExecutionProvider"]
579
+ )
580
+ sessions[key] = sess
581
+ self._model_display_names[key] = (
582
+ f"{onnx_path.name} [onnxruntime CPU]"
583
+ )
584
+ self._session_input_names[key] = [inp.name for inp in sess.get_inputs()]
585
+ logging.info("loaded %s → %s (onnxruntime CPU)", key, onnx_path.name)
586
+ return sess
587
+
588
+ if self.use_onnx_decode:
589
+ load_onnx_cpu("decode")
590
  else:
591
  axmodel_name = _AXMODEL_FILE_MAP["decode"]
592
  axmodel_path = self.axmodel_dir / axmodel_name
 
599
  self._session_input_names["decode"] = [inp.name for inp in sess.get_inputs()]
600
  logging.info("loaded decode → %s (axmodel)", axmodel_path.name)
601
 
602
+ if self.use_onnx_local_fixed:
603
+ load_onnx_cpu("local_fixed_sampled_frame")
604
+
605
+ if self.use_onnx_prefill:
606
+ load_onnx_cpu("prefill")
607
+
608
  for key in ("prefill", "local_decoder", "local_fixed_sampled_frame", "codec_decode", "codec_encode"):
609
+ if key in sessions:
610
+ continue
611
  if key not in _AXMODEL_FILE_MAP:
612
  continue
613
  axmodel_name = _AXMODEL_FILE_MAP[key]
 
964
  global_hidden: np.ndarray,
965
  *,
966
  previous_token_sets_by_channel: list[set[int]],
967
+ frame_index: int | None = None,
968
+ greedy_prefix_frames: int = 0,
969
  ) -> tuple[bool, list[int]]:
970
  n_vq = int(self.manifest["tts_config"]["n_vq"])
971
  codebook_size = int(self.tts_meta["model_config"]["audio_codebook_sizes"][0])
 
982
  }
983
  asst_u: float | None = None
984
  if "assistant_random_u" in session_input_names:
985
+ # Always consume the seeded draw so fixing only the assistant
986
+ # continue/end decision does not shift subsequent audio draws.
987
+ sampled_asst_u = float(self.rng.random())
988
+ fixed_asst_u = getattr(self, "local_fixed_assistant_random_u", None)
989
+ asst_u = sampled_asst_u if fixed_asst_u is None else float(fixed_asst_u)
990
  input_feed["assistant_random_u"] = np.array([asst_u], dtype=np.float32)
991
  if "audio_random_u" in session_input_names:
992
+ audio_random_u = [float(self.rng.random()) for _ in range(n_vq)]
993
+ if frame_index is not None and frame_index < int(greedy_prefix_frames):
994
+ audio_random_u = [0.0] * n_vq
995
+ input_feed["audio_random_u"] = np.asarray([audio_random_u], dtype=np.float32)
996
  outputs = self._run_session("local_fixed_sampled_frame", input_feed)
997
  out_names = [o.name for o in sess.get_outputs()]
998
  named = dict(zip(out_names, outputs, strict=True))
 
1006
  )
1007
  return should_continue, [int(x) for x in frame_ids]
1008
 
1009
+ def _local_fixed_sampled_frame_mode(self) -> str | None:
1010
  sess = self.sessions.get("local_fixed_sampled_frame")
1011
  if sess is None:
1012
+ return None
1013
  session_input_names = {inp.name for inp in sess.get_inputs()}
1014
+ required_inputs = {"global_hidden", "repetition_seen_mask"}
1015
+ supported_inputs = required_inputs | {"assistant_random_u", "audio_random_u"}
1016
+ if not required_inputs.issubset(session_input_names):
1017
+ return None
1018
+ if not session_input_names.issubset(supported_inputs):
1019
+ return None
1020
+ return "audio_sampling" if "audio_random_u" in session_input_names else "audio_argmax"
1021
+
1022
+ def _can_use_local_fixed_sampled_frame(self) -> bool:
1023
+ return self._local_fixed_sampled_frame_mode() is not None
1024
 
1025
  def run_local_decoder(
1026
  self,
 
1121
  consecutive_pad_frames = 0
1122
  _MAX_CONSECUTIVE_PAD_FRAMES = 10 # 0.8s silence → force stop (guard against quantized AXModel)
1123
 
1124
+ local_fixed_mode = self._local_fixed_sampled_frame_mode()
1125
+ use_local_fixed = (
1126
+ generation_defaults["sample_mode"] == SAMPLE_MODE_FIXED
1127
+ and local_fixed_mode is not None
1128
+ )
1129
+ if use_local_fixed:
1130
+ logging.info(
1131
+ "[local-route] fast_path=local_fixed_sampled_frame mode=%s calls_per_frame=1",
1132
+ local_fixed_mode,
1133
+ )
1134
+ else:
1135
+ if "local_decoder" not in self.sessions:
1136
+ raise RuntimeError(
1137
+ "local-fixed fast path is unavailable and local_decoder fallback was not loaded"
1138
+ )
1139
+ fallback_reason = (
1140
+ f"sample_mode={generation_defaults['sample_mode']}"
1141
+ if generation_defaults["sample_mode"] != SAMPLE_MODE_FIXED
1142
+ else "local_fixed inputs are unsupported or model is missing"
1143
+ )
1144
+ logging.warning(
1145
+ "[local-route] slow_path=local_decoder calls_per_frame=17 reason=%s",
1146
+ fallback_reason,
1147
+ )
1148
+
1149
  for step in range(int(generation_defaults["max_new_frames"])):
1150
  frame: list[int] = []
1151
 
1152
+ if use_local_fixed:
 
 
 
1153
  should_continue, frame = self.run_local_fixed_sampled_frame(
1154
+ global_hidden,
1155
+ previous_token_sets_by_channel=prev_tok_sets,
1156
+ frame_index=step,
1157
+ greedy_prefix_frames=int(
1158
+ generation_defaults.get("greedy_prefix_frames", 0)
1159
+ ),
1160
  )
1161
  if not should_continue:
1162
  logging.info("[gen] frame %d: should_continue=False → 停止生成", step)
 
1165
  prev_tokens[ci].append(tok)
1166
  prev_tok_sets[ci].add(tok)
1167
  elif "local_decoder" in self.sessions:
 
 
 
 
 
 
 
 
1168
  local_text_logits, _ = self.run_local_decoder(global_hidden, 0, [])
1169
  next_text = _sample_assistant_text_token(
1170
  local_text_logits, self.manifest, generation_defaults, self.rng
 
1383
  max_new_frames: int | None = None,
1384
  voice_clone_max_text_tokens: int = 75,
1385
  seed: int | None = None,
1386
+ greedy_prefix_frames: int = 0,
1387
+ assistant_random_u: float | None = None,
1388
  ) -> dict[str, Any]:
1389
  self._reset_timing_stats()
1390
  gen = self.manifest["generation_defaults"]
 
1399
  normalized_mode = self._normalize_sample_mode(sample_mode, do_sample)
1400
  gen["sample_mode"] = normalized_mode
1401
  gen["do_sample"] = normalized_mode != SAMPLE_MODE_GREEDY
1402
+ if int(greedy_prefix_frames) < 0:
1403
+ raise ValueError("greedy_prefix_frames must be non-negative")
1404
+ gen["greedy_prefix_frames"] = int(greedy_prefix_frames)
1405
+ if assistant_random_u is not None and not 0.0 <= float(assistant_random_u) < 1.0:
1406
+ raise ValueError("assistant_random_u must be in [0, 1)")
1407
+ self.local_fixed_assistant_random_u = (
1408
+ None if assistant_random_u is None else float(assistant_random_u)
1409
+ )
1410
  if seed is not None:
1411
  self.rng = np.random.default_rng(int(seed))
1412
 
 
1489
  )
1490
  rtf = (total_infer_time / audio_duration_sec) if audio_duration_sec > 0 else float("inf")
1491
  model_time_total = sum(self._model_time_stats.values())
1492
+ frame_audio_duration_sec = (
1493
+ float(self.codec_meta["codec_config"]["downsample_rate"]) / float(sample_rate)
1494
+ if sample_rate > 0 else 0.0
1495
+ )
1496
+ generated_audio_duration_sec = len(all_generated_frames) * frame_audio_duration_sec
1497
+ generation_model_keys = ("local_fixed_sampled_frame", "local_decoder", "decode")
1498
+ generation_model_time_sec = sum(
1499
+ self._model_time_stats.get(key, 0.0) for key in generation_model_keys
1500
+ )
1501
+ generation_model_rtf = (
1502
+ generation_model_time_sec / generated_audio_duration_sec
1503
+ if generated_audio_duration_sec > 0 else float("inf")
1504
+ )
1505
+ realtime_budget_ms_per_frame = frame_audio_duration_sec * 1000.0
1506
+ per_model_mean_ms = {
1507
+ key: (
1508
+ self._model_time_stats.get(key, 0.0)
1509
+ * 1000.0
1510
+ / self._model_call_stats.get(key, 1)
1511
+ if self._model_call_stats.get(key, 0) > 0 else 0.0
1512
+ )
1513
+ for key in self._model_display_names
1514
+ }
1515
  audio_path = out_path if streaming else _write_waveform_to_wav(out_path, final_waveform, sample_rate)
1516
  logging.info("已保存 %s sample_rate=%s frames=%s", audio_path, sample_rate, len(all_generated_frames))
1517
  logging.info(
 
1521
  rtf,
1522
  model_time_total,
1523
  )
1524
+ logging.info(
1525
+ "[timing] generated_frames=%d realtime_budget=%.3fms/frame "
1526
+ "generation_model_time=%.3fs generation_model_rtf=%.4f",
1527
+ len(all_generated_frames),
1528
+ realtime_budget_ms_per_frame,
1529
+ generation_model_time_sec,
1530
+ generation_model_rtf,
1531
+ )
1532
  for key in sorted(self._model_display_names):
1533
  model_name = self._model_display_names[key]
1534
+ calls = self._model_call_stats.get(key, 0)
1535
+ total_sec = self._model_time_stats.get(key, 0.0)
1536
  logging.info(
1537
+ "[timing] session=%s model=%s calls=%d total=%.3fs mean=%.3fms share=%.1f%%",
1538
  key,
1539
  model_name,
1540
+ calls,
1541
+ total_sec,
1542
+ per_model_mean_ms[key],
1543
+ (100.0 * total_sec / total_infer_time) if total_infer_time > 0 else 0.0,
1544
  )
1545
  return {
1546
  "audio_path": str(audio_path),
 
1556
  "audio_duration_sec": audio_duration_sec,
1557
  "rtf": rtf,
1558
  "model_time_total_sec": model_time_total,
1559
+ "frame_audio_duration_sec": frame_audio_duration_sec,
1560
+ "realtime_budget_ms_per_frame": realtime_budget_ms_per_frame,
1561
+ "generated_audio_duration_sec": generated_audio_duration_sec,
1562
+ "generation_model_time_sec": generation_model_time_sec,
1563
+ "generation_model_rtf": generation_model_rtf,
1564
  "per_model_time_sec": dict(self._model_time_stats),
1565
  "per_model_calls": dict(self._model_call_stats),
1566
+ "per_model_mean_ms": per_model_mean_ms,
1567
  "per_model_display_name": dict(self._model_display_names),
1568
  "used_model_keys": list(self._used_model_keys),
1569
  },
verify_package.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ cd "$ROOT_DIR"
5
+ required=(
6
+ README.md configuration.json config.json requirements.txt run_ax650.sh
7
+ python/infer_moss_tts.py python/prepare_request.py
8
+ scripts/axe_session.py scripts/tts_runtime.py scripts/__init__.py
9
+ config/browser_poc_manifest.json config/codec_browser_onnx_meta.json
10
+ config/tts_browser_onnx_meta.json config/tokenizer.model
11
+ models/axmodels_650/tts_prefill.axmodel models/axmodels_650/tts_decode_step.axmodel
12
+ models/axmodels_650/tts_local_fixed_sampled_frame.axmodel models/axmodels_650/codec_decode.axmodel
13
+ models/onnxmodels/moss_tts_decode_step.onnx models/onnxmodels/moss_tts_decode_step.data
14
+ models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.onnx
15
+ models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.data
16
+ models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx
17
+ models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx.data
18
+ models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.onnx
19
+ models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.data
20
+ bin/moss_tts_ax650/moss_tts_ax650 bin/moss_tts_ax650/libonnxruntime.so.1.23.2
21
+ )
22
+ for path in "${required[@]}"; do
23
+ [[ -f "$path" ]] || { echo "[FAIL] missing $path" >&2; exit 1; }
24
+ done
25
+ python3 -B - <<'PY'
26
+ import ast
27
+ from pathlib import Path
28
+
29
+ for path in (
30
+ Path("python/infer_moss_tts.py"),
31
+ Path("python/prepare_request.py"),
32
+ Path("scripts/axe_session.py"),
33
+ Path("scripts/tts_runtime.py"),
34
+ ):
35
+ ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
36
+ PY
37
+ bash -n run_ax650.sh
38
+ readelf -d bin/moss_tts_ax650/moss_tts_ax650 | grep -q 'libonnxruntime.so.1'
39
+ echo "[PASS] Hugging Face package structure and runtime dependencies are valid"