Update inference demo
Browse files- README.md +5 -5
- demo_kokoro_ax.py +321 -0
- 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
|
| 95 |
英文:
|
| 96 |
-
python
|
| 97 |
日文:
|
| 98 |
-
python
|
| 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
|
| 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 | 语言代码(如
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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]
|