update: use static320 FP32 decode path and add config.json
Browse filesPromote the quality-equivalent static320 FP32 ONNX decode model to the default path. Update README/runtime scripts and remove legacy package layout files.
- .gitattributes +12 -0
- .gitignore +4 -0
- MODEL_SHA256SUMS +15 -0
- README.md +162 -89
- models/axmodels/tts_local_decoder.axmodel → bin/moss_tts_ax650/libonnxruntime.so +2 -2
- models/axmodels/tts_decode_step_u16.axmodel → bin/moss_tts_ax650/libonnxruntime.so.1 +2 -2
- models/axmodels/codec_encode.axmodel → bin/moss_tts_ax650/libonnxruntime.so.1.23.2 +2 -2
- bin/moss_tts_ax650/libonnxruntime_providers_shared.so +0 -0
- bin/moss_tts_ax650/moss_tts_ax650 +3 -0
- {configs → config}/browser_poc_manifest.json +0 -0
- {configs → config}/codec_browser_onnx_meta.json +0 -0
- {configs → config}/tokenizer.model +0 -0
- {configs → config}/tts_browser_onnx_meta.json +0 -0
- configuration.json +15 -0
- infer_board_axmodel_decode.py +0 -112
- models/{axmodels → axmodels_650}/codec_decode.axmodel +2 -2
- models/{axmodels → axmodels_650}/tts_decode_step.axmodel +2 -2
- models/{axmodels → axmodels_650}/tts_local_fixed_sampled_frame.axmodel +2 -2
- models/{axmodels → axmodels_650}/tts_prefill.axmodel +2 -2
- models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.data +3 -0
- models/onnxmodels/candidates/default_512_fp32/moss_tts_decode_step.onnx +3 -0
- models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.data +3 -0
- models/onnxmodels/candidates/static320_fp32/moss_tts_decode_step.onnx +3 -0
- models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx +3 -0
- models/onnxmodels/candidates/static320_int8_qdq/moss_tts_decode_step.onnx.data +3 -0
- models/onnxmodels/moss_tts_decode_step.data +3 -0
- models/onnxmodels/moss_tts_decode_step.onnx +3 -0
- models/onnxmodels/tts_decode_step.onnx +0 -3
- infer_board_onnx_decode.py → python/infer_moss_tts.py +98 -14
- python/prepare_request.py +146 -0
- requirements-hybrid.txt +2 -0
- requirements.txt +0 -3
- run_ax650.sh +69 -0
- scripts/tts_runtime.py +148 -33
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
MOSS-TTS-Nano
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
| 6 |
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
##
|
| 16 |
|
| 17 |
```text
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 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 |
-
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
```
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
```
|
| 37 |
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
```
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
```bash
|
| 49 |
-
|
| 50 |
-
--text "你好,今天是美好的一天" \
|
| 51 |
-
--
|
| 52 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
--max-new-frames 128 \
|
| 54 |
-
--
|
|
|
|
| 55 |
```
|
| 56 |
|
| 57 |
-
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
```
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
```
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
```bash
|
| 73 |
-
python3
|
| 74 |
-
--
|
| 75 |
-
--
|
| 76 |
-
--
|
| 77 |
-
--
|
| 78 |
-
--output-audio-path outputs/
|
| 79 |
```
|
| 80 |
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
```
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
| 121 |
|
| 122 |
-
|
| 123 |
|
| 124 |
```bash
|
| 125 |
-
|
| 126 |
-
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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 |
-
"
|
|
|
|
|
|
|
| 17 |
)
|
| 18 |
)
|
| 19 |
-
p.add_argument("--config-dir", default=str(_HERE / "
|
| 20 |
help="config directory (default: ./configs)")
|
| 21 |
-
p.add_argument("--axmodel-dir", default=str(_HERE / "models" / "
|
| 22 |
help="axmodel directory (default: ./models/axmodels)")
|
| 23 |
-
p.add_argument("--onnx-dir", default=str(_HERE / "models" / "onnxmodels"),
|
| 24 |
-
help="ONNX
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
p.add_argument("--output-audio-path",
|
| 26 |
-
default=str(_HERE / "outputs" / "
|
| 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(
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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:
|
| 84 |
-
logging.info("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
|
|
|
| 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 |
-
|
| 557 |
-
onnx_name = _ONNX_FILE_MAP[
|
| 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(
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
input_feed["assistant_random_u"] = np.array([asst_u], dtype=np.float32)
|
| 955 |
if "audio_random_u" in session_input_names:
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 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
|
| 974 |
sess = self.sessions.get("local_fixed_sampled_frame")
|
| 975 |
if sess is None:
|
| 976 |
-
return
|
| 977 |
session_input_names = {inp.name for inp in sess.get_inputs()}
|
| 978 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1434 |
-
|
|
|
|
|
|
|
| 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"
|