HY-2012 commited on
Commit
6dde92c
·
verified ·
1 Parent(s): 400cebf

Update inference demo

Browse files
Files changed (3) hide show
  1. README.md +5 -5
  2. demo_kokoro_ax.py +321 -0
  3. inference_utils.py +4 -1
README.md CHANGED
@@ -91,16 +91,16 @@ python -m spacy download en_core_web_sm
91
 
92
  ```bash
93
  中文:
94
- python kokoro_ax.py --text "致力于打造世界领先的人工智能感知与边缘计算芯片。" --lang zh --voice checkpoints/voices/zf_xiaoyi.pt --output output_zh.wav -d models -f 0.3
95
  英文:
96
- python kokoro_ax.py --text "The sky above the port was the color of television, tuned to a dead channel." --lang en --voice checkpoints/voices/af_heart.pt --output output_en.wav -d models -f 0.3
97
  日文:
98
- python kokoro_ax.py --text "「もしおれがただ偶然、そしてこうしようというつもりでなくここに立っているのなら、ちょっとばかり絶望するところだな」と、そんなことが彼の頭に思い浮かんだ。" --lang ja --voice checkpoints/voices/jm_kumo.pt --output output_jp.wav -d models -f 0.3
99
  ```
100
 
101
  注:AX630C平台推理,更改模型文件目录即可,将 -d models 改成 -d models_620E 如下:
102
  ```
103
- python kokoro_ax.py --text "The sky above the port was the color of television, tuned to a dead channel." --lang en --voice checkpoints/voices/af_heart.pt --output output_en.wav -d models_620E -f 0.3
104
  ```
105
 
106
  参数说明:
@@ -109,7 +109,7 @@ python kokoro_ax.py --text "The sky above the port was the color of television,
109
  | --axmodel_dir, -d | 模型文件目录(默认 models) |
110
  | --voice, -v | 声纹文件路径(必填) |
111
  | --text, -t | 合成文本(支持多语言) |
112
- | --lang, -l | 语言代码(如 en, zh, ja, ...),目前只测了中英日 |
113
  | --config, -c | 配置文件路径 |
114
  | --output, -o | 输出 wav 文件名 |
115
  | --fade_out, -f | 音频结尾淡出时长(秒),减少音频末尾有杂音 |
 
91
 
92
  ```bash
93
  中文:
94
+ python demo_kokoro_ax.py --text "致力于打造世界领先的人工智能感知与边缘计算芯片。" --lang z --voice checkpoints/voices/zf_xiaoyi.pt --output output_zh.wav -d models -f 0.3
95
  英文:
96
+ python demo_kokoro_ax.py --text "The sky above the port was the color of television, tuned to a dead channel." --lang a --voice checkpoints/voices/af_heart.pt --output output_en.wav -d models -f 0.3
97
  日文:
98
+ python demo_kokoro_ax.py --text "「もしおれがただ偶然、そしてこうしようというつもりでなくここに立っているのなら、ちょっとばかり絶望するところだな」と、そんなことが彼の頭に思い浮かんだ。" --lang j --voice checkpoints/voices/jm_kumo.pt --output output_jp.wav -d models -f 0.3
99
  ```
100
 
101
  注:AX630C平台推理,更改模型文件目录即可,将 -d models 改成 -d models_620E 如下:
102
  ```
103
+ python kokoro_ax.py --text "The sky above the port was the color of television, tuned to a dead channel." --lang a --voice checkpoints/voices/af_heart.pt --output output_en.wav -d models_620E -f 0.3
104
  ```
105
 
106
  参数说明:
 
109
  | --axmodel_dir, -d | 模型文件目录(默认 models) |
110
  | --voice, -v | 声纹文件路径(必填) |
111
  | --text, -t | 合成文本(支持多语言) |
112
+ | --lang, -l | 语言代码(如 a, z, j, ...),目前只测了中英日 |
113
  | --config, -c | 配置文件路径 |
114
  | --output, -o | 输出 wav 文件名 |
115
  | --fade_out, -f | 音频结尾淡出时长(秒),减少音频末尾有杂音 |
demo_kokoro_ax.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import numpy as np
4
+ import soundfile as sf
5
+ import time
6
+ import torch
7
+ from typing import Tuple
8
+ #from onnxruntime import InferenceSession
9
+ from axengine import InferenceSession
10
+
11
+ from inference_utils import (
12
+ audio_numpy_concat, load_vocab_from_config, init_g2p,
13
+ split_input_ids_semantic, generate_input_ids_from_text,
14
+ process_and_merge_sentences, run_batch_inference, apply_fade_out,
15
+ SAMPLE_RATE, DEFAULT_SPEED, DEFAULT_PAUSE, DEFAULT_FADE_OUT
16
+ )
17
+
18
+
19
+ class InferenceEngine:
20
+ """推理"""
21
+
22
+ # 常量
23
+ FIXED_SEQ_LEN = 96
24
+ N_FFT = 20
25
+ HOP_LENGTH = 5
26
+ DOUBLE_INPUT_THRESHOLD = 32 # 输入长度小于此值时复制一倍,适配短文本
27
+
28
+ def __init__(self, axmodel_dir: str):
29
+ self.axmodel_dir = axmodel_dir
30
+
31
+ # 加载模型
32
+ model_files = {
33
+ 'model1': "kokoro_part1_96.axmodel",
34
+ 'model2': "kokoro_part2_96.axmodel",
35
+ 'model3': "kokoro_part3_96.axmodel",
36
+ 'model4': "model4_har_sim.onnx"
37
+ }
38
+
39
+ providers = ['CPUExecutionProvider']
40
+ self.session1 = InferenceSession(os.path.join(axmodel_dir, model_files['model1']))
41
+ self.session2 = InferenceSession(os.path.join(axmodel_dir, model_files['model2']))
42
+ self.session3 = InferenceSession(os.path.join(axmodel_dir, model_files['model3']))
43
+ import onnxruntime as ort
44
+ self.session4 = ort.InferenceSession(os.path.join(axmodel_dir, model_files['model4']), providers=providers)
45
+
46
+ # 统计
47
+ self.model1_time = 0.0
48
+ self.model2_time = 0.0
49
+ self.model3_time = 0.0
50
+ self.har_time = 0.0
51
+ self.inference_count = 0
52
+
53
+ def _compute_external_preprocessing(self, input_ids: np.ndarray, actual_len: int = None) -> Tuple[np.ndarray, np.ndarray]:
54
+ """计算输入预处理:长度和mask"""
55
+ if actual_len is None:
56
+ actual_len = self.FIXED_SEQ_LEN
57
+ input_lengths = np.full((input_ids.shape[0],), actual_len, dtype=np.int64)
58
+ text_mask = np.arange(self.FIXED_SEQ_LEN)[np.newaxis, :] >= input_lengths[:, np.newaxis]
59
+ return input_lengths, text_mask
60
+
61
+ def _compute_har_onnx(self, F0_pred: np.ndarray) -> np.ndarray:
62
+ """使用ONNX模型计算谐波"""
63
+ return self.session4.run(None, {'F0_pred': F0_pred.astype(np.float32)})[0]
64
+
65
+ def _postprocess_x_to_audio(self, x: np.ndarray) -> np.ndarray:
66
+ """将频谱转换为音频波形"""
67
+ spec_part = x[:, :self.N_FFT//2+1, :]
68
+ phase_part = x[:, self.N_FFT//2+1:, :]
69
+
70
+ spec = np.exp(spec_part)
71
+ phase = np.sin(phase_part)
72
+
73
+ spec_torch = torch.from_numpy(spec).float()
74
+ phase_torch = torch.from_numpy(phase).float()
75
+ cos_part = torch.sqrt(1.0 - phase_torch.pow(2).clamp(0, 1))
76
+
77
+ real = spec_torch * cos_part
78
+ imag = spec_torch * phase_torch
79
+ complex_spec = torch.complex(real, imag)
80
+
81
+ audio = torch.istft(
82
+ complex_spec, n_fft=self.N_FFT, hop_length=self.HOP_LENGTH,
83
+ win_length=self.N_FFT, window=torch.hann_window(self.N_FFT),
84
+ center=True, return_complex=False
85
+ )
86
+ return audio.squeeze().numpy()
87
+
88
+ def _prepare_input_ids(self, input_ids: np.ndarray, actual_len: int) -> Tuple[np.ndarray, int, bool]:
89
+ """准备输入ID,对短输入进行复制处理"""
90
+ is_doubled = False
91
+ original_actual_len = actual_len
92
+
93
+ if actual_len <= self.DOUBLE_INPUT_THRESHOLD:
94
+ is_doubled = True
95
+ valid_content = input_ids[:, :actual_len]
96
+ input_ids_doubled = np.concatenate([valid_content, valid_content], axis=1)
97
+
98
+ padding_len = self.FIXED_SEQ_LEN - input_ids_doubled.shape[1]
99
+ if padding_len > 0:
100
+ input_ids = np.concatenate([input_ids_doubled, np.zeros((1, padding_len), dtype=input_ids.dtype)], axis=1)
101
+ else:
102
+ input_ids = input_ids_doubled[:, :self.FIXED_SEQ_LEN]
103
+
104
+ actual_len = min(original_actual_len * 2, self.FIXED_SEQ_LEN)
105
+
106
+ return input_ids, actual_len, is_doubled
107
+
108
+ def inference_single_chunk(
109
+ self,
110
+ input_ids: np.ndarray,
111
+ ref_s: np.ndarray,
112
+ actual_len: int,
113
+ speed: float
114
+ ) -> Tuple[np.ndarray, int, int]:
115
+ """单个chunk的推理"""
116
+ self.inference_count += 1
117
+
118
+ input_ids, actual_len, is_doubled = self._prepare_input_ids(input_ids, actual_len)
119
+
120
+ input_lengths, text_mask = self._compute_external_preprocessing(input_ids, actual_len=actual_len)
121
+
122
+ # Model1: 预测duration
123
+ t1 = time.time()
124
+ outputs1 = self.session1.run(None, {'input_ids': input_ids.astype(np.int32), 'ref_s': ref_s, 'text_mask': text_mask.astype(np.uint8)})
125
+ self.model1_time += time.time() - t1
126
+ duration, d = outputs1
127
+
128
+ # 处理duration并对齐
129
+ pred_dur, total_frames = self._process_duration(duration, actual_len, speed)
130
+ pred_aln_trg = self._create_alignment_matrix(pred_dur, total_frames)
131
+
132
+ # Model2: 预测F0和ASR特征
133
+ d_transposed = np.transpose(d, (0, 2, 1))
134
+ en = d_transposed @ pred_aln_trg
135
+
136
+ t2 = time.time()
137
+ outputs2 = self.session2.run(None, {
138
+ 'en': en.astype(np.float32),
139
+ 'ref_s': ref_s,
140
+ 'input_ids': input_ids.astype(np.int32),
141
+ 'text_mask': text_mask.astype(np.float32),
142
+ 'pred_aln_trg': pred_aln_trg.astype(np.float32)
143
+ })
144
+ self.model2_time += time.time() - t2
145
+ F0_pred, N_pred, asr = outputs2
146
+
147
+ # Model4: 计算谐波
148
+ t_har = time.time()
149
+ har = self._compute_har_onnx(F0_pred)
150
+ self.har_time += time.time() - t_har
151
+
152
+ # Model3: 解码生成频谱
153
+ t3 = time.time()
154
+ outputs3 = self.session3.run(None, {
155
+ 'asr': asr, 'F0_pred': F0_pred, 'N_pred': N_pred, 'ref_s': ref_s, 'har': har
156
+ })
157
+ self.model3_time += time.time() - t3
158
+ x = outputs3[0]
159
+
160
+ # 转换为音频
161
+ audio = self._postprocess_x_to_audio(x)
162
+ actual_content_frames = pred_dur[:actual_len].sum()
163
+
164
+ # 如果输入被复制了,截取前一半音频
165
+ if is_doubled:
166
+ audio = audio[:len(audio) // 2]
167
+ actual_content_frames = actual_content_frames // 2
168
+ total_frames = total_frames // 2
169
+
170
+ return audio, actual_content_frames, total_frames
171
+
172
+ def _process_duration(self, duration: np.ndarray, actual_len: int, speed: float) -> Tuple[np.ndarray, int]:
173
+ """处理duration预测,调整到固定帧数"""
174
+ duration_processed = 1.0 / (1.0 + np.exp(-duration))
175
+ duration_processed = duration_processed.sum(axis=-1) / speed
176
+ pred_dur_original = np.round(duration_processed).clip(min=1).astype(np.int64).squeeze()
177
+
178
+ # 分离实际内容和padding
179
+ pred_dur_actual = pred_dur_original[:actual_len]
180
+ pred_dur_padding = np.zeros(self.FIXED_SEQ_LEN - actual_len, dtype=np.int64)
181
+ pred_dur = np.concatenate([pred_dur_actual, pred_dur_padding])
182
+
183
+ # 调整实际内容部分,只处理长度超出情况
184
+ fixed_total_frames = self.FIXED_SEQ_LEN * 2
185
+ diff = fixed_total_frames - pred_dur[:actual_len].sum()
186
+
187
+ if diff < 0:
188
+ # 减少帧数
189
+ indices = np.argsort(pred_dur[:actual_len])[::-1]
190
+ decreased = 0
191
+ for idx in indices:
192
+ if pred_dur[idx] > 1 and decreased < abs(diff):
193
+ pred_dur[idx] -= 1
194
+ decreased += 1
195
+ if decreased >= abs(diff):
196
+ break
197
+
198
+ # 将剩余帧数分配到padding部分
199
+ remaining_frames = fixed_total_frames - pred_dur[:actual_len].sum()
200
+ padding_len = self.FIXED_SEQ_LEN - actual_len
201
+ if remaining_frames > 0 and padding_len > 0:
202
+ frames_per_padding = remaining_frames // padding_len
203
+ remainder = remaining_frames % padding_len
204
+ pred_dur[actual_len:] = frames_per_padding
205
+ if remainder > 0:
206
+ pred_dur[actual_len:actual_len+remainder] += 1
207
+
208
+ total_frames = pred_dur.sum()
209
+ return pred_dur, total_frames
210
+
211
+ def _create_alignment_matrix(self, pred_dur: np.ndarray, total_frames: int) -> np.ndarray:
212
+ """创建对齐矩阵"""
213
+ indices = np.repeat(np.arange(self.FIXED_SEQ_LEN), pred_dur)
214
+ pred_aln_trg = np.zeros((self.FIXED_SEQ_LEN, total_frames), dtype=np.float32)
215
+ if len(indices) > 0:
216
+ pred_aln_trg[indices, np.arange(total_frames)] = 1.0
217
+ return pred_aln_trg[np.newaxis, ...]
218
+
219
+ def _trim_audio_by_content(self, audio: np.ndarray, actual_content_frames: int,
220
+ total_frames: int, actual_len: int) -> np.ndarray:
221
+ """根据实际内容比例裁剪音频"""
222
+ padding_len = self.FIXED_SEQ_LEN - actual_len
223
+ if padding_len > 0:
224
+ content_ratio = actual_content_frames / total_frames
225
+ audio_len_to_keep = int(len(audio) * content_ratio)
226
+ return audio[:audio_len_to_keep]
227
+ return audio
228
+
229
+
230
+ def inference(
231
+ self,
232
+ input_ids: np.ndarray,
233
+ ref_s: np.ndarray,
234
+ phonemes: str,
235
+ vocab: dict,
236
+ speed: float = DEFAULT_SPEED,
237
+ fade_out_duration: float = DEFAULT_FADE_OUT
238
+ ) -> np.ndarray:
239
+ """推理生成音频"""
240
+ chunks = split_input_ids_semantic(input_ids, self.FIXED_SEQ_LEN)
241
+ fade_samples = int(SAMPLE_RATE * fade_out_duration) if fade_out_duration > 0 else 0
242
+
243
+ # if len(chunks) == 1:
244
+ # # 单个chunk
245
+ audio, actual_content_frames, total_frames = self.inference_single_chunk(
246
+ chunks[0]['input_ids'], ref_s, chunks[0]['actual_len'], speed
247
+ )
248
+ audio_trimmed = self._trim_audio_by_content(
249
+ audio, actual_content_frames, total_frames, chunks[0]['actual_len']
250
+ )
251
+ if fade_samples > 0:
252
+ audio_trimmed = apply_fade_out(audio_trimmed, fade_samples)
253
+ return audio_trimmed
254
+
255
+
256
+ def main():
257
+ parser = argparse.ArgumentParser()
258
+ parser.add_argument("--axmodel_dir", "-d", type=str, default="onnx")
259
+ parser.add_argument("--text", "-t", type=str, default="The sky above the port was the color of television, tuned to a dead channel.")
260
+ parser.add_argument("--lang", "-l", type=str, default='a')
261
+ parser.add_argument("--voice", "-v", type=str, required=True)
262
+ parser.add_argument("--config", "-c", type=str, default="checkpoints/config.json")
263
+ parser.add_argument("--output", "-o", type=str, default="output.wav")
264
+ parser.add_argument("--fade_out", "-f", type=float, default=0.3)
265
+ parser.add_argument("--max_len", "-m", type=int, default=96)
266
+ args = parser.parse_args()
267
+
268
+ SPEED = 1.0 #速度暂时固定
269
+ PAUSE = 0.0 #不加长停顿
270
+
271
+ start_total = time.time()
272
+
273
+ # 初始化
274
+ print("初始化模型...")
275
+ engine = InferenceEngine(args.axmodel_dir)
276
+ vocab = load_vocab_from_config(args.config)
277
+ g2p, g2p_type = init_g2p(args.lang)
278
+
279
+ # Warmup g2p模型
280
+ warmup_text = "你好" if args.lang == 'z' else "hello"
281
+ _, _ = generate_input_ids_from_text(warmup_text, g2p=g2p, g2p_type=g2p_type, vocab=vocab)
282
+ init_time = time.time() - start_total
283
+ print(f"初始化完成: {init_time:.3f}s\n")
284
+
285
+ # 文本处理
286
+ time_cost = time.time()
287
+ merged_groups = process_and_merge_sentences(
288
+ args.text, args.lang, g2p, g2p_type, vocab, max_merge_len=args.max_len
289
+ )
290
+
291
+ # pdb.set_trace()
292
+ # 推理
293
+ audio_list = run_batch_inference(
294
+ engine, merged_groups, args.voice, vocab,
295
+ speed=SPEED, fade_out_duration=args.fade_out
296
+ )
297
+
298
+ inference_time = time.time() - time_cost
299
+
300
+ # 音频保存
301
+ final_audio = audio_numpy_concat(audio_list, sr=SAMPLE_RATE, speed=SPEED, pause_duration=PAUSE)
302
+ sf.write(args.output, final_audio, SAMPLE_RATE)
303
+
304
+ audio_duration = len(final_audio) / SAMPLE_RATE
305
+ print("\n" + "="*60)
306
+ print(f"输出: {args.output} | 时长: {audio_duration:.2f}s")
307
+ print("="*60)
308
+ print(f"初始化: {init_time:.3f}s")
309
+ print(f"音频推理: {inference_time:.3f}s (共{engine.inference_count}次)")
310
+ if engine.inference_count > 0:
311
+ print(f" ├─ Model1: {engine.model1_time:.3f}s (平均{engine.model1_time/engine.inference_count*1000:.1f}ms)")
312
+ print(f" ├─ Model2: {engine.model2_time:.3f}s (平均{engine.model2_time/engine.inference_count*1000:.1f}ms)")
313
+ print(f" ├─ Model3: {engine.model3_time:.3f}s (平均{engine.model3_time/engine.inference_count*1000:.1f}ms)")
314
+ print(f" └─ Model4 onnx: {engine.har_time:.3f}s (平均{engine.har_time/engine.inference_count*1000:.1f}ms)")
315
+
316
+ print(f"\n rtf:{inference_time/audio_duration:.3f}")
317
+ print("="*60)
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()
inference_utils.py CHANGED
@@ -160,7 +160,10 @@ def phonemes_to_input_ids(phonemes: str, vocab: Dict[str, int], debug: bool = Fa
160
 
161
 
162
  def load_voice_embedding(voice_path: str, phoneme_len: Optional[int] = None) -> np.ndarray:
163
- voice_path = voice_path.replace('checkpoints/voices', 'checkpoints/voices_npy').replace('.pt', '.npy')
 
 
 
164
  pack = np.load(voice_path).reshape(510,1,256)
165
  if phoneme_len is not None:
166
  ref_s = pack[phoneme_len:phoneme_len+1]
 
160
 
161
 
162
  def load_voice_embedding(voice_path: str, phoneme_len: Optional[int] = None) -> np.ndarray:
163
+ if "checkpoints/voices_npy" in voice_path:
164
+ pass
165
+ else:
166
+ voice_path = voice_path.replace('checkpoints/voices', 'checkpoints/voices_npy').replace('.pt', '.npy')
167
  pack = np.load(voice_path).reshape(510,1,256)
168
  if phoneme_len is not None:
169
  ref_s = pack[phoneme_len:phoneme_len+1]