First commit
Browse files- .gitattributes +11 -0
- README.md +168 -3
- assets/moss_prompts/en_4_4p5s.wav +3 -0
- assets/moss_prompts/zh_1_4p5s.wav +3 -0
- assets/paragraphs/en_scavenger.txt +1 -0
- assets/paragraphs/zh_ginkgo.txt +1 -0
- configs/decoder4_split_manifest.json +74 -0
- configs/runtime_config.json +12 -0
- infer_zipvoice_axera.py +192 -0
- models/axmodels/decoder_part0.axmodel +3 -0
- models/axmodels/decoder_part1.axmodel +3 -0
- models/axmodels/decoder_part2.axmodel +3 -0
- models/axmodels/decoder_part3.axmodel +3 -0
- models/axmodels/encoder.axmodel +3 -0
- outputs/en_long_paragraph.wav +3 -0
- outputs/en_sentence.wav +3 -0
- outputs/zh_long_paragraph.wav +3 -0
- outputs/zh_sentence.wav +3 -0
- requirements.txt +9 -0
- resources/vocos-mel-24khz/config.yaml +24 -0
- resources/vocos-mel-24khz/pytorch_model.bin +3 -0
- resources/zipvoice_hf/zipvoice/model.json +26 -0
- resources/zipvoice_hf/zipvoice/tokens.txt +360 -0
- resources/zipvoice_hf/zipvoice/zipvoice_base.json +26 -0
- scripts/__init__.py +1 -0
- scripts/common_infer.py +68 -0
- scripts/local_audio.py +85 -0
- scripts/local_tokenizer.py +218 -0
- scripts/text_processing.py +390 -0
- scripts/zipvoice_decoder4_runtime.py +364 -0
- scripts/zipvoice_runtime.py +69 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,14 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/moss_prompts/en_4_4p5s.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/moss_prompts/zh_1_4p5s.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
models/axmodels/decoder_part0.axmodel filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
models/axmodels/decoder_part1.axmodel filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
models/axmodels/decoder_part2.axmodel filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
models/axmodels/decoder_part3.axmodel filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
models/axmodels/encoder.axmodel filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
outputs/en_long_paragraph.wav filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
outputs/en_sentence.wav filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
outputs/zh_long_paragraph.wav filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
outputs/zh_sentence.wav filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,3 +1,168 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ZipVoice.AXERA
|
| 2 |
+
|
| 3 |
+
ZipVoice 板端推理 demo。
|
| 4 |
+
|
| 5 |
+
## 功能
|
| 6 |
+
|
| 7 |
+
- 支持中文和英文语音生成。
|
| 8 |
+
- 支持语音克隆。
|
| 9 |
+
|
| 10 |
+
## 模型转换
|
| 11 |
+
|
| 12 |
+
For those interested in model conversion, refer to:
|
| 13 |
+
|
| 14 |
+
- [Pulsar2 Docs — How to Convert ONNX to axmodel](https://pulsar2-docs.readthedocs.io/en/latest/pulsar2/introduction.html)
|
| 15 |
+
- [export / quantization Project](https://github.com/AXERA-TECH/ZipVoice.AXERA)
|
| 16 |
+
|
| 17 |
+
## 支持平台
|
| 18 |
+
|
| 19 |
+
- AX650
|
| 20 |
+
- [M4N-Dock(爱芯派Pro)](https://wiki.sipeed.com/hardware/zh/maixIV/m4ndock/m4ndock.html)
|
| 21 |
+
- [M.2 Accelerator Card](https://docs.m5stack.com/zh_CN/ai_hardware/LLM-8850_Card)
|
| 22 |
+
|
| 23 |
+
## 目录结构
|
| 24 |
+
|
| 25 |
+
```text
|
| 26 |
+
ZipVoice.AXERA/
|
| 27 |
+
├── assets/
|
| 28 |
+
│ ├── moss_prompts/
|
| 29 |
+
│ └── paragraphs/
|
| 30 |
+
├── configs/
|
| 31 |
+
├── models/
|
| 32 |
+
│ └── axmodels/
|
| 33 |
+
├── resources/
|
| 34 |
+
│ ├── vocos-mel-24khz/
|
| 35 |
+
│ └── zipvoice_hf/
|
| 36 |
+
├── scripts/
|
| 37 |
+
├── infer_zipvoice_axera.py
|
| 38 |
+
├── requirements.txt
|
| 39 |
+
└── README.md
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## 环境
|
| 43 |
+
|
| 44 |
+
### 安装 pyaxengine
|
| 45 |
+
|
| 46 |
+
[pyaxengine Releases](https://github.com/AXERA-TECH/pyaxengine/releases/latest) 下载对应版本的 `.whl` 文件,然后安装:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
pip3 install axengine-x.x.x-py3-none-any.whl
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### Python 依赖
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
pip3 install -r requirements.txt
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## 推理命令
|
| 59 |
+
|
| 60 |
+
进入目录:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
cd /root/huyuan/workspace/TTS/ZipVoice.AXERA
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### 句子
|
| 67 |
+
|
| 68 |
+
中文:
|
| 69 |
+
```bash
|
| 70 |
+
python3 infer_zipvoice_axera.py \
|
| 71 |
+
--text "今天午后天气很好,我打开窗户,听见远处有人聊天,水杯也轻轻晃了一下。" \
|
| 72 |
+
--prompt-text "不管怎么样我和汤姆还是要感谢贝尔卡金的援手" \
|
| 73 |
+
--prompt-wav assets/moss_prompts/zh_1_4p5s.wav \
|
| 74 |
+
--output-wav outputs/zh_sentence.wav \
|
| 75 |
+
--speed 1.0 \
|
| 76 |
+
--num-step 10 \
|
| 77 |
+
--seed 42
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
推理结果:
|
| 81 |
+
```text
|
| 82 |
+
推理耗时: 5.781s
|
| 83 |
+
生成语音耗时: 6.411s
|
| 84 |
+
RTF: 0.9018
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
音频:[outputs/zh_sentence.wav](outputs/zh_sentence.wav)
|
| 88 |
+
|
| 89 |
+
英文:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
python3 infer_zipvoice_axera.py \
|
| 93 |
+
--text "This morning, a small train left the station, carrying sleepy passengers toward a bright coastal town." \
|
| 94 |
+
--prompt-text "This is almost twice the current industry production level per train." \
|
| 95 |
+
--prompt-wav assets/moss_prompts/en_4_4p5s.wav \
|
| 96 |
+
--output-wav outputs/en_sentence.wav \
|
| 97 |
+
--speed 1.0 \
|
| 98 |
+
--num-step 10 \
|
| 99 |
+
--seed 42
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
推理结果:
|
| 103 |
+
```text
|
| 104 |
+
推理耗时: 5.711s
|
| 105 |
+
生成语音耗时: 6.411s
|
| 106 |
+
RTF: 0.8909
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
音频:[outputs/en_sentence.wav](outputs/en_sentence.wav)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
### 段落
|
| 113 |
+
|
| 114 |
+
中文:
|
| 115 |
+
```bash
|
| 116 |
+
python3 infer_zipvoice_axera.py \
|
| 117 |
+
--text-file assets/paragraphs/zh_ginkgo.txt \
|
| 118 |
+
--prompt-text "不管怎么样我和汤姆还是要感谢贝尔卡金的援手" \
|
| 119 |
+
--prompt-wav assets/moss_prompts/zh_1_4p5s.wav \
|
| 120 |
+
--output-wav outputs/zh_long_paragraph.wav \
|
| 121 |
+
--speed 1.0 \
|
| 122 |
+
--num-step 10 \
|
| 123 |
+
--seed 42
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
推理结果:
|
| 127 |
+
```text
|
| 128 |
+
推理耗时: 40.292s
|
| 129 |
+
生成语音耗时: 44.744s
|
| 130 |
+
RTF: 0.9005
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
音频:[outputs/zh_long_paragraph.wav](outputs/zh_long_paragraph.wav)
|
| 134 |
+
|
| 135 |
+
英文:
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
python3 infer_zipvoice_axera.py \
|
| 139 |
+
--text-file assets/paragraphs/en_scavenger.txt \
|
| 140 |
+
--prompt-text "This is almost twice the current industry production level per train." \
|
| 141 |
+
--prompt-wav assets/moss_prompts/en_4_4p5s.wav \
|
| 142 |
+
--output-wav outputs/en_long_paragraph.wav \
|
| 143 |
+
--speed 1.0 \
|
| 144 |
+
--num-step 10 \
|
| 145 |
+
--seed 42
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
推理结果:
|
| 149 |
+
```
|
| 150 |
+
推理耗时: 62.161s
|
| 151 |
+
生成语音耗时: 64.749s
|
| 152 |
+
RTF: 0.9600
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
音频:[outputs/en_long_paragraph.wav](outputs/en_long_paragraph.wav)
|
| 156 |
+
|
| 157 |
+
## 参数说明
|
| 158 |
+
|
| 159 |
+
- `--prompt-wav`:参考音频,用于控制音色。
|
| 160 |
+
- `--prompt-text`:参考音频对应文本,必须尽量和 `prompt-wav` 内容一致,否则 tokenizer 长度和音频帧数不匹配,会影响语速和效果。
|
| 161 |
+
- `--speed`:控制生成帧数,值越大音频越短,语速越快;值越小音频越长,语速越慢。
|
| 162 |
+
- `--num-step`:采样步数。步数越大通常效果更稳,推理耗时线性增加(当 num-step=8 时,以上推理的 rtf=0.75 左右)。
|
| 163 |
+
- `--max-feat-len`:decoder 固定 feature 长度,当前为 1024。
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
## 参考
|
| 167 |
+
|
| 168 |
+
- [ZipVoice](https://github.com/k2-fsa/ZipVoice)
|
assets/moss_prompts/en_4_4p5s.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fa360ae39ff73bfee70f10ba997ff5ae5c15c1dc25bb382510f6e9654f0f11b8
|
| 3 |
+
size 216078
|
assets/moss_prompts/zh_1_4p5s.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6f377c00e0cce4c1ca699bd0e53b553151faeddcb1755a5e9604cdbbeb67e2eb
|
| 3 |
+
size 216078
|
assets/paragraphs/en_scavenger.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
At dusk, I saw an old scavenger digging through a bin on a quiet street corner. His clothes were torn, his back bent from years of labor. I was about to walk past him, as most people did, when something made me stop. From the pile of waste, he pulled out a thin, battered book. Curious, I stepped closer. It was a collection of poems, its pages yellowed and its cover stained. Instead of tossing it aside, the old man wiped the dirt off with his sleeve. Then, in a soft, trembling voice, he began to read: "Beside the eastern fence, I pick chrysanthemums; in quiet leisure, I see the southern hills." The words floated in the evening air. At that moment, the setting sun touched his silver hair, and his eyes, bright and clear, seemed to belong to someone else entirely. He was no longer a scavenger standing by garbage; he was a traveler in a world of beauty and peace. After a long while, he closed the book, pressed it gently to his chest, and slipped it inside his worn jacket. Then he went back to searching for plastic bottles, his movements slow and steady. I stood there, speechless.
|
assets/paragraphs/zh_ginkgo.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
窗外的银杏叶又黄了。我坐在老家的院子里,看那一片片扇形的叶子在风中轻轻摇摆,像是无数小扇子,扇走了夏天最后的暑气,也扇来了秋天的凉意。阳光透过枝叶的缝隙洒下来,在地上投下斑驳的光影,那光影随着风不停地晃动,像一群调皮的孩子在捉迷藏。说起来,这棵银杏树是我爷爷年轻时种下的,算算已有十多年了。爷爷去年秋天走了,走的时候,银杏叶正黄得灿烂。我记得他临终前说的最后一句话是:“叶子落了,来年还会再长。”当时我不太明白这句话的意思,现在看着满树金黄,忽然好像懂了些什么。
|
configs/decoder4_split_manifest.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": 2,
|
| 3 |
+
"source_summary": "/data/huyuan/workspace/LLM/TTS/ZIPVOICE_RICH_CHECK0_REQUANT_SUMMARY.md",
|
| 4 |
+
"source_static_onnx_manifest": "/data/huyuan/workspace/LLM/TTS/ZipVoice.AXERA/models/static_mask_1024_cfgfix_nolog_encoder_attn_loop_sigmoid_bump_decoder4/decoder4_split_manifest.json",
|
| 5 |
+
"source_encoder_axmodel": "/data/huyuan/workspace/LLM/TTS/ZipVoice.AXERA/model_convert/axmodels_nolog_encoder_core_rich_check0/encoder_core_nolog_rich_check0.axmodel",
|
| 6 |
+
"source_decoder_axmodel_dir": "/data/huyuan/workspace/LLM/TTS/ZipVoice.AXERA/model_convert/axmodels_attn_loop_sigmoid_bump_decoder4_rich_check0",
|
| 7 |
+
"encoder": {
|
| 8 |
+
"name": "encoder_core",
|
| 9 |
+
"file": "encoder.axmodel",
|
| 10 |
+
"inputs": [
|
| 11 |
+
"cat_tokens"
|
| 12 |
+
],
|
| 13 |
+
"outputs": [
|
| 14 |
+
"encoded"
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
"decoder_parts": [
|
| 18 |
+
{
|
| 19 |
+
"name": "fm_decoder_part0",
|
| 20 |
+
"file": "decoder_part0.axmodel",
|
| 21 |
+
"inputs": [
|
| 22 |
+
"t",
|
| 23 |
+
"x",
|
| 24 |
+
"text_condition",
|
| 25 |
+
"speech_condition",
|
| 26 |
+
"guidance_scale",
|
| 27 |
+
"padding_mask"
|
| 28 |
+
],
|
| 29 |
+
"outputs": [
|
| 30 |
+
"decoder_hidden_p0",
|
| 31 |
+
"time_hidden",
|
| 32 |
+
"padding_mask2",
|
| 33 |
+
"cfg_scale"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"name": "fm_decoder_part1",
|
| 38 |
+
"file": "decoder_part1.axmodel",
|
| 39 |
+
"inputs": [
|
| 40 |
+
"decoder_hidden_p0",
|
| 41 |
+
"time_hidden",
|
| 42 |
+
"padding_mask2"
|
| 43 |
+
],
|
| 44 |
+
"outputs": [
|
| 45 |
+
"decoder_hidden_p1"
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"name": "fm_decoder_part2",
|
| 50 |
+
"file": "decoder_part2.axmodel",
|
| 51 |
+
"inputs": [
|
| 52 |
+
"decoder_hidden_p1",
|
| 53 |
+
"time_hidden",
|
| 54 |
+
"padding_mask2"
|
| 55 |
+
],
|
| 56 |
+
"outputs": [
|
| 57 |
+
"decoder_hidden_p2"
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"name": "fm_decoder_part3",
|
| 62 |
+
"file": "decoder_part3.axmodel",
|
| 63 |
+
"inputs": [
|
| 64 |
+
"decoder_hidden_p2",
|
| 65 |
+
"time_hidden",
|
| 66 |
+
"padding_mask2",
|
| 67 |
+
"cfg_scale"
|
| 68 |
+
],
|
| 69 |
+
"outputs": [
|
| 70 |
+
"v"
|
| 71 |
+
]
|
| 72 |
+
}
|
| 73 |
+
]
|
| 74 |
+
}
|
configs/runtime_config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "zipvoice_decoder4",
|
| 3 |
+
"max_tokens": 384,
|
| 4 |
+
"max_feat_len": 1024,
|
| 5 |
+
"feat_dim": 100,
|
| 6 |
+
"sampling_rate": 24000,
|
| 7 |
+
"hop_length": 256,
|
| 8 |
+
"num_step": 16,
|
| 9 |
+
"t_shift": 0.5,
|
| 10 |
+
"guidance_scale": 1.0,
|
| 11 |
+
"speed": 1.0
|
| 12 |
+
}
|
infer_zipvoice_axera.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import logging
|
| 7 |
+
import time
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import soundfile as sf
|
| 12 |
+
|
| 13 |
+
from scripts.common_infer import extract_prompt_features, load_tokenizer
|
| 14 |
+
from scripts.common_infer import load_vocoder, vocoder_decode_loaded
|
| 15 |
+
from scripts.text_processing import build_cat_tokens, build_segments
|
| 16 |
+
from scripts.text_processing import load_text, normalize_punctuation, setup_jieba_cache
|
| 17 |
+
from scripts.zipvoice_decoder4_runtime import Decoder4ZipVoiceBoardRuntime
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def parse_args() -> argparse.Namespace:
|
| 21 |
+
repo_dir = Path(__file__).resolve().parent
|
| 22 |
+
p = argparse.ArgumentParser(
|
| 23 |
+
description="ZipVoice encoder + decoder4 inference on AX650",
|
| 24 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 25 |
+
)
|
| 26 |
+
p.add_argument("--config-dir", default=str(repo_dir / "configs"))
|
| 27 |
+
p.add_argument("--model-dir", default=str(repo_dir / "models" / "axmodels"))
|
| 28 |
+
p.add_argument("--text", default=None, help="Text to synthesize")
|
| 29 |
+
p.add_argument("--text-file", default=None, help="UTF-8 text file containing text")
|
| 30 |
+
p.add_argument("--prompt-text", required=True)
|
| 31 |
+
p.add_argument("--prompt-wav", required=True)
|
| 32 |
+
p.add_argument("--output-wav", default="outputs/zipvoice_axera.wav")
|
| 33 |
+
p.add_argument("--num-step", type=int, default=12)
|
| 34 |
+
p.add_argument("--guidance-scale", type=float, default=1.0)
|
| 35 |
+
p.add_argument("--speed", type=float, default=0.85)
|
| 36 |
+
p.add_argument("--t-shift", type=float, default=0.5)
|
| 37 |
+
p.add_argument("--seed", type=int, default=42)
|
| 38 |
+
p.add_argument("--max-tokens", type=int, default=384)
|
| 39 |
+
p.add_argument("--max-feat-len", type=int, default=1024)
|
| 40 |
+
p.add_argument("--feat-scale", type=float, default=0.1)
|
| 41 |
+
p.add_argument("--target-rms", type=float, default=0.1)
|
| 42 |
+
p.add_argument("--min-generated-frames", type=int, default=360)
|
| 43 |
+
p.add_argument("--max-generated-frames", type=int, default=620)
|
| 44 |
+
p.add_argument(
|
| 45 |
+
"--max-raw-feat-ratio",
|
| 46 |
+
type=float,
|
| 47 |
+
default=1.2,
|
| 48 |
+
help="Allow raw features_len up to this ratio of max_feat_len before splitting",
|
| 49 |
+
)
|
| 50 |
+
p.add_argument("--silence-ms", type=int, default=140)
|
| 51 |
+
return p.parse_args()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def main() -> None:
|
| 55 |
+
args = parse_args()
|
| 56 |
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
| 57 |
+
|
| 58 |
+
repo_dir = Path(__file__).resolve().parent
|
| 59 |
+
setup_jieba_cache(repo_dir)
|
| 60 |
+
output_wav = Path(args.output_wav)
|
| 61 |
+
output_wav.parent.mkdir(parents=True, exist_ok=True)
|
| 62 |
+
|
| 63 |
+
t_target_text_start = time.perf_counter()
|
| 64 |
+
original_text = load_text(args)
|
| 65 |
+
text = normalize_punctuation(original_text)
|
| 66 |
+
target_text_preprocess_sec = time.perf_counter() - t_target_text_start
|
| 67 |
+
prompt_text = normalize_punctuation(args.prompt_text)
|
| 68 |
+
if text != original_text:
|
| 69 |
+
logging.debug("Normalized text punctuation: %s", text)
|
| 70 |
+
if prompt_text != args.prompt_text:
|
| 71 |
+
logging.debug("Normalized prompt punctuation: %s", prompt_text)
|
| 72 |
+
|
| 73 |
+
logging.debug("Loading tokenizer...")
|
| 74 |
+
tokenizer = load_tokenizer(repo_dir)
|
| 75 |
+
prompt_tokens = tokenizer.texts_to_token_ids([prompt_text])[0]
|
| 76 |
+
max_text_tokens = args.max_tokens - len(prompt_tokens) - 1
|
| 77 |
+
if max_text_tokens <= 0:
|
| 78 |
+
raise ValueError("prompt_tokens leaves no room for text")
|
| 79 |
+
|
| 80 |
+
logging.debug("Extracting prompt features from %s...", args.prompt_wav)
|
| 81 |
+
prompt_features, prompt_rms = extract_prompt_features(
|
| 82 |
+
args.prompt_wav,
|
| 83 |
+
repo_dir=repo_dir,
|
| 84 |
+
sampling_rate=24000,
|
| 85 |
+
feat_scale=args.feat_scale,
|
| 86 |
+
target_rms=args.target_rms,
|
| 87 |
+
)
|
| 88 |
+
prompt_frames = int(prompt_features.shape[1])
|
| 89 |
+
logging.debug(
|
| 90 |
+
"prompt_tokens=%d, prompt_frames=%d",
|
| 91 |
+
len(prompt_tokens),
|
| 92 |
+
prompt_frames,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
t_segment_planning_start = time.perf_counter()
|
| 96 |
+
segments = build_segments(
|
| 97 |
+
tokenizer=tokenizer,
|
| 98 |
+
text=text,
|
| 99 |
+
prompt_frames=prompt_frames,
|
| 100 |
+
prompt_tokens_len=len(prompt_tokens),
|
| 101 |
+
speed=args.speed,
|
| 102 |
+
max_feat_len=args.max_feat_len,
|
| 103 |
+
max_text_tokens=max_text_tokens,
|
| 104 |
+
min_generated_frames=args.min_generated_frames,
|
| 105 |
+
max_generated_frames=args.max_generated_frames,
|
| 106 |
+
max_raw_feat_ratio=args.max_raw_feat_ratio,
|
| 107 |
+
)
|
| 108 |
+
segment_planning_sec = time.perf_counter() - t_segment_planning_start
|
| 109 |
+
logging.debug("Built %d segments", len(segments))
|
| 110 |
+
for index, segment in enumerate(segments, start=1):
|
| 111 |
+
logging.debug(
|
| 112 |
+
"segment %02d: text_tokens=%d generated_frames=%d features_len=%d clamped=%s text=%s",
|
| 113 |
+
index,
|
| 114 |
+
segment["text_tokens"],
|
| 115 |
+
segment["generated_frames"],
|
| 116 |
+
segment["features_len"],
|
| 117 |
+
segment["clamped"],
|
| 118 |
+
segment["text"],
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
logging.info("正在加载模型...")
|
| 122 |
+
runtime = Decoder4ZipVoiceBoardRuntime(
|
| 123 |
+
config_dir=args.config_dir,
|
| 124 |
+
models_dir=args.model_dir,
|
| 125 |
+
max_feat_len=args.max_feat_len,
|
| 126 |
+
max_tokens=args.max_tokens,
|
| 127 |
+
num_step=args.num_step,
|
| 128 |
+
t_shift=args.t_shift,
|
| 129 |
+
)
|
| 130 |
+
logging.debug("Loading vocoder...")
|
| 131 |
+
vocoder = load_vocoder(repo_dir)
|
| 132 |
+
logging.info("模型加载完成")
|
| 133 |
+
|
| 134 |
+
audios: list[np.ndarray] = []
|
| 135 |
+
silence = np.zeros(int(24000 * args.silence_ms / 1000), dtype=np.float32)
|
| 136 |
+
total_segment_sec = 0.0
|
| 137 |
+
|
| 138 |
+
for index, segment in enumerate(segments, start=1):
|
| 139 |
+
logging.info("开始推理第 %d/%d 句", index, len(segments))
|
| 140 |
+
t_segment_start = time.perf_counter()
|
| 141 |
+
seg_text = str(segment["text"])
|
| 142 |
+
text_tokens = tokenizer.texts_to_token_ids([seg_text])[0]
|
| 143 |
+
cat_tokens = build_cat_tokens(tokenizer, prompt_tokens, text_tokens, args.max_tokens)
|
| 144 |
+
|
| 145 |
+
pred_features, timing = runtime.sample(
|
| 146 |
+
cat_tokens=cat_tokens,
|
| 147 |
+
prompt_tokens_len=len(prompt_tokens),
|
| 148 |
+
text_tokens_len=len(text_tokens),
|
| 149 |
+
prompt_features=prompt_features,
|
| 150 |
+
prompt_features_len=prompt_frames,
|
| 151 |
+
speed=args.speed,
|
| 152 |
+
guidance_scale=args.guidance_scale,
|
| 153 |
+
seed=args.seed + index - 1,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
audio = vocoder_decode_loaded(
|
| 157 |
+
vocoder,
|
| 158 |
+
pred_features,
|
| 159 |
+
feat_scale=args.feat_scale,
|
| 160 |
+
target_rms=args.target_rms,
|
| 161 |
+
prompt_rms=prompt_rms,
|
| 162 |
+
)
|
| 163 |
+
segment_sec = time.perf_counter() - t_segment_start
|
| 164 |
+
audio_sec = len(audio) / 24000
|
| 165 |
+
|
| 166 |
+
if audios:
|
| 167 |
+
audios.append(silence)
|
| 168 |
+
audios.append(audio.astype(np.float32, copy=False))
|
| 169 |
+
|
| 170 |
+
total_segment_sec += segment_sec
|
| 171 |
+
logging.debug(
|
| 172 |
+
"segment %02d done: audio=%.2fs runtime=%.2fs",
|
| 173 |
+
index,
|
| 174 |
+
audio_sec,
|
| 175 |
+
segment_sec,
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
final_audio = np.concatenate(audios) if audios else np.zeros(0, dtype=np.float32)
|
| 179 |
+
sf.write(str(output_wav), final_audio, samplerate=24000)
|
| 180 |
+
final_audio_sec = len(final_audio) / 24000
|
| 181 |
+
runtime_sec_for_rtf = target_text_preprocess_sec + segment_planning_sec + total_segment_sec
|
| 182 |
+
rtf = (
|
| 183 |
+
runtime_sec_for_rtf / final_audio_sec if final_audio_sec > 0 else float("inf")
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
logging.info("推理耗时: %.3fs", runtime_sec_for_rtf)
|
| 187 |
+
logging.info("生成语音耗时: %.3fs", final_audio_sec)
|
| 188 |
+
logging.info("RTF: %.4f", rtf)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
if __name__ == "__main__":
|
| 192 |
+
main()
|
models/axmodels/decoder_part0.axmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c174cd5458a1490936055a5060b00f9198431b5b97a1cd64176fcb67378e548b
|
| 3 |
+
size 46276224
|
models/axmodels/decoder_part1.axmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:94d2c75c7b2199f829fe0a9a44762ac8bac194e3e5ae589826a69671f3031442
|
| 3 |
+
size 35358649
|
models/axmodels/decoder_part2.axmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c9eb7ceec3baaef0b4e56513a791ffedaa3fbea63aeac41fcdd96f6850bf0fb8
|
| 3 |
+
size 38312969
|
models/axmodels/decoder_part3.axmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2cb91ec05e2d4cb69de73b4e258ce8eb85ce7d507fa795029bb071a336c72821
|
| 3 |
+
size 47283806
|
models/axmodels/encoder.axmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7d6d7564e79edb513c0d456663c27a7606d272fa8f472b58364a59d9682a3ee3
|
| 3 |
+
size 14093939
|
outputs/en_long_paragraph.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d59398ed12e49d51011777d80a0dbbf8b3c6322be7ec4f12338e8ca2740ddc2
|
| 3 |
+
size 3108012
|
outputs/en_sentence.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cc94c226559874053edde5331913364b6e2cd01ef7dde58e3c8df7735296e377
|
| 3 |
+
size 307756
|
outputs/zh_long_paragraph.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6805938a16e5b4a257444c2b33897ffba0d3a5cdbdb7a2de9dd93dd629273947
|
| 3 |
+
size 2147756
|
outputs/zh_sentence.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2997789e759fd6852ea30c73226d04a09369dabb777244c75d641798b09f1dab
|
| 3 |
+
size 307756
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.24.0
|
| 2 |
+
soundfile
|
| 3 |
+
torch
|
| 4 |
+
torchaudio
|
| 5 |
+
vocos
|
| 6 |
+
jieba
|
| 7 |
+
pypinyin
|
| 8 |
+
piper_phonemize
|
| 9 |
+
|
resources/vocos-mel-24khz/config.yaml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
feature_extractor:
|
| 2 |
+
class_path: vocos.feature_extractors.MelSpectrogramFeatures
|
| 3 |
+
init_args:
|
| 4 |
+
sample_rate: 24000
|
| 5 |
+
n_fft: 1024
|
| 6 |
+
hop_length: 256
|
| 7 |
+
n_mels: 100
|
| 8 |
+
padding: center
|
| 9 |
+
|
| 10 |
+
backbone:
|
| 11 |
+
class_path: vocos.models.VocosBackbone
|
| 12 |
+
init_args:
|
| 13 |
+
input_channels: 100
|
| 14 |
+
dim: 512
|
| 15 |
+
intermediate_dim: 1536
|
| 16 |
+
num_layers: 8
|
| 17 |
+
|
| 18 |
+
head:
|
| 19 |
+
class_path: vocos.heads.ISTFTHead
|
| 20 |
+
init_args:
|
| 21 |
+
dim: 512
|
| 22 |
+
n_fft: 1024
|
| 23 |
+
hop_length: 256
|
| 24 |
+
padding: center
|
resources/vocos-mel-24khz/pytorch_model.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:97ec976ad1fd67a33ab2682d29c0ac7df85234fae875aefcc5fb215681a91b2a
|
| 3 |
+
size 54365991
|
resources/zipvoice_hf/zipvoice/model.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model" : {
|
| 3 |
+
"fm_decoder_downsampling_factor" : [1,2,4,2,1],
|
| 4 |
+
"fm_decoder_num_layers" : [2,2,4,4,4],
|
| 5 |
+
"fm_decoder_cnn_module_kernel" : [31,15,7,15,31],
|
| 6 |
+
"fm_decoder_feedforward_dim" : 1536,
|
| 7 |
+
"fm_decoder_num_heads" : 4,
|
| 8 |
+
"fm_decoder_dim" : 512,
|
| 9 |
+
"text_encoder_num_layers" : 4,
|
| 10 |
+
"text_encoder_feedforward_dim" : 512,
|
| 11 |
+
"text_encoder_cnn_module_kernel" : 9,
|
| 12 |
+
"text_encoder_num_heads" : 4,
|
| 13 |
+
"text_encoder_dim" : 192,
|
| 14 |
+
"query_head_dim" : 32,
|
| 15 |
+
"value_head_dim" : 12,
|
| 16 |
+
"pos_head_dim" : 4,
|
| 17 |
+
"pos_dim" : 48,
|
| 18 |
+
"time_embed_dim" : 192,
|
| 19 |
+
"text_embed_dim" : 192,
|
| 20 |
+
"feat_dim": 100
|
| 21 |
+
},
|
| 22 |
+
"feature" : {
|
| 23 |
+
"sampling_rate": 24000,
|
| 24 |
+
"type": "vocos"
|
| 25 |
+
}
|
| 26 |
+
}
|
resources/zipvoice_hf/zipvoice/tokens.txt
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_ 0
|
| 2 |
+
^ 1
|
| 3 |
+
$ 2
|
| 4 |
+
3
|
| 5 |
+
! 4
|
| 6 |
+
' 5
|
| 7 |
+
( 6
|
| 8 |
+
) 7
|
| 9 |
+
, 8
|
| 10 |
+
- 9
|
| 11 |
+
. 10
|
| 12 |
+
: 11
|
| 13 |
+
; 12
|
| 14 |
+
? 13
|
| 15 |
+
a 14
|
| 16 |
+
b 15
|
| 17 |
+
c 16
|
| 18 |
+
d 17
|
| 19 |
+
e 18
|
| 20 |
+
f 19
|
| 21 |
+
h 20
|
| 22 |
+
i 21
|
| 23 |
+
j 22
|
| 24 |
+
k 23
|
| 25 |
+
l 24
|
| 26 |
+
m 25
|
| 27 |
+
n 26
|
| 28 |
+
o 27
|
| 29 |
+
p 28
|
| 30 |
+
q 29
|
| 31 |
+
r 30
|
| 32 |
+
s 31
|
| 33 |
+
t 32
|
| 34 |
+
u 33
|
| 35 |
+
v 34
|
| 36 |
+
w 35
|
| 37 |
+
x 36
|
| 38 |
+
y 37
|
| 39 |
+
z 38
|
| 40 |
+
æ 39
|
| 41 |
+
ç 40
|
| 42 |
+
ð 41
|
| 43 |
+
ø 42
|
| 44 |
+
ħ 43
|
| 45 |
+
ŋ 44
|
| 46 |
+
œ 45
|
| 47 |
+
ǀ 46
|
| 48 |
+
ǁ 47
|
| 49 |
+
ǂ 48
|
| 50 |
+
ǃ 49
|
| 51 |
+
ɐ 50
|
| 52 |
+
ɑ 51
|
| 53 |
+
ɒ 52
|
| 54 |
+
ɓ 53
|
| 55 |
+
ɔ 54
|
| 56 |
+
ɕ 55
|
| 57 |
+
ɖ 56
|
| 58 |
+
ɗ 57
|
| 59 |
+
ɘ 58
|
| 60 |
+
ə 59
|
| 61 |
+
ɚ 60
|
| 62 |
+
ɛ 61
|
| 63 |
+
ɜ 62
|
| 64 |
+
ɞ 63
|
| 65 |
+
ɟ 64
|
| 66 |
+
ɠ 65
|
| 67 |
+
ɡ 66
|
| 68 |
+
ɢ 67
|
| 69 |
+
ɣ 68
|
| 70 |
+
ɤ 69
|
| 71 |
+
ɥ 70
|
| 72 |
+
ɦ 71
|
| 73 |
+
ɧ 72
|
| 74 |
+
ɨ 73
|
| 75 |
+
ɪ 74
|
| 76 |
+
ɫ 75
|
| 77 |
+
ɬ 76
|
| 78 |
+
ɭ 77
|
| 79 |
+
ɮ 78
|
| 80 |
+
ɯ 79
|
| 81 |
+
ɰ 80
|
| 82 |
+
ɱ 81
|
| 83 |
+
ɲ 82
|
| 84 |
+
ɳ 83
|
| 85 |
+
ɴ 84
|
| 86 |
+
ɵ 85
|
| 87 |
+
ɶ 86
|
| 88 |
+
ɸ 87
|
| 89 |
+
ɹ 88
|
| 90 |
+
ɺ 89
|
| 91 |
+
ɻ 90
|
| 92 |
+
ɽ 91
|
| 93 |
+
ɾ 92
|
| 94 |
+
ʀ 93
|
| 95 |
+
ʁ 94
|
| 96 |
+
ʂ 95
|
| 97 |
+
ʃ 96
|
| 98 |
+
ʄ 97
|
| 99 |
+
ʈ 98
|
| 100 |
+
ʉ 99
|
| 101 |
+
ʊ 100
|
| 102 |
+
ʋ 101
|
| 103 |
+
ʌ 102
|
| 104 |
+
ʍ 103
|
| 105 |
+
ʎ 104
|
| 106 |
+
ʏ 105
|
| 107 |
+
ʐ 106
|
| 108 |
+
ʑ 107
|
| 109 |
+
ʒ 108
|
| 110 |
+
ʔ 109
|
| 111 |
+
ʕ 110
|
| 112 |
+
ʘ 111
|
| 113 |
+
ʙ 112
|
| 114 |
+
ʛ 113
|
| 115 |
+
ʜ 114
|
| 116 |
+
ʝ 115
|
| 117 |
+
ʟ 116
|
| 118 |
+
ʡ 117
|
| 119 |
+
ʢ 118
|
| 120 |
+
ʲ 119
|
| 121 |
+
ˈ 120
|
| 122 |
+
ˌ 121
|
| 123 |
+
ː 122
|
| 124 |
+
ˑ 123
|
| 125 |
+
˞ 124
|
| 126 |
+
β 125
|
| 127 |
+
θ 126
|
| 128 |
+
χ 127
|
| 129 |
+
ᵻ 128
|
| 130 |
+
ⱱ 129
|
| 131 |
+
0 130
|
| 132 |
+
1 131
|
| 133 |
+
2 132
|
| 134 |
+
3 133
|
| 135 |
+
4 134
|
| 136 |
+
5 135
|
| 137 |
+
6 136
|
| 138 |
+
7 137
|
| 139 |
+
8 138
|
| 140 |
+
9 139
|
| 141 |
+
̧ 140
|
| 142 |
+
̃ 141
|
| 143 |
+
̪ 142
|
| 144 |
+
̯ 143
|
| 145 |
+
̩ 144
|
| 146 |
+
ʰ 145
|
| 147 |
+
ˤ 146
|
| 148 |
+
ε 147
|
| 149 |
+
↓ 148
|
| 150 |
+
# 149
|
| 151 |
+
" 150
|
| 152 |
+
↑ 151
|
| 153 |
+
̺ 152
|
| 154 |
+
̻ 153
|
| 155 |
+
g 154
|
| 156 |
+
ʦ 155
|
| 157 |
+
X 156
|
| 158 |
+
̝ 157
|
| 159 |
+
̊ 158
|
| 160 |
+
a1 159
|
| 161 |
+
a2 160
|
| 162 |
+
a3 161
|
| 163 |
+
a4 162
|
| 164 |
+
a5 163
|
| 165 |
+
ai1 164
|
| 166 |
+
ai2 165
|
| 167 |
+
ai3 166
|
| 168 |
+
ai4 167
|
| 169 |
+
ai5 168
|
| 170 |
+
an1 169
|
| 171 |
+
an2 170
|
| 172 |
+
an3 171
|
| 173 |
+
an4 172
|
| 174 |
+
an5 173
|
| 175 |
+
ang1 174
|
| 176 |
+
ang2 175
|
| 177 |
+
ang3 176
|
| 178 |
+
ang4 177
|
| 179 |
+
ang5 178
|
| 180 |
+
ao1 179
|
| 181 |
+
ao2 180
|
| 182 |
+
ao3 181
|
| 183 |
+
ao4 182
|
| 184 |
+
ao5 183
|
| 185 |
+
b0 184
|
| 186 |
+
c0 185
|
| 187 |
+
ch0 186
|
| 188 |
+
d0 187
|
| 189 |
+
e1 188
|
| 190 |
+
e2 189
|
| 191 |
+
e3 190
|
| 192 |
+
e4 191
|
| 193 |
+
e5 192
|
| 194 |
+
ei1 193
|
| 195 |
+
ei2 194
|
| 196 |
+
ei3 195
|
| 197 |
+
ei4 196
|
| 198 |
+
ei5 197
|
| 199 |
+
en1 198
|
| 200 |
+
en2 199
|
| 201 |
+
en3 200
|
| 202 |
+
en4 201
|
| 203 |
+
en5 202
|
| 204 |
+
eng1 203
|
| 205 |
+
eng2 204
|
| 206 |
+
eng3 205
|
| 207 |
+
eng4 206
|
| 208 |
+
eng5 207
|
| 209 |
+
er2 208
|
| 210 |
+
er3 209
|
| 211 |
+
er4 210
|
| 212 |
+
er5 211
|
| 213 |
+
f0 212
|
| 214 |
+
g0 213
|
| 215 |
+
g2 214
|
| 216 |
+
g3 215
|
| 217 |
+
g4 216
|
| 218 |
+
g5 217
|
| 219 |
+
h0 218
|
| 220 |
+
i1 219
|
| 221 |
+
i2 220
|
| 222 |
+
i3 221
|
| 223 |
+
i4 222
|
| 224 |
+
i5 223
|
| 225 |
+
ia1 224
|
| 226 |
+
ia2 225
|
| 227 |
+
ia3 226
|
| 228 |
+
ia4 227
|
| 229 |
+
ia5 228
|
| 230 |
+
ian1 229
|
| 231 |
+
ian2 230
|
| 232 |
+
ian3 231
|
| 233 |
+
ian4 232
|
| 234 |
+
ian5 233
|
| 235 |
+
iang1 234
|
| 236 |
+
iang2 235
|
| 237 |
+
iang3 236
|
| 238 |
+
iang4 237
|
| 239 |
+
iang5 238
|
| 240 |
+
iao1 239
|
| 241 |
+
iao2 240
|
| 242 |
+
iao3 241
|
| 243 |
+
iao4 242
|
| 244 |
+
iao5 243
|
| 245 |
+
ie1 244
|
| 246 |
+
ie2 245
|
| 247 |
+
ie3 246
|
| 248 |
+
ie4 247
|
| 249 |
+
ie5 248
|
| 250 |
+
in1 249
|
| 251 |
+
in2 250
|
| 252 |
+
in3 251
|
| 253 |
+
in4 252
|
| 254 |
+
in5 253
|
| 255 |
+
ing1 254
|
| 256 |
+
ing2 255
|
| 257 |
+
ing3 256
|
| 258 |
+
ing4 257
|
| 259 |
+
ing5 258
|
| 260 |
+
iong1 259
|
| 261 |
+
iong2 260
|
| 262 |
+
iong3 261
|
| 263 |
+
iong4 262
|
| 264 |
+
iu1 263
|
| 265 |
+
iu2 264
|
| 266 |
+
iu3 265
|
| 267 |
+
iu4 266
|
| 268 |
+
iu5 267
|
| 269 |
+
j0 268
|
| 270 |
+
k0 269
|
| 271 |
+
l0 270
|
| 272 |
+
m0 271
|
| 273 |
+
m1 272
|
| 274 |
+
m2 273
|
| 275 |
+
m4 274
|
| 276 |
+
m5 275
|
| 277 |
+
n0 276
|
| 278 |
+
n2 277
|
| 279 |
+
n3 278
|
| 280 |
+
n4 279
|
| 281 |
+
n5 280
|
| 282 |
+
ng5 281
|
| 283 |
+
o1 282
|
| 284 |
+
o2 283
|
| 285 |
+
o3 284
|
| 286 |
+
o4 285
|
| 287 |
+
o5 286
|
| 288 |
+
ong1 287
|
| 289 |
+
ong2 288
|
| 290 |
+
ong3 289
|
| 291 |
+
ong4 290
|
| 292 |
+
ong5 291
|
| 293 |
+
ou1 292
|
| 294 |
+
ou2 293
|
| 295 |
+
ou3 294
|
| 296 |
+
ou4 295
|
| 297 |
+
ou5 296
|
| 298 |
+
p0 297
|
| 299 |
+
q0 298
|
| 300 |
+
r0 299
|
| 301 |
+
s0 300
|
| 302 |
+
sh0 301
|
| 303 |
+
t0 302
|
| 304 |
+
u1 303
|
| 305 |
+
u2 304
|
| 306 |
+
u3 305
|
| 307 |
+
u4 306
|
| 308 |
+
u5 307
|
| 309 |
+
ua1 308
|
| 310 |
+
ua2 309
|
| 311 |
+
ua3 310
|
| 312 |
+
ua4 311
|
| 313 |
+
uai1 312
|
| 314 |
+
uai2 313
|
| 315 |
+
uai3 314
|
| 316 |
+
uai4 315
|
| 317 |
+
uai5 316
|
| 318 |
+
uan1 317
|
| 319 |
+
uan2 318
|
| 320 |
+
uan3 319
|
| 321 |
+
uan4 320
|
| 322 |
+
uan5 321
|
| 323 |
+
uang1 322
|
| 324 |
+
uang2 323
|
| 325 |
+
uang3 324
|
| 326 |
+
uang4 325
|
| 327 |
+
uang5 326
|
| 328 |
+
ue1 327
|
| 329 |
+
ue2 328
|
| 330 |
+
ue3 329
|
| 331 |
+
ue4 330
|
| 332 |
+
ui1 331
|
| 333 |
+
ui2 332
|
| 334 |
+
ui3 333
|
| 335 |
+
ui4 334
|
| 336 |
+
ui5 335
|
| 337 |
+
un1 336
|
| 338 |
+
un2 337
|
| 339 |
+
un3 338
|
| 340 |
+
un4 339
|
| 341 |
+
un5 340
|
| 342 |
+
uo1 341
|
| 343 |
+
uo2 342
|
| 344 |
+
uo3 343
|
| 345 |
+
uo4 344
|
| 346 |
+
uo5 345
|
| 347 |
+
v2 346
|
| 348 |
+
v3 347
|
| 349 |
+
v4 348
|
| 350 |
+
ve3 349
|
| 351 |
+
ve4 350
|
| 352 |
+
w0 351
|
| 353 |
+
x0 352
|
| 354 |
+
y0 353
|
| 355 |
+
z0 354
|
| 356 |
+
zh0 355
|
| 357 |
+
ê1 356
|
| 358 |
+
ê2 357
|
| 359 |
+
ê3 358
|
| 360 |
+
ê4 359
|
resources/zipvoice_hf/zipvoice/zipvoice_base.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model" : {
|
| 3 |
+
"fm_decoder_downsampling_factor" : [1,2,4,2,1],
|
| 4 |
+
"fm_decoder_num_layers" : [2,2,4,4,4],
|
| 5 |
+
"fm_decoder_cnn_module_kernel" : [31,15,7,15,31],
|
| 6 |
+
"fm_decoder_feedforward_dim" : 1536,
|
| 7 |
+
"fm_decoder_num_heads" : 4,
|
| 8 |
+
"fm_decoder_dim" : 512,
|
| 9 |
+
"text_encoder_num_layers" : 4,
|
| 10 |
+
"text_encoder_feedforward_dim" : 512,
|
| 11 |
+
"text_encoder_cnn_module_kernel" : 9,
|
| 12 |
+
"text_encoder_num_heads" : 4,
|
| 13 |
+
"text_encoder_dim" : 192,
|
| 14 |
+
"query_head_dim" : 32,
|
| 15 |
+
"value_head_dim" : 12,
|
| 16 |
+
"pos_head_dim" : 4,
|
| 17 |
+
"pos_dim" : 48,
|
| 18 |
+
"time_embed_dim" : 192,
|
| 19 |
+
"text_embed_dim" : 192,
|
| 20 |
+
"feat_dim": 100
|
| 21 |
+
},
|
| 22 |
+
"feature" : {
|
| 23 |
+
"sampling_rate": 24000,
|
| 24 |
+
"type": "vocos"
|
| 25 |
+
}
|
| 26 |
+
}
|
scripts/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# ZipVoice board inference scripts
|
scripts/common_infer.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load_tokenizer(repo_dir: str | Path):
|
| 9 |
+
from scripts.local_tokenizer import LocalEmiliaTokenizer
|
| 10 |
+
|
| 11 |
+
token_file = Path(repo_dir) / "resources" / "zipvoice_hf" / "zipvoice" / "tokens.txt"
|
| 12 |
+
if not token_file.is_file():
|
| 13 |
+
raise FileNotFoundError(f"tokens.txt not found: {token_file}")
|
| 14 |
+
return LocalEmiliaTokenizer(token_file=str(token_file))
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def extract_prompt_features(
|
| 18 |
+
prompt_wav: str | Path,
|
| 19 |
+
repo_dir: str | Path,
|
| 20 |
+
sampling_rate: int,
|
| 21 |
+
feat_scale: float,
|
| 22 |
+
target_rms: float,
|
| 23 |
+
):
|
| 24 |
+
from scripts.local_audio import LocalVocosFbank, load_prompt_wav, rms_norm
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
wav = load_prompt_wav(prompt_wav, sampling_rate=sampling_rate)
|
| 28 |
+
wav, prompt_rms = rms_norm(wav, target_rms)
|
| 29 |
+
extractor = LocalVocosFbank()
|
| 30 |
+
features = extractor.extract(wav, sampling_rate=sampling_rate)
|
| 31 |
+
if not isinstance(features, torch.Tensor):
|
| 32 |
+
features = torch.from_numpy(features)
|
| 33 |
+
features = features.unsqueeze(0) * feat_scale
|
| 34 |
+
return features.cpu().numpy().astype(np.float32), float(prompt_rms)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load_vocoder(repo_dir: str | Path):
|
| 38 |
+
from scripts.local_audio import load_local_vocos
|
| 39 |
+
import torch
|
| 40 |
+
|
| 41 |
+
vocoder_dir = Path(repo_dir) / "resources" / "vocos-mel-24khz"
|
| 42 |
+
if not (vocoder_dir / "config.yaml").is_file() or not (
|
| 43 |
+
vocoder_dir / "pytorch_model.bin"
|
| 44 |
+
).is_file():
|
| 45 |
+
raise FileNotFoundError(f"Local Vocos files not found in {vocoder_dir}")
|
| 46 |
+
vocoder = load_local_vocos(vocoder_dir)
|
| 47 |
+
vocoder = vocoder.to(torch.device("cpu"))
|
| 48 |
+
vocoder.eval()
|
| 49 |
+
return vocoder
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def vocoder_decode_loaded(
|
| 53 |
+
vocoder,
|
| 54 |
+
features: np.ndarray,
|
| 55 |
+
feat_scale: float,
|
| 56 |
+
target_rms: float,
|
| 57 |
+
prompt_rms: float,
|
| 58 |
+
) -> np.ndarray:
|
| 59 |
+
from scripts.local_audio import rms_norm
|
| 60 |
+
import torch
|
| 61 |
+
|
| 62 |
+
feat_tensor = torch.from_numpy(features).float().permute(0, 2, 1) / feat_scale
|
| 63 |
+
with torch.no_grad():
|
| 64 |
+
wav = vocoder.decode(feat_tensor).squeeze(1).clamp(-1, 1)
|
| 65 |
+
wav = rms_norm(wav, target_rms)[0]
|
| 66 |
+
if prompt_rms < target_rms:
|
| 67 |
+
wav = wav * prompt_rms / target_rms
|
| 68 |
+
return wav.squeeze().cpu().numpy()
|
scripts/local_audio.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torchaudio
|
| 8 |
+
from vocos import Vocos
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class VocosFbankConfig:
|
| 13 |
+
sampling_rate: int = 24000
|
| 14 |
+
n_mels: int = 100
|
| 15 |
+
n_fft: int = 1024
|
| 16 |
+
hop_length: int = 256
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def compute_num_frames(num_samples: int, hop_length: int) -> int:
|
| 20 |
+
return int((num_samples + hop_length // 2) // hop_length)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class LocalVocosFbank:
|
| 24 |
+
def __init__(self) -> None:
|
| 25 |
+
self.config = VocosFbankConfig()
|
| 26 |
+
self.fbank = torchaudio.transforms.MelSpectrogram(
|
| 27 |
+
sample_rate=self.config.sampling_rate,
|
| 28 |
+
n_fft=self.config.n_fft,
|
| 29 |
+
hop_length=self.config.hop_length,
|
| 30 |
+
n_mels=self.config.n_mels,
|
| 31 |
+
center=True,
|
| 32 |
+
power=1,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def extract(self, samples: torch.Tensor, sampling_rate: int) -> torch.Tensor:
|
| 36 |
+
if sampling_rate != self.config.sampling_rate:
|
| 37 |
+
raise ValueError(
|
| 38 |
+
f"Mismatched sampling rate: expected {self.config.sampling_rate}, got {sampling_rate}"
|
| 39 |
+
)
|
| 40 |
+
if samples.ndim == 1:
|
| 41 |
+
samples = samples.unsqueeze(0)
|
| 42 |
+
if samples.ndim != 2:
|
| 43 |
+
raise ValueError(f"Expected waveform shape [C, T], got {tuple(samples.shape)}")
|
| 44 |
+
if samples.shape[0] == 2:
|
| 45 |
+
samples = samples.mean(dim=0, keepdim=True)
|
| 46 |
+
|
| 47 |
+
mel = self.fbank(samples).clamp(min=1e-7).log()
|
| 48 |
+
mel = mel.reshape(-1, mel.shape[-1]).t()
|
| 49 |
+
num_frames = compute_num_frames(samples.shape[1], self.config.hop_length)
|
| 50 |
+
|
| 51 |
+
if mel.shape[0] > num_frames:
|
| 52 |
+
mel = mel[:num_frames]
|
| 53 |
+
elif mel.shape[0] < num_frames:
|
| 54 |
+
mel = torch.nn.functional.pad(
|
| 55 |
+
mel.unsqueeze(0),
|
| 56 |
+
(0, 0, 0, num_frames - mel.shape[0]),
|
| 57 |
+
mode="replicate",
|
| 58 |
+
).squeeze(0)
|
| 59 |
+
return mel
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def load_prompt_wav(prompt_wav: str | Path, sampling_rate: int) -> torch.Tensor:
|
| 63 |
+
wav, sr = torchaudio.load(str(prompt_wav))
|
| 64 |
+
if sr != sampling_rate:
|
| 65 |
+
wav = torchaudio.transforms.Resample(orig_freq=sr, new_freq=sampling_rate)(wav)
|
| 66 |
+
return wav
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def rms_norm(wav: torch.Tensor, target_rms: float):
|
| 70 |
+
wav_rms = torch.sqrt(torch.mean(torch.square(wav)))
|
| 71 |
+
if wav_rms < target_rms:
|
| 72 |
+
wav = wav * target_rms / wav_rms
|
| 73 |
+
return wav, wav_rms
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def load_local_vocos(vocoder_dir: str | Path):
|
| 77 |
+
vocoder_dir = Path(vocoder_dir)
|
| 78 |
+
vocoder = Vocos.from_hparams(str(vocoder_dir / "config.yaml"))
|
| 79 |
+
state_dict = torch.load(
|
| 80 |
+
str(vocoder_dir / "pytorch_model.bin"),
|
| 81 |
+
weights_only=True,
|
| 82 |
+
map_location="cpu",
|
| 83 |
+
)
|
| 84 |
+
vocoder.load_state_dict(state_dict)
|
| 85 |
+
return vocoder
|
scripts/local_tokenizer.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import re
|
| 5 |
+
from functools import reduce
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Dict, List
|
| 8 |
+
|
| 9 |
+
import jieba
|
| 10 |
+
from pypinyin import Style, lazy_pinyin
|
| 11 |
+
from pypinyin.contrib.tone_convert import to_finals_tone3, to_initials
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from piper_phonemize import phonemize_espeak
|
| 15 |
+
except Exception as ex: # pragma: no cover - board dependency check
|
| 16 |
+
raise RuntimeError(
|
| 17 |
+
f"{ex}\nPlease install piper_phonemize for English tokenization."
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
jieba.default_logger.setLevel(logging.INFO)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class EnglishTextNormalizer:
|
| 24 |
+
def normalize(self, text: str) -> str:
|
| 25 |
+
return text
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ChineseTextNormalizer:
|
| 29 |
+
def normalize(self, text: str) -> str:
|
| 30 |
+
try:
|
| 31 |
+
import cn2an
|
| 32 |
+
|
| 33 |
+
return cn2an.transform(text, "an2cn")
|
| 34 |
+
except Exception:
|
| 35 |
+
return text
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class LocalEmiliaTokenizer:
|
| 39 |
+
"""Small board-side Emilia tokenizer without lhotse/CutSet dependencies."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, token_file: str | Path):
|
| 42 |
+
self.english_normalizer = EnglishTextNormalizer()
|
| 43 |
+
self.chinese_normalizer = ChineseTextNormalizer()
|
| 44 |
+
self.token2id: Dict[str, int] = {}
|
| 45 |
+
with open(token_file, "r", encoding="utf-8") as f:
|
| 46 |
+
for line in f:
|
| 47 |
+
token, token_id = line.rstrip().split("\t")
|
| 48 |
+
self.token2id[token] = int(token_id)
|
| 49 |
+
self.pad_id = self.token2id["_"]
|
| 50 |
+
self.vocab_size = len(self.token2id)
|
| 51 |
+
|
| 52 |
+
def texts_to_token_ids(self, texts: List[str]) -> List[List[int]]:
|
| 53 |
+
return self.tokens_to_token_ids(self.texts_to_tokens(texts))
|
| 54 |
+
|
| 55 |
+
def texts_to_tokens(self, texts: List[str]) -> List[List[str]]:
|
| 56 |
+
phoneme_list = []
|
| 57 |
+
for text in texts:
|
| 58 |
+
text = self.map_punctuations(text)
|
| 59 |
+
segments = self.get_segment(text)
|
| 60 |
+
all_phoneme = []
|
| 61 |
+
for seg_text, seg_type in segments:
|
| 62 |
+
if seg_type == "zh":
|
| 63 |
+
all_phoneme += self.tokenize_zh(seg_text)
|
| 64 |
+
elif seg_type == "en":
|
| 65 |
+
all_phoneme += self.tokenize_en(seg_text)
|
| 66 |
+
elif seg_type == "pinyin":
|
| 67 |
+
all_phoneme += self.tokenize_pinyin(seg_text)
|
| 68 |
+
elif seg_type == "tag":
|
| 69 |
+
all_phoneme.append(seg_text)
|
| 70 |
+
else:
|
| 71 |
+
logging.debug("Skipping unknown language segment: %r", (seg_text, seg_type))
|
| 72 |
+
phoneme_list.append(all_phoneme)
|
| 73 |
+
return phoneme_list
|
| 74 |
+
|
| 75 |
+
def tokens_to_token_ids(self, tokens_list: List[List[str]]) -> List[List[int]]:
|
| 76 |
+
token_ids_list = []
|
| 77 |
+
for tokens in tokens_list:
|
| 78 |
+
token_ids = []
|
| 79 |
+
for token in tokens:
|
| 80 |
+
if token not in self.token2id:
|
| 81 |
+
logging.debug("Skip OOV token %s", token)
|
| 82 |
+
continue
|
| 83 |
+
token_ids.append(self.token2id[token])
|
| 84 |
+
token_ids_list.append(token_ids)
|
| 85 |
+
return token_ids_list
|
| 86 |
+
|
| 87 |
+
def tokenize_zh(self, text: str) -> List[str]:
|
| 88 |
+
try:
|
| 89 |
+
text = self.chinese_normalizer.normalize(text)
|
| 90 |
+
segs = list(jieba.cut(text))
|
| 91 |
+
full = lazy_pinyin(
|
| 92 |
+
segs,
|
| 93 |
+
style=Style.TONE3,
|
| 94 |
+
tone_sandhi=True,
|
| 95 |
+
neutral_tone_with_five=True,
|
| 96 |
+
)
|
| 97 |
+
phones = []
|
| 98 |
+
for item in full:
|
| 99 |
+
if not (item[0:-1].isalpha() and item[-1] in ("1", "2", "3", "4", "5")):
|
| 100 |
+
phones.append(item)
|
| 101 |
+
else:
|
| 102 |
+
phones.extend(self.separate_pinyin(item))
|
| 103 |
+
return phones
|
| 104 |
+
except Exception as ex:
|
| 105 |
+
logging.debug("Tokenization of Chinese text failed: %s", ex)
|
| 106 |
+
return []
|
| 107 |
+
|
| 108 |
+
def tokenize_en(self, text: str) -> List[str]:
|
| 109 |
+
try:
|
| 110 |
+
text = self.english_normalizer.normalize(text)
|
| 111 |
+
tokens = phonemize_espeak(text, "en-us")
|
| 112 |
+
return reduce(lambda x, y: x + y, tokens)
|
| 113 |
+
except Exception as ex:
|
| 114 |
+
logging.debug("Tokenization of English text failed: %s", ex)
|
| 115 |
+
return []
|
| 116 |
+
|
| 117 |
+
def tokenize_pinyin(self, text: str) -> List[str]:
|
| 118 |
+
try:
|
| 119 |
+
text = text.lstrip("<").rstrip(">")
|
| 120 |
+
if not (text[0:-1].isalpha() and text[-1] in ("1", "2", "3", "4", "5")):
|
| 121 |
+
logging.debug("Invalid pinyin token: %s", text)
|
| 122 |
+
return []
|
| 123 |
+
return self.separate_pinyin(text)
|
| 124 |
+
except Exception as ex:
|
| 125 |
+
logging.debug("Tokenize pinyin failed: %s", ex)
|
| 126 |
+
return []
|
| 127 |
+
|
| 128 |
+
@staticmethod
|
| 129 |
+
def separate_pinyin(text: str) -> List[str]:
|
| 130 |
+
pinyins = []
|
| 131 |
+
initial = to_initials(text, strict=False)
|
| 132 |
+
final = to_finals_tone3(text, strict=False, neutral_tone_with_five=True)
|
| 133 |
+
if initial:
|
| 134 |
+
pinyins.append(initial + "0")
|
| 135 |
+
if final:
|
| 136 |
+
pinyins.append(final)
|
| 137 |
+
return pinyins
|
| 138 |
+
|
| 139 |
+
@staticmethod
|
| 140 |
+
def map_punctuations(text: str) -> str:
|
| 141 |
+
replacements = {
|
| 142 |
+
",": ",",
|
| 143 |
+
"。": ".",
|
| 144 |
+
"!": "!",
|
| 145 |
+
"?": "?",
|
| 146 |
+
";": ";",
|
| 147 |
+
":": ":",
|
| 148 |
+
"、": ",",
|
| 149 |
+
"‘": "'",
|
| 150 |
+
"“": '"',
|
| 151 |
+
"”": '"',
|
| 152 |
+
"’": "'",
|
| 153 |
+
"⋯": "…",
|
| 154 |
+
"···": "…",
|
| 155 |
+
"・・・": "…",
|
| 156 |
+
"...": "…",
|
| 157 |
+
}
|
| 158 |
+
for src, dst in replacements.items():
|
| 159 |
+
text = text.replace(src, dst)
|
| 160 |
+
return text
|
| 161 |
+
|
| 162 |
+
def get_segment(self, text: str):
|
| 163 |
+
segments = []
|
| 164 |
+
types = []
|
| 165 |
+
temp_seg = ""
|
| 166 |
+
temp_lang = ""
|
| 167 |
+
parts = re.compile(r"[<[].*?[>\]]|.").findall(text)
|
| 168 |
+
for part in parts:
|
| 169 |
+
if self.is_chinese(part) or self.is_pinyin(part):
|
| 170 |
+
types.append("zh")
|
| 171 |
+
elif self.is_alphabet(part):
|
| 172 |
+
types.append("en")
|
| 173 |
+
else:
|
| 174 |
+
types.append("other")
|
| 175 |
+
|
| 176 |
+
for index, part_type in enumerate(types):
|
| 177 |
+
if index == 0:
|
| 178 |
+
temp_seg += parts[index]
|
| 179 |
+
temp_lang = part_type
|
| 180 |
+
elif temp_lang == "other":
|
| 181 |
+
temp_seg += parts[index]
|
| 182 |
+
temp_lang = part_type
|
| 183 |
+
elif part_type in [temp_lang, "other"]:
|
| 184 |
+
temp_seg += parts[index]
|
| 185 |
+
else:
|
| 186 |
+
segments.append((temp_seg, temp_lang))
|
| 187 |
+
temp_seg = parts[index]
|
| 188 |
+
temp_lang = part_type
|
| 189 |
+
segments.append((temp_seg, temp_lang))
|
| 190 |
+
return self.split_segments(segments)
|
| 191 |
+
|
| 192 |
+
@staticmethod
|
| 193 |
+
def split_segments(segments):
|
| 194 |
+
result = []
|
| 195 |
+
for temp_seg, temp_lang in segments:
|
| 196 |
+
parts = re.split(r"([<[].*?[>\]])", temp_seg)
|
| 197 |
+
for part in parts:
|
| 198 |
+
if not part:
|
| 199 |
+
continue
|
| 200 |
+
if part.startswith("<") and part.endswith(">"):
|
| 201 |
+
result.append((part, "pinyin"))
|
| 202 |
+
elif part.startswith("[") and part.endswith("]"):
|
| 203 |
+
result.append((part, "tag"))
|
| 204 |
+
else:
|
| 205 |
+
result.append((part, temp_lang))
|
| 206 |
+
return result
|
| 207 |
+
|
| 208 |
+
@staticmethod
|
| 209 |
+
def is_chinese(char: str) -> bool:
|
| 210 |
+
return "\u4e00" <= char <= "\u9fa5"
|
| 211 |
+
|
| 212 |
+
@staticmethod
|
| 213 |
+
def is_alphabet(char: str) -> bool:
|
| 214 |
+
return ("A" <= char <= "Z") or ("a" <= char <= "z")
|
| 215 |
+
|
| 216 |
+
@staticmethod
|
| 217 |
+
def is_pinyin(part: str) -> bool:
|
| 218 |
+
return part.startswith("<") and part.endswith(">")
|
scripts/text_processing.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import re
|
| 5 |
+
import unicodedata
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
SENTENCE_PUNCT = "。!?.!?。."
|
| 12 |
+
ELLIPSIS_PUNCT = {"…"}
|
| 13 |
+
QUOTE_PUNCT = set("\"'“”‘’「」『』")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def is_text_punctuation(ch: str) -> bool:
|
| 17 |
+
return ch in ELLIPSIS_PUNCT or unicodedata.category(ch).startswith("P")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def comma_for_context(left: str, right: str) -> str:
|
| 21 |
+
context = left + right
|
| 22 |
+
return "," if re.search(r"[A-Za-z0-9]", context) else ","
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def setup_jieba_cache(repo_dir: Path) -> None:
|
| 26 |
+
import jieba
|
| 27 |
+
|
| 28 |
+
cache_dir = repo_dir / ".work_tmp" / "jieba"
|
| 29 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 30 |
+
jieba.dt.tmp_dir = str(cache_dir)
|
| 31 |
+
jieba.dt.cache_file = "jieba.cache"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def load_text(args: argparse.Namespace) -> str:
|
| 35 |
+
if args.text_file:
|
| 36 |
+
text = Path(args.text_file).read_text(encoding="utf-8")
|
| 37 |
+
elif args.text:
|
| 38 |
+
text = args.text
|
| 39 |
+
else:
|
| 40 |
+
raise ValueError("Either --text or --text-file is required")
|
| 41 |
+
text = re.sub(r"[ \t\r\f\v]+", " ", text)
|
| 42 |
+
text = re.sub(r"\n+", " ", text)
|
| 43 |
+
return text.strip()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def normalize_punctuation(text: str) -> str:
|
| 47 |
+
text = re.sub(r"\s+", " ", text.strip())
|
| 48 |
+
text = text.replace("……", "。").replace("...", ".")
|
| 49 |
+
chars: list[str] = []
|
| 50 |
+
i = 0
|
| 51 |
+
while i < len(text):
|
| 52 |
+
ch = text[i]
|
| 53 |
+
if is_text_punctuation(ch):
|
| 54 |
+
j = i
|
| 55 |
+
punct_run = []
|
| 56 |
+
while j < len(text) and is_text_punctuation(text[j]):
|
| 57 |
+
punct_run.append(text[j])
|
| 58 |
+
j += 1
|
| 59 |
+
if all(p in QUOTE_PUNCT for p in punct_run):
|
| 60 |
+
i = j
|
| 61 |
+
continue
|
| 62 |
+
sentence_marks = [p for p in punct_run if p in SENTENCE_PUNCT]
|
| 63 |
+
left = chars[-1] if chars else ""
|
| 64 |
+
right = text[j] if j < len(text) else ""
|
| 65 |
+
mark = sentence_marks[-1] if sentence_marks else comma_for_context(left, right)
|
| 66 |
+
if chars and is_text_punctuation(chars[-1]):
|
| 67 |
+
chars[-1] = mark
|
| 68 |
+
else:
|
| 69 |
+
chars.append(mark)
|
| 70 |
+
i = j
|
| 71 |
+
continue
|
| 72 |
+
chars.append(ch)
|
| 73 |
+
i += 1
|
| 74 |
+
|
| 75 |
+
normalized = "".join(chars)
|
| 76 |
+
normalized = re.sub(r"[ \t]+([,。!?,.!?.。])", r"\1", normalized)
|
| 77 |
+
normalized = re.sub(r"([,。!?])\s+", r"\1", normalized)
|
| 78 |
+
normalized = re.sub(r"([,.!?.。])(?=[A-Za-z0-9])", r"\1 ", normalized)
|
| 79 |
+
normalized = re.sub(r"\s{2,}", " ", normalized)
|
| 80 |
+
normalized = re.sub(r"([,,])\s*[,,]+", r"\1", normalized)
|
| 81 |
+
normalized = re.sub(r"[,,]+([。!?.!?.。])", r"\1", normalized)
|
| 82 |
+
normalized = re.sub(r"([。!?.!?.。])([,,]+)", r"\1", normalized)
|
| 83 |
+
return normalized.strip()
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def join_units(left: str, right: str) -> str:
|
| 87 |
+
if not left:
|
| 88 |
+
return right.strip()
|
| 89 |
+
right = right.strip()
|
| 90 |
+
if not right:
|
| 91 |
+
return left.strip()
|
| 92 |
+
if re.search(r"[\u4e00-\u9fff]$", left) or re.match(r"^[\u4e00-\u9fff]", right):
|
| 93 |
+
return left.rstrip() + right
|
| 94 |
+
return left.rstrip() + " " + right
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def split_units(text: str) -> list[str]:
|
| 98 |
+
text = text.strip()
|
| 99 |
+
if not text:
|
| 100 |
+
return []
|
| 101 |
+
pattern = r"[^。!?!?;;,,、::\n]+[。!?!?;;,,、::]?"
|
| 102 |
+
units = [m.group(0).strip() for m in re.finditer(pattern, text) if m.group(0).strip()]
|
| 103 |
+
return units or [text]
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def estimate_lengths(
|
| 107 |
+
prompt_frames: int,
|
| 108 |
+
prompt_tokens: int,
|
| 109 |
+
text_tokens: int,
|
| 110 |
+
speed: float,
|
| 111 |
+
max_feat_len: int,
|
| 112 |
+
) -> dict[str, int | bool]:
|
| 113 |
+
raw_features_len = int(
|
| 114 |
+
np.ceil(prompt_frames / prompt_tokens * (prompt_tokens + text_tokens) / speed)
|
| 115 |
+
)
|
| 116 |
+
features_len = min(raw_features_len, max_feat_len)
|
| 117 |
+
generated_frames = features_len - prompt_frames
|
| 118 |
+
if generated_frames <= 0:
|
| 119 |
+
generated_frames = features_len
|
| 120 |
+
return {
|
| 121 |
+
"raw_features_len": raw_features_len,
|
| 122 |
+
"features_len": features_len,
|
| 123 |
+
"generated_frames": generated_frames,
|
| 124 |
+
"clamped": raw_features_len > max_feat_len,
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def token_count(tokenizer, text: str) -> int:
|
| 129 |
+
return len(tokenizer.texts_to_token_ids([text])[0])
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def split_long_unit(tokenizer, unit: str, max_text_tokens: int) -> list[str]:
|
| 133 |
+
if token_count(tokenizer, unit) <= max_text_tokens:
|
| 134 |
+
return [unit]
|
| 135 |
+
|
| 136 |
+
if " " in unit:
|
| 137 |
+
pieces = unit.split()
|
| 138 |
+
chunks: list[str] = []
|
| 139 |
+
current = ""
|
| 140 |
+
for piece in pieces:
|
| 141 |
+
candidate = join_units(current, piece)
|
| 142 |
+
if current and token_count(tokenizer, candidate) > max_text_tokens:
|
| 143 |
+
chunks.append(current)
|
| 144 |
+
current = piece
|
| 145 |
+
else:
|
| 146 |
+
current = candidate
|
| 147 |
+
if current:
|
| 148 |
+
chunks.append(current)
|
| 149 |
+
return chunks
|
| 150 |
+
|
| 151 |
+
chunks = []
|
| 152 |
+
current = ""
|
| 153 |
+
for char in unit:
|
| 154 |
+
candidate = current + char
|
| 155 |
+
if current and token_count(tokenizer, candidate) > max_text_tokens:
|
| 156 |
+
chunks.append(current)
|
| 157 |
+
current = char
|
| 158 |
+
else:
|
| 159 |
+
current = candidate
|
| 160 |
+
if current:
|
| 161 |
+
chunks.append(current)
|
| 162 |
+
return chunks
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def build_segments(
|
| 166 |
+
tokenizer,
|
| 167 |
+
text: str,
|
| 168 |
+
prompt_frames: int,
|
| 169 |
+
prompt_tokens_len: int,
|
| 170 |
+
speed: float,
|
| 171 |
+
max_feat_len: int,
|
| 172 |
+
max_text_tokens: int,
|
| 173 |
+
min_generated_frames: int,
|
| 174 |
+
max_generated_frames: int,
|
| 175 |
+
max_raw_feat_ratio: float,
|
| 176 |
+
) -> list[dict[str, object]]:
|
| 177 |
+
raw_units = split_units(text)
|
| 178 |
+
units: list[str] = []
|
| 179 |
+
for unit in raw_units:
|
| 180 |
+
units.extend(split_long_unit(tokenizer, unit, max_text_tokens))
|
| 181 |
+
|
| 182 |
+
segments: list[dict[str, object]] = []
|
| 183 |
+
current = ""
|
| 184 |
+
for unit in units:
|
| 185 |
+
candidate = join_units(current, unit)
|
| 186 |
+
cand_tokens = token_count(tokenizer, candidate)
|
| 187 |
+
cand_est = estimate_lengths(
|
| 188 |
+
prompt_frames,
|
| 189 |
+
prompt_tokens_len,
|
| 190 |
+
cand_tokens,
|
| 191 |
+
speed,
|
| 192 |
+
max_feat_len,
|
| 193 |
+
)
|
| 194 |
+
raw_too_long = int(cand_est["raw_features_len"]) > int(max_feat_len * max_raw_feat_ratio)
|
| 195 |
+
too_long = (
|
| 196 |
+
cand_tokens > max_text_tokens
|
| 197 |
+
or int(cand_est["generated_frames"]) > max_generated_frames
|
| 198 |
+
or raw_too_long
|
| 199 |
+
)
|
| 200 |
+
if current and too_long:
|
| 201 |
+
cur_tokens = token_count(tokenizer, current)
|
| 202 |
+
cur_est = estimate_lengths(
|
| 203 |
+
prompt_frames,
|
| 204 |
+
prompt_tokens_len,
|
| 205 |
+
cur_tokens,
|
| 206 |
+
speed,
|
| 207 |
+
max_feat_len,
|
| 208 |
+
)
|
| 209 |
+
segments.append({"text": current, "text_tokens": cur_tokens, **cur_est})
|
| 210 |
+
current = unit
|
| 211 |
+
else:
|
| 212 |
+
current = candidate
|
| 213 |
+
|
| 214 |
+
if current:
|
| 215 |
+
cur_tokens = token_count(tokenizer, current)
|
| 216 |
+
cur_est = estimate_lengths(
|
| 217 |
+
prompt_frames,
|
| 218 |
+
prompt_tokens_len,
|
| 219 |
+
cur_tokens,
|
| 220 |
+
speed,
|
| 221 |
+
max_feat_len,
|
| 222 |
+
)
|
| 223 |
+
segments.append({"text": current, "text_tokens": cur_tokens, **cur_est})
|
| 224 |
+
|
| 225 |
+
if len(segments) >= 2 and int(segments[-1]["generated_frames"]) < min_generated_frames:
|
| 226 |
+
merged_text = join_units(str(segments[-2]["text"]), str(segments[-1]["text"]))
|
| 227 |
+
merged_tokens = token_count(tokenizer, merged_text)
|
| 228 |
+
merged_est = estimate_lengths(
|
| 229 |
+
prompt_frames,
|
| 230 |
+
prompt_tokens_len,
|
| 231 |
+
merged_tokens,
|
| 232 |
+
speed,
|
| 233 |
+
max_feat_len,
|
| 234 |
+
)
|
| 235 |
+
raw_ok = int(merged_est["raw_features_len"]) <= int(max_feat_len * max_raw_feat_ratio)
|
| 236 |
+
if (
|
| 237 |
+
merged_tokens <= max_text_tokens
|
| 238 |
+
and int(merged_est["generated_frames"]) <= max_generated_frames
|
| 239 |
+
and raw_ok
|
| 240 |
+
):
|
| 241 |
+
segments[-2] = {"text": merged_text, "text_tokens": merged_tokens, **merged_est}
|
| 242 |
+
segments.pop()
|
| 243 |
+
else:
|
| 244 |
+
rebalanced = rebalance_short_tail(
|
| 245 |
+
tokenizer,
|
| 246 |
+
str(segments[-2]["text"]),
|
| 247 |
+
str(segments[-1]["text"]),
|
| 248 |
+
prompt_frames,
|
| 249 |
+
prompt_tokens_len,
|
| 250 |
+
speed,
|
| 251 |
+
max_feat_len,
|
| 252 |
+
max_text_tokens,
|
| 253 |
+
min_generated_frames,
|
| 254 |
+
max_generated_frames,
|
| 255 |
+
max_raw_feat_ratio,
|
| 256 |
+
)
|
| 257 |
+
if rebalanced is not None:
|
| 258 |
+
segments[-2], segments[-1] = rebalanced
|
| 259 |
+
|
| 260 |
+
return segments
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def split_rebalance_pieces(text: str) -> list[str]:
|
| 264 |
+
units = split_units(text)
|
| 265 |
+
if len(units) > 1:
|
| 266 |
+
return units
|
| 267 |
+
if " " in text:
|
| 268 |
+
return text.split()
|
| 269 |
+
return list(text)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def segment_record(
|
| 273 |
+
tokenizer,
|
| 274 |
+
text: str,
|
| 275 |
+
prompt_frames: int,
|
| 276 |
+
prompt_tokens_len: int,
|
| 277 |
+
speed: float,
|
| 278 |
+
max_feat_len: int,
|
| 279 |
+
) -> dict[str, object]:
|
| 280 |
+
text_tokens = token_count(tokenizer, text)
|
| 281 |
+
est = estimate_lengths(
|
| 282 |
+
prompt_frames,
|
| 283 |
+
prompt_tokens_len,
|
| 284 |
+
text_tokens,
|
| 285 |
+
speed,
|
| 286 |
+
max_feat_len,
|
| 287 |
+
)
|
| 288 |
+
return {"text": text, "text_tokens": text_tokens, **est}
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def segment_within_limits(
|
| 292 |
+
record: dict[str, object],
|
| 293 |
+
max_text_tokens: int,
|
| 294 |
+
max_generated_frames: int,
|
| 295 |
+
max_raw_feat: int,
|
| 296 |
+
) -> bool:
|
| 297 |
+
return (
|
| 298 |
+
int(record["text_tokens"]) <= max_text_tokens
|
| 299 |
+
and int(record["generated_frames"]) <= max_generated_frames
|
| 300 |
+
and int(record["raw_features_len"]) <= max_raw_feat
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def rebalance_short_tail(
|
| 305 |
+
tokenizer,
|
| 306 |
+
prev_text: str,
|
| 307 |
+
tail_text: str,
|
| 308 |
+
prompt_frames: int,
|
| 309 |
+
prompt_tokens_len: int,
|
| 310 |
+
speed: float,
|
| 311 |
+
max_feat_len: int,
|
| 312 |
+
max_text_tokens: int,
|
| 313 |
+
min_generated_frames: int,
|
| 314 |
+
max_generated_frames: int,
|
| 315 |
+
max_raw_feat_ratio: float,
|
| 316 |
+
) -> tuple[dict[str, object], dict[str, object]] | None:
|
| 317 |
+
max_raw_feat = int(max_feat_len * max_raw_feat_ratio)
|
| 318 |
+
best: tuple[dict[str, object], dict[str, object]] | None = None
|
| 319 |
+
best_tail_frames = -1
|
| 320 |
+
|
| 321 |
+
piece_sets = [split_rebalance_pieces(prev_text)]
|
| 322 |
+
if " " in prev_text:
|
| 323 |
+
word_pieces = prev_text.split()
|
| 324 |
+
if len(word_pieces) > 1 and word_pieces != piece_sets[0]:
|
| 325 |
+
piece_sets.append(word_pieces)
|
| 326 |
+
|
| 327 |
+
for pieces in piece_sets:
|
| 328 |
+
if len(pieces) < 2:
|
| 329 |
+
continue
|
| 330 |
+
for split_at in range(len(pieces) - 1, 0, -1):
|
| 331 |
+
new_prev = (
|
| 332 |
+
join_units("", " ".join(pieces[:split_at]))
|
| 333 |
+
if " " in prev_text
|
| 334 |
+
else "".join(pieces[:split_at])
|
| 335 |
+
)
|
| 336 |
+
moved_tail = (
|
| 337 |
+
join_units("", " ".join(pieces[split_at:]))
|
| 338 |
+
if " " in prev_text
|
| 339 |
+
else "".join(pieces[split_at:])
|
| 340 |
+
)
|
| 341 |
+
new_tail = join_units(moved_tail, tail_text)
|
| 342 |
+
prev_record = segment_record(
|
| 343 |
+
tokenizer,
|
| 344 |
+
new_prev,
|
| 345 |
+
prompt_frames,
|
| 346 |
+
prompt_tokens_len,
|
| 347 |
+
speed,
|
| 348 |
+
max_feat_len,
|
| 349 |
+
)
|
| 350 |
+
tail_record = segment_record(
|
| 351 |
+
tokenizer,
|
| 352 |
+
new_tail,
|
| 353 |
+
prompt_frames,
|
| 354 |
+
prompt_tokens_len,
|
| 355 |
+
speed,
|
| 356 |
+
max_feat_len,
|
| 357 |
+
)
|
| 358 |
+
if not segment_within_limits(
|
| 359 |
+
tail_record,
|
| 360 |
+
max_text_tokens,
|
| 361 |
+
max_generated_frames,
|
| 362 |
+
max_raw_feat,
|
| 363 |
+
):
|
| 364 |
+
continue
|
| 365 |
+
if not segment_within_limits(
|
| 366 |
+
prev_record,
|
| 367 |
+
max_text_tokens,
|
| 368 |
+
max_generated_frames,
|
| 369 |
+
max_raw_feat,
|
| 370 |
+
):
|
| 371 |
+
continue
|
| 372 |
+
prev_frames = int(prev_record["generated_frames"])
|
| 373 |
+
tail_frames = int(tail_record["generated_frames"])
|
| 374 |
+
if prev_frames < min_generated_frames:
|
| 375 |
+
continue
|
| 376 |
+
if tail_frames > best_tail_frames:
|
| 377 |
+
best = (prev_record, tail_record)
|
| 378 |
+
best_tail_frames = tail_frames
|
| 379 |
+
if tail_frames >= min_generated_frames:
|
| 380 |
+
return best
|
| 381 |
+
|
| 382 |
+
return best
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def build_cat_tokens(tokenizer, prompt_tokens: list[int], text_tokens: list[int], max_tokens: int) -> np.ndarray:
|
| 386 |
+
pad_id = tokenizer.pad_id
|
| 387 |
+
cat = prompt_tokens + text_tokens + [pad_id]
|
| 388 |
+
cat_tokens = np.full((1, max_tokens), pad_id, dtype=np.int64)
|
| 389 |
+
cat_tokens[0, : len(cat)] = np.array(cat, dtype=np.int64)
|
| 390 |
+
return cat_tokens
|
scripts/zipvoice_decoder4_runtime.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import time
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Dict, Iterable, List
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
from scripts.zipvoice_runtime import AxeSession
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Decoder4ZipVoiceBoardRuntime:
|
| 17 |
+
"""Runs encoder_core_nolog.axmodel and fm_decoder_part0..part3.axmodel."""
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
config_dir: str | Path,
|
| 22 |
+
models_dir: str | Path,
|
| 23 |
+
max_feat_len: int = 1024,
|
| 24 |
+
max_tokens: int = 384,
|
| 25 |
+
num_step: int = 16,
|
| 26 |
+
t_shift: float = 0.5,
|
| 27 |
+
) -> None:
|
| 28 |
+
self.config_dir = Path(config_dir)
|
| 29 |
+
self.models_dir = Path(models_dir)
|
| 30 |
+
self.max_feat_len = int(max_feat_len)
|
| 31 |
+
self.max_tokens = int(max_tokens)
|
| 32 |
+
self.num_step = int(num_step)
|
| 33 |
+
self.t_shift = float(t_shift)
|
| 34 |
+
|
| 35 |
+
self._load_config()
|
| 36 |
+
self._load_manifest()
|
| 37 |
+
self.sessions: Dict[str, AxeSession] = {}
|
| 38 |
+
self._load_models()
|
| 39 |
+
self._load_decoder_input_metadata()
|
| 40 |
+
|
| 41 |
+
def _load_config(self) -> None:
|
| 42 |
+
config_path = self.config_dir / "runtime_config.json"
|
| 43 |
+
config = json.loads(config_path.read_text()) if config_path.exists() else {}
|
| 44 |
+
self.feat_dim = int(config.get("feat_dim", 100))
|
| 45 |
+
self.sampling_rate = int(config.get("sampling_rate", 24000))
|
| 46 |
+
self.hop_length = int(config.get("hop_length", 256))
|
| 47 |
+
logging.debug(
|
| 48 |
+
"Decoder4 runtime: max_tokens=%d, max_feat_len=%d, feat_dim=%d, num_step=%d",
|
| 49 |
+
self.max_tokens,
|
| 50 |
+
self.max_feat_len,
|
| 51 |
+
self.feat_dim,
|
| 52 |
+
self.num_step,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def _load_manifest(self) -> None:
|
| 56 |
+
manifest_path = self.config_dir / "decoder4_split_manifest.json"
|
| 57 |
+
if not manifest_path.exists():
|
| 58 |
+
raise FileNotFoundError(f"decoder4_split_manifest.json not found: {manifest_path}")
|
| 59 |
+
self.manifest = json.loads(manifest_path.read_text())
|
| 60 |
+
self.encoder_info = self.manifest["encoder"]
|
| 61 |
+
self.decoder_parts = self.manifest["decoder_parts"]
|
| 62 |
+
|
| 63 |
+
def _load_models(self) -> None:
|
| 64 |
+
model_infos = [self.encoder_info, *self.decoder_parts]
|
| 65 |
+
for info in model_infos:
|
| 66 |
+
name = info["name"]
|
| 67 |
+
path = self.models_dir / info["file"]
|
| 68 |
+
logging.debug("Loading %s from %s", name, path)
|
| 69 |
+
self.sessions[name] = AxeSession(path)
|
| 70 |
+
logging.debug("Loaded encoder + %d decoder4 parts", len(self.decoder_parts))
|
| 71 |
+
|
| 72 |
+
def _load_decoder_input_metadata(self) -> None:
|
| 73 |
+
part0 = self.decoder_parts[0]
|
| 74 |
+
sess = self.sessions[part0["name"]]
|
| 75 |
+
input_names = sess.input_names
|
| 76 |
+
self.decoder_has_padding_mask = "padding_mask" in input_names
|
| 77 |
+
self.decoder_seq_len = self.max_feat_len
|
| 78 |
+
if "x" in input_names:
|
| 79 |
+
index = input_names.index("x")
|
| 80 |
+
input_info = sess._inputs[index] if index < len(sess._inputs) else None
|
| 81 |
+
shape = getattr(input_info, "shape", None) if input_info is not None else None
|
| 82 |
+
if shape is not None and len(shape) >= 2 and isinstance(shape[1], (int, np.integer)):
|
| 83 |
+
self.decoder_seq_len = int(shape[1])
|
| 84 |
+
|
| 85 |
+
if self.decoder_seq_len != self.max_feat_len:
|
| 86 |
+
logging.debug(
|
| 87 |
+
"decoder x seq_len=%d differs from configured max_feat_len=%d; "
|
| 88 |
+
"using model seq_len for decoder feeds",
|
| 89 |
+
self.decoder_seq_len,
|
| 90 |
+
self.max_feat_len,
|
| 91 |
+
)
|
| 92 |
+
logging.debug(
|
| 93 |
+
"Decoder4 model metadata: seq_len=%d, has_padding_mask=%s",
|
| 94 |
+
self.decoder_seq_len,
|
| 95 |
+
self.decoder_has_padding_mask,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
@staticmethod
|
| 99 |
+
def _coerce_input_dtype(value: np.ndarray, input_info: Any | None) -> np.ndarray:
|
| 100 |
+
if input_info is None:
|
| 101 |
+
return value
|
| 102 |
+
|
| 103 |
+
expected_dtype = getattr(input_info, "dtype", None)
|
| 104 |
+
if expected_dtype is None:
|
| 105 |
+
expected_dtype = getattr(input_info, "type", None)
|
| 106 |
+
if expected_dtype is None:
|
| 107 |
+
return value
|
| 108 |
+
|
| 109 |
+
dtype_text = str(expected_dtype).lower()
|
| 110 |
+
if "float32" in dtype_text:
|
| 111 |
+
return np.ascontiguousarray(value, dtype=np.float32)
|
| 112 |
+
if "int32" in dtype_text:
|
| 113 |
+
return np.ascontiguousarray(value, dtype=np.int32)
|
| 114 |
+
if "int64" in dtype_text:
|
| 115 |
+
return np.ascontiguousarray(value, dtype=np.int64)
|
| 116 |
+
if "uint8" in dtype_text:
|
| 117 |
+
return np.ascontiguousarray(value, dtype=np.uint8)
|
| 118 |
+
if "bool" in dtype_text:
|
| 119 |
+
return np.ascontiguousarray(value, dtype=np.bool_)
|
| 120 |
+
return np.ascontiguousarray(value)
|
| 121 |
+
|
| 122 |
+
def _run_model(
|
| 123 |
+
self,
|
| 124 |
+
name: str,
|
| 125 |
+
expected_inputs: Iterable[str],
|
| 126 |
+
expected_outputs: Iterable[str],
|
| 127 |
+
values: Dict[str, np.ndarray],
|
| 128 |
+
) -> Dict[str, np.ndarray]:
|
| 129 |
+
sess = self.sessions[name]
|
| 130 |
+
expected_inputs = list(expected_inputs)
|
| 131 |
+
expected_outputs = list(expected_outputs)
|
| 132 |
+
|
| 133 |
+
feed: Dict[str, np.ndarray] = {}
|
| 134 |
+
for index, actual_name in enumerate(sess.input_names):
|
| 135 |
+
input_info = sess._inputs[index] if index < len(sess._inputs) else None
|
| 136 |
+
if actual_name in values:
|
| 137 |
+
feed[actual_name] = self._coerce_input_dtype(values[actual_name], input_info)
|
| 138 |
+
continue
|
| 139 |
+
if index < len(expected_inputs) and expected_inputs[index] in values:
|
| 140 |
+
feed[actual_name] = self._coerce_input_dtype(
|
| 141 |
+
values[expected_inputs[index]], input_info
|
| 142 |
+
)
|
| 143 |
+
continue
|
| 144 |
+
expected = expected_inputs[index] if index < len(expected_inputs) else None
|
| 145 |
+
raise KeyError(
|
| 146 |
+
f"Missing input for {name}: actual={actual_name!r}, expected={expected!r}"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
raw_outputs = sess.run(feed)
|
| 150 |
+
mapped: Dict[str, np.ndarray] = {}
|
| 151 |
+
for index, expected_name in enumerate(expected_outputs):
|
| 152 |
+
if expected_name in raw_outputs:
|
| 153 |
+
mapped[expected_name] = raw_outputs[expected_name]
|
| 154 |
+
continue
|
| 155 |
+
if index < len(sess.output_names) and sess.output_names[index] in raw_outputs:
|
| 156 |
+
mapped[expected_name] = raw_outputs[sess.output_names[index]]
|
| 157 |
+
continue
|
| 158 |
+
raise KeyError(f"Missing output for {name}: {expected_name!r}")
|
| 159 |
+
return mapped
|
| 160 |
+
|
| 161 |
+
def run_encoder(self, cat_tokens: np.ndarray) -> np.ndarray:
|
| 162 |
+
# Pulsar2/AXEngine exposes the quantized encoder token input as int32
|
| 163 |
+
# even though the reference ONNX path uses int64 token IDs.
|
| 164 |
+
cat_tokens = np.asarray(cat_tokens, dtype=np.int32)
|
| 165 |
+
outputs = self._run_model(
|
| 166 |
+
self.encoder_info["name"],
|
| 167 |
+
self.encoder_info["inputs"],
|
| 168 |
+
self.encoder_info["outputs"],
|
| 169 |
+
{"cat_tokens": cat_tokens},
|
| 170 |
+
)
|
| 171 |
+
return outputs[self.encoder_info["outputs"][0]].astype(np.float32)
|
| 172 |
+
|
| 173 |
+
def run_decoder(
|
| 174 |
+
self,
|
| 175 |
+
t: np.ndarray,
|
| 176 |
+
x: np.ndarray,
|
| 177 |
+
text_condition: np.ndarray,
|
| 178 |
+
speech_condition: np.ndarray,
|
| 179 |
+
guidance_scale: np.ndarray,
|
| 180 |
+
padding_mask: np.ndarray | None = None,
|
| 181 |
+
) -> np.ndarray:
|
| 182 |
+
seq_len = x.shape[1]
|
| 183 |
+
values: Dict[str, np.ndarray] = {
|
| 184 |
+
"t": np.asarray(t, dtype=np.float32).reshape(1),
|
| 185 |
+
"x": x.astype(np.float32),
|
| 186 |
+
"text_condition": text_condition.astype(np.float32),
|
| 187 |
+
"speech_condition": speech_condition.astype(np.float32),
|
| 188 |
+
"guidance_scale": np.asarray(guidance_scale, dtype=np.float32).reshape(1),
|
| 189 |
+
"padding_mask": padding_mask.astype(np.bool_)
|
| 190 |
+
if padding_mask is not None
|
| 191 |
+
else np.zeros((1, seq_len), dtype=np.bool_),
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
for part in self.decoder_parts:
|
| 195 |
+
outputs = self._run_model(
|
| 196 |
+
part["name"],
|
| 197 |
+
part["inputs"],
|
| 198 |
+
part["outputs"],
|
| 199 |
+
values,
|
| 200 |
+
)
|
| 201 |
+
values.update(outputs)
|
| 202 |
+
|
| 203 |
+
final_output = self.decoder_parts[-1]["outputs"][0]
|
| 204 |
+
return values[final_output].astype(np.float32)
|
| 205 |
+
|
| 206 |
+
def duration_expand(
|
| 207 |
+
self,
|
| 208 |
+
encoded: np.ndarray,
|
| 209 |
+
prompt_tokens_len: int,
|
| 210 |
+
text_tokens_len: int,
|
| 211 |
+
prompt_features_len: int,
|
| 212 |
+
speed: float,
|
| 213 |
+
) -> tuple[np.ndarray, int]:
|
| 214 |
+
total_tokens_len = prompt_tokens_len + text_tokens_len
|
| 215 |
+
features_len = int(
|
| 216 |
+
np.ceil(prompt_features_len / prompt_tokens_len * total_tokens_len / speed)
|
| 217 |
+
)
|
| 218 |
+
if features_len > self.max_feat_len:
|
| 219 |
+
logging.debug(
|
| 220 |
+
"features_len=%d > max_feat_len=%d, clamping",
|
| 221 |
+
features_len,
|
| 222 |
+
self.max_feat_len,
|
| 223 |
+
)
|
| 224 |
+
features_len = self.max_feat_len
|
| 225 |
+
|
| 226 |
+
token_dur = features_len // total_tokens_len
|
| 227 |
+
embed_no_pad = encoded[0, :total_tokens_len, :]
|
| 228 |
+
text_condition = np.repeat(embed_no_pad, token_dur, axis=0)
|
| 229 |
+
|
| 230 |
+
residual = features_len - text_condition.shape[0]
|
| 231 |
+
if residual > 0:
|
| 232 |
+
last_embed = encoded[0, total_tokens_len : total_tokens_len + 1, :]
|
| 233 |
+
text_condition = np.concatenate(
|
| 234 |
+
[text_condition, np.repeat(last_embed, residual, axis=0)],
|
| 235 |
+
axis=0,
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
text_condition = text_condition[:features_len, :]
|
| 239 |
+
return text_condition[np.newaxis, :, :].astype(np.float32), features_len
|
| 240 |
+
|
| 241 |
+
def _get_time_steps(self) -> np.ndarray:
|
| 242 |
+
t = np.linspace(0.0, 1.0, self.num_step + 1, dtype=np.float32)
|
| 243 |
+
ts = self.t_shift
|
| 244 |
+
return ts * t / (1.0 + (ts - 1.0) * t)
|
| 245 |
+
|
| 246 |
+
def sample(
|
| 247 |
+
self,
|
| 248 |
+
cat_tokens: np.ndarray,
|
| 249 |
+
prompt_tokens_len: int,
|
| 250 |
+
text_tokens_len: int,
|
| 251 |
+
prompt_features: np.ndarray,
|
| 252 |
+
prompt_features_len: int,
|
| 253 |
+
speed: float = 1.0,
|
| 254 |
+
guidance_scale: float = 1.0,
|
| 255 |
+
seed: int = 666,
|
| 256 |
+
) -> tuple[np.ndarray, Dict[str, Any]]:
|
| 257 |
+
logging.debug(
|
| 258 |
+
"sample: prompt_tokens=%d, text_tokens=%d, prompt_frames=%d, "
|
| 259 |
+
"speed=%.2f, guidance_scale=%.2f, seed=%d",
|
| 260 |
+
prompt_tokens_len,
|
| 261 |
+
text_tokens_len,
|
| 262 |
+
prompt_features_len,
|
| 263 |
+
speed,
|
| 264 |
+
guidance_scale,
|
| 265 |
+
seed,
|
| 266 |
+
)
|
| 267 |
+
t_total_start = time.perf_counter()
|
| 268 |
+
|
| 269 |
+
t_start = time.perf_counter()
|
| 270 |
+
encoded = self.run_encoder(cat_tokens)
|
| 271 |
+
t_enc = time.perf_counter() - t_start
|
| 272 |
+
logging.debug(" encoder: %.3f s (output shape=%s)", t_enc, encoded.shape)
|
| 273 |
+
|
| 274 |
+
t_start = time.perf_counter()
|
| 275 |
+
text_condition, features_len = self.duration_expand(
|
| 276 |
+
encoded,
|
| 277 |
+
prompt_tokens_len,
|
| 278 |
+
text_tokens_len,
|
| 279 |
+
prompt_features_len,
|
| 280 |
+
speed,
|
| 281 |
+
)
|
| 282 |
+
t_dur = time.perf_counter() - t_start
|
| 283 |
+
logging.debug(" duration_expand: %.3f s (features_len=%d)", t_dur, features_len)
|
| 284 |
+
|
| 285 |
+
seq_len = self.decoder_seq_len or self.max_feat_len
|
| 286 |
+
if features_len > seq_len:
|
| 287 |
+
raise ValueError(
|
| 288 |
+
f"features_len={features_len} exceeds decoder sequence length {seq_len}"
|
| 289 |
+
)
|
| 290 |
+
if (
|
| 291 |
+
self.decoder_seq_len is not None
|
| 292 |
+
and not self.decoder_has_padding_mask
|
| 293 |
+
and features_len != seq_len
|
| 294 |
+
):
|
| 295 |
+
raise ValueError(
|
| 296 |
+
"Fixed no-mask decoder requires exact feature length: "
|
| 297 |
+
f"features_len={features_len}, decoder_seq_len={seq_len}"
|
| 298 |
+
)
|
| 299 |
+
if prompt_features.shape[1] > seq_len:
|
| 300 |
+
raise ValueError(
|
| 301 |
+
f"prompt feature length {prompt_features.shape[1]} exceeds "
|
| 302 |
+
f"decoder sequence length {seq_len}"
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
text_cond_padded = np.zeros((1, seq_len, self.feat_dim), dtype=np.float32)
|
| 306 |
+
text_cond_padded[0, :features_len] = text_condition[0, :features_len]
|
| 307 |
+
|
| 308 |
+
speech_cond_padded = np.zeros((1, seq_len, self.feat_dim), dtype=np.float32)
|
| 309 |
+
prompt_actual_len = prompt_features.shape[1]
|
| 310 |
+
speech_cond_padded[0, :prompt_actual_len] = prompt_features[0].astype(np.float32)
|
| 311 |
+
|
| 312 |
+
padding_mask = np.zeros((1, seq_len), dtype=np.bool_)
|
| 313 |
+
padding_mask[:, features_len:] = True
|
| 314 |
+
|
| 315 |
+
rng = np.random.RandomState(seed)
|
| 316 |
+
x = rng.randn(1, seq_len, self.feat_dim).astype(np.float32)
|
| 317 |
+
x[:, features_len:, :] = 0.0
|
| 318 |
+
|
| 319 |
+
timesteps = self._get_time_steps()
|
| 320 |
+
gs = np.array([guidance_scale], dtype=np.float32)
|
| 321 |
+
|
| 322 |
+
t_dec_total = 0.0
|
| 323 |
+
for step in range(self.num_step):
|
| 324 |
+
t_val = np.array([float(timesteps[step])], dtype=np.float32)
|
| 325 |
+
t_start = time.perf_counter()
|
| 326 |
+
v = self.run_decoder(
|
| 327 |
+
t_val,
|
| 328 |
+
x,
|
| 329 |
+
text_cond_padded,
|
| 330 |
+
speech_cond_padded,
|
| 331 |
+
gs,
|
| 332 |
+
padding_mask,
|
| 333 |
+
)
|
| 334 |
+
t_dec_total += time.perf_counter() - t_start
|
| 335 |
+
dt = float(timesteps[step + 1] - timesteps[step])
|
| 336 |
+
x = (x + v * dt).astype(np.float32)
|
| 337 |
+
x[:, features_len:, :] = 0.0
|
| 338 |
+
|
| 339 |
+
logging.debug(
|
| 340 |
+
" %s (NPU x%d): %.3f s total (avg %.3f ms/step)",
|
| 341 |
+
getattr(self, "decoder_label", "decoder4"),
|
| 342 |
+
self.num_step,
|
| 343 |
+
t_dec_total,
|
| 344 |
+
t_dec_total / self.num_step * 1000,
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
generated_frames = features_len - prompt_features_len
|
| 348 |
+
if generated_frames <= 0:
|
| 349 |
+
generated_frames = features_len
|
| 350 |
+
pred_features = x[0, :features_len, :]
|
| 351 |
+
else:
|
| 352 |
+
pred_features = x[0, prompt_features_len:features_len, :]
|
| 353 |
+
|
| 354 |
+
t_total = time.perf_counter() - t_total_start
|
| 355 |
+
timing = {
|
| 356 |
+
"encoder_time_sec": round(t_enc, 3),
|
| 357 |
+
"duration_expand_time_sec": round(t_dur, 3),
|
| 358 |
+
"decoder_time_sec": round(t_dec_total, 3),
|
| 359 |
+
"total_time_sec": round(t_total, 3),
|
| 360 |
+
"generated_frames": int(generated_frames),
|
| 361 |
+
"features_len": int(features_len),
|
| 362 |
+
}
|
| 363 |
+
logging.debug(" total: %.3f s", t_total)
|
| 364 |
+
return pred_features[np.newaxis, :, :].astype(np.float32), timing
|
scripts/zipvoice_runtime.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from contextlib import contextmanager
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@contextmanager
|
| 13 |
+
def _suppress_native_stdio():
|
| 14 |
+
if os.environ.get("ZIPVOICE_AXERA_VERBOSE"):
|
| 15 |
+
yield
|
| 16 |
+
return
|
| 17 |
+
|
| 18 |
+
stdout_fd = os.dup(1)
|
| 19 |
+
stderr_fd = os.dup(2)
|
| 20 |
+
try:
|
| 21 |
+
with open(os.devnull, "w") as devnull:
|
| 22 |
+
os.dup2(devnull.fileno(), 1)
|
| 23 |
+
os.dup2(devnull.fileno(), 2)
|
| 24 |
+
yield
|
| 25 |
+
finally:
|
| 26 |
+
os.dup2(stdout_fd, 1)
|
| 27 |
+
os.dup2(stderr_fd, 2)
|
| 28 |
+
os.close(stdout_fd)
|
| 29 |
+
os.close(stderr_fd)
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
with _suppress_native_stdio():
|
| 33 |
+
import axengine
|
| 34 |
+
HAS_AXENGINE = True
|
| 35 |
+
except ImportError:
|
| 36 |
+
HAS_AXENGINE = False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class AxeSession:
|
| 40 |
+
"""Thin wrapper around axengine.InferenceSession for one axmodel."""
|
| 41 |
+
|
| 42 |
+
def __init__(self, model_path: str | Path):
|
| 43 |
+
if not HAS_AXENGINE:
|
| 44 |
+
raise RuntimeError("axengine not available; run this script on the AX650 board")
|
| 45 |
+
self.path = Path(model_path)
|
| 46 |
+
if not self.path.exists():
|
| 47 |
+
raise FileNotFoundError(f"Model not found: {self.path}")
|
| 48 |
+
with _suppress_native_stdio():
|
| 49 |
+
self._session = axengine.InferenceSession(str(self.path))
|
| 50 |
+
self._inputs = self._session.get_inputs()
|
| 51 |
+
self._outputs = self._session.get_outputs()
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def input_names(self) -> List[str]:
|
| 55 |
+
return [item.name for item in self._inputs]
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def output_names(self) -> List[str]:
|
| 59 |
+
return [item.name for item in self._outputs]
|
| 60 |
+
|
| 61 |
+
def run(self, feed_dict: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
| 62 |
+
outputs = self._session.run(None, feed_dict)
|
| 63 |
+
if isinstance(outputs, dict):
|
| 64 |
+
return outputs
|
| 65 |
+
if isinstance(outputs, (list, tuple)):
|
| 66 |
+
return {name: value for name, value in zip(self.output_names, outputs)}
|
| 67 |
+
if len(self.output_names) == 1:
|
| 68 |
+
return {self.output_names[0]: outputs}
|
| 69 |
+
raise RuntimeError(f"Unexpected AXEngine output type from {self.path.name}: {type(outputs)}")
|