RvcPyInfer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ from typing import Any, Literal
2
+
3
+ import numpy as np
4
+ import samplerate
5
+ from numpy.typing import NDArray
6
+
7
+ from ..type_alist import Audio
8
+
9
+
10
+ def reSR(orig: Audio,
11
+ target_sr: int,
12
+ algorithm: Literal["sinc_best", "sinc_medium", "sinc_fastest", "linear", "zero_order_hold"] = "sinc_medium") -> Audio:
13
+ orig_data, orig_sr = orig
14
+ ratio = target_sr / orig_sr
15
+ if target_sr == orig_sr:
16
+ target_data = orig_data
17
+ else:
18
+ target_data: NDArray[Any] = samplerate.resample(orig_data, ratio, algorithm)
19
+ assert isinstance(target_data, np.ndarray), "这库不对吧?返回值不是 ndarray"
20
+ target_data = target_data.astype(np.float32)
21
+ return target_data, target_sr
22
+
23
+ def frame_rms(audio: Audio, frame_len: int = 20, hop_len: int = 10) -> NDArray[np.float32]:
24
+ """
25
+ 分帧计算RMS能量
26
+
27
+ 参数:
28
+ audio: 输入音频
29
+ frame_len: 帧长度(ms)
30
+ hop_len: 帧移(ms)
31
+
32
+ 返回:
33
+ rms_values: 每帧的RMS值
34
+ """
35
+ data, sr = audio
36
+
37
+ frame_size = int(round(frame_len * sr / 1000))
38
+ hop_size = int(round(hop_len * sr / 1000))
39
+
40
+ if len(data) < frame_size:
41
+ return np.array([np.sqrt(np.mean(data ** 2))], dtype=np.float32)
42
+
43
+ shape = ((len(data) - frame_size) // hop_size + 1, frame_size)
44
+ strides = (data.strides[0] * hop_size, data.strides[0])
45
+ frames = np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides)
46
+
47
+ rms_values = np.sqrt(np.mean(frames ** 2, axis=1))
48
+
49
+ return rms_values
50
+
51
+ def rms_frame_match(source: Audio,
52
+ target: Audio,
53
+ frame_len: int = 20,
54
+ hop_len: int = 10,
55
+ mix: float = 1.0,
56
+ eps: float = 1e-8) -> Audio:
57
+ """
58
+ 将 source 的逐帧 RMS 包络匹配到 target。
59
+
60
+ 参数:
61
+ source: 源音频(将被增益调整)
62
+ target: 目标音频(提取 RMS 包络的参考)
63
+ frame_len: 帧长度(ms)
64
+ hop_len: 帧移(ms)
65
+ mix: 匹配系数,0.0=直接输出源音频,1.0=完全匹配目标RMS
66
+ eps: 防除零,和检测 mix 是否为0
67
+
68
+ 返回:
69
+ Audio: RMS 匹配后的音频
70
+ """
71
+ def _smooth_gain[T: np.floating](gain: NDArray[T], smooth_window: int = 15) -> NDArray[T]:
72
+ """边缘延伸 padding 后卷积,常数增益卷积后仍为常数"""
73
+ if smooth_window <= 1 or len(gain) <= 1:
74
+ return gain
75
+ kernel = np.ones(smooth_window) / smooth_window
76
+ pad = smooth_window // 2
77
+ padded = np.concatenate([
78
+ np.full(pad, gain[0]),
79
+ gain,
80
+ np.full(pad, gain[-1])
81
+ ])
82
+ return np.convolve(padded, kernel, mode='valid').astype(gain.dtype)
83
+
84
+ if mix < eps: # 因为太小了,几乎没有更改
85
+ return source
86
+
87
+ src_data, src_sr = source
88
+ _, tgt_sr = target
89
+
90
+ assert src_sr == tgt_sr, "采样率必须一致"
91
+
92
+ N = len(src_data)
93
+
94
+ # ---- 1. 逐帧 RMS ----
95
+ src_rms = frame_rms(source, frame_len, hop_len)
96
+ tgt_rms = frame_rms(target, frame_len, hop_len)
97
+ assert len(src_rms) == len(tgt_rms), "帧数必须一致"
98
+
99
+ # ---- 2. 逐帧增益 + 匹配系数混合 ----
100
+ gain = tgt_rms / (src_rms + eps)
101
+ gain = 1.0 + mix * (gain - 1.0)
102
+
103
+ # ---- 3. 增益曲线平滑(边缘延伸 padding,无边界失真)----
104
+ gain = _smooth_gain(gain)
105
+
106
+ # ---- 4. 帧参数 ----
107
+ frame_size = int(round(frame_len * src_sr / 1000))
108
+ hop_size = int(round(hop_len * src_sr / 1000))
109
+ n_frames = len(gain)
110
+
111
+ # ---- 5. 逐帧增益 → 逐样本增益(overlap-add 归一化)----
112
+ sample_gain = np.zeros(N, dtype=np.float64)
113
+ norm = np.zeros(N, dtype=np.float64)
114
+ for i in range(n_frames):
115
+ start = i * hop_size
116
+ end = min(start + frame_size, N)
117
+ sample_gain[start:end] += gain[i]
118
+ norm[start:end] += 1.0
119
+ norm[norm == 0] = 1.0
120
+ sample_gain /= norm
121
+
122
+ # ---- 6. 尾部 ----
123
+ if n_frames > 0:
124
+ last_end = (n_frames - 1) * hop_size + frame_size
125
+ if last_end < N:
126
+ sample_gain[last_end:] = gain[-1]
127
+
128
+ # ---- 7. 应用增益 ----
129
+ output = src_data * sample_gain
130
+ return (output.astype(np.float32), src_sr)
131
+
132
+ def rms_to_db[T: np.floating](rms_values: NDArray[T], ref: float = 1.0) -> NDArray[T]:
133
+ """
134
+ 将RMS值转换为dB
135
+
136
+ 参数:
137
+ rms_values: RMS值数组
138
+ ref: 参考值 (1.0 表示归一化音频的 dBFS)
139
+ min_db: 最小dB值 (防止静音时出现 -inf)
140
+
141
+ 返回:
142
+ db_values: 对应的dB值
143
+ """
144
+ # 防止 log(0) 错误:给一个极小值 epsilon
145
+ epsilon = 1e-10
146
+
147
+ # 公式: 20 * log10(rms / ref)
148
+ db_values = 20 * np.log10(np.maximum(rms_values, epsilon) / ref)
149
+
150
+ return db_values
151
+
152
+ def split_by_silence(
153
+ audio: Audio,
154
+ *,
155
+ frame_len: int = 20,
156
+ hop_len: int = 10,
157
+ silence_thresh_db: float = -35,
158
+ ref: float = 1.0,
159
+ min_silence_duration_ms: float = 800.0,
160
+ max_transition_ms: float = 50
161
+ ) -> list[tuple[Audio, bool]]:
162
+ """
163
+ 按静音切片,返回所有片段(含静音段)。
164
+
165
+ 参数:
166
+ audio: 输入音频
167
+ frame_len: 帧长度
168
+ hop_len: 帧移
169
+ silence_thresh_db: 静音阈值
170
+ ref: RMS参考值
171
+ min_silence_duration_ms: 最短静音持续时间。小于此值的静音段将被忽略。请注意,这不代表返回的静音段一定大于这个值
172
+ max_transition_ms: 语音段的过渡时间,不得大于 0.5*min_silence_duration_ms
173
+
174
+ 返回: List[Tuple[Audio, bool]]
175
+ - Audio: 切片后的音频
176
+ - bool: True = 该段为静音, False = 该段为语音
177
+ """
178
+ data, sr = audio
179
+ data = np.asarray(data)
180
+
181
+ # 1. 复用 frame_rms + rms_to_db
182
+ rms_values = frame_rms(audio, frame_len=frame_len, hop_len=hop_len)
183
+ db_values = rms_to_db(rms_values, ref=ref)
184
+ is_silence = db_values < silence_thresh_db
185
+ n_frames = len(is_silence)
186
+
187
+ # 2. 找静音区间起止帧(padding False 保证首尾闭合)
188
+ padded = np.concatenate([[False], is_silence, [False]])
189
+ diff = np.diff(padded.astype(np.int8))
190
+ sil_starts = np.where(diff == 1)[0] # 静音开始帧
191
+ sil_ends = np.where(diff == -1)[0] # 静音结束帧
192
+
193
+ if len(sil_starts) > 0 and min_silence_duration_ms > 0:
194
+ # 计算最短静音所需帧数 (向上取整,至少1帧)
195
+ min_silence_frames = max(1, int(np.ceil(min_silence_duration_ms / hop_len)))
196
+
197
+ # 计算每个静音段的持续帧数
198
+ sil_lengths = sil_ends - sil_starts
199
+
200
+ # 过滤出长度达标的静音段
201
+ valid_mask = sil_lengths >= min_silence_frames
202
+ sil_starts = sil_starts[valid_mask]
203
+ sil_ends = sil_ends[valid_mask]
204
+
205
+ # 3. 交错排列得到所有边界
206
+ # bounds = [0, ss0, se0, ss1, se1, ..., n_frames]
207
+ # 偶数段索引 → 语音(False), 奇数段索引 → 静音(True)
208
+ bounds = np.empty(len(sil_starts) + len(sil_ends) + 2, dtype=np.int64)
209
+ bounds[0] = 0
210
+ bounds[-1] = n_frames
211
+ if len(sil_starts) > 0:
212
+ bounds[1:-1:2] = sil_starts
213
+ bounds[2:-1:2] = sil_ends
214
+
215
+ # 4. 帧索引 → 采样索引
216
+ hop_size = int(round(hop_len * sr / 1000))
217
+ sample_bounds = bounds * hop_size
218
+ sample_bounds[-1] = len(data) # 末尾对齐信号终点
219
+
220
+ if bounds[-1] == bounds[-2]:
221
+ sample_bounds[-2] = len(data) # 说明实际上没有这个段,这是靠 pad 出来的段
222
+
223
+ # 5. 调整索引形成过渡
224
+ max_transition_size = max_transition_ms * sr // 1000
225
+ s = sample_bounds[1:-1:2]
226
+ e = sample_bounds[2::2]
227
+ sil_size = e - s
228
+ voice_size = sample_bounds[1::2] - sample_bounds[0:-1:2]
229
+ # 因为前面修正 pad 逻辑的时候会产生 0 长切片,这里不能对 0 长切片调整(因为那没有意义)
230
+ voice_mask = voice_size > 0
231
+ start_mask = voice_mask[:-1]
232
+ end_mask = voice_mask[1:]
233
+ transition_size = np.minimum(max_transition_size, sil_size // 2)
234
+ s[start_mask] = s[start_mask] + transition_size[start_mask]
235
+ e[end_mask] = e[end_mask] - transition_size[end_mask]
236
+
237
+ # 6. 切片前的合并
238
+ # 6.1 处理零长
239
+ is_sil = np.empty(len(sample_bounds) - 1, dtype=np.bool)
240
+ is_sil[1::2] = True
241
+ is_sil[0::2] = False
242
+ index_diff = np.diff(sample_bounds)
243
+ index_mask = index_diff > 0 # 0 长丢掉
244
+ index_mask = np.pad(index_mask, [1, 0], mode="constant", constant_values=True)
245
+ is_sil = is_sil[index_mask[1:]]
246
+ sample_bounds = sample_bounds[index_mask]
247
+ # 6.2 合并同类项
248
+ sil_mask = is_sil[:-1] != is_sil[1:]
249
+ sil_mask = np.pad(sil_mask, [1, 1], mode="constant", constant_values=True)
250
+ is_sil = is_sil[sil_mask[:-1]]
251
+ sample_bounds = sample_bounds[sil_mask]
252
+
253
+ # 7. 切片!
254
+ return [((data[sample_bounds[i]:sample_bounds[i+1]], sr), is_sil[i]) for i in range(len(sample_bounds) - 1)]
255
+
256
+ def split_by_max_len_with_overlap(
257
+ audio: Audio,
258
+ *,
259
+ max_len: int = 30,
260
+ overlap_len: int = 5
261
+ ) -> list[Audio]:
262
+ data, sr = audio
263
+ max_size = sr * max_len
264
+
265
+ if overlap_len < 0 or overlap_len >= max_len or len(data) <= max_size:
266
+ return [audio]
267
+
268
+ overlap_size = sr * overlap_len
269
+ step = max_size - overlap_size # 每次向前推进的步长
270
+
271
+ res: list[Audio] = []
272
+ offset = 0
273
+
274
+ while offset < len(data):
275
+ end = min(offset + max_size, len(data))
276
+ res.append((data[offset:end], sr))
277
+
278
+ if end == len(data):
279
+ break
280
+
281
+ offset += step
282
+
283
+ return res
284
+
285
+ def crossfade(
286
+ audios: list[Audio],
287
+ *,
288
+ overlap_len: int = 5
289
+ ) -> Audio:
290
+ if len(audios) == 0:
291
+ raise ValueError("音频列表为空")
292
+ elif len(audios) == 1:
293
+ return audios[0]
294
+
295
+ _, sr = audios[0]
296
+ datas = [d for d, _ in audios]
297
+ overlap_size = sr * overlap_len
298
+ fade_in = np.linspace(0.0, 1.0, overlap_size)
299
+ fade_out = np.linspace(1.0, 0.0, overlap_size)
300
+
301
+ data_list = []
302
+ # 第一个
303
+ data_list.append(datas[0][:-overlap_size])
304
+ data_list.append(datas[0][-overlap_size:] * fade_out + datas[1][:overlap_size] * fade_in)
305
+
306
+ # 中间
307
+ for i in range(1, len(datas) - 1):
308
+ curr = datas[i]
309
+ next = datas[i + 1]
310
+ data_list.append(curr[overlap_size:-overlap_size])
311
+ data_list.append(curr[-overlap_size:] * fade_out + next[:overlap_size] * fade_in)
312
+
313
+ # 最后一个
314
+ data_list.append(datas[-1][overlap_size:])
315
+
316
+ return np.concatenate(data_list), sr
317
+
318
+ def copy_audio(audios: list[Audio]) -> list[Audio]:
319
+ for i in range(1, len(audios)):
320
+ audios[i] = (audios[i][0].copy(), audios[i][1])
321
+ return audios
322
+
323
+ def print_segments_info(segments: list[tuple[Audio, bool]]) -> None:
324
+ """
325
+ 打印切片结果的时间戳和静音标注。
326
+
327
+ 参数:
328
+ segments: split_by_silence 函数的返回结果
329
+ """
330
+ print(f"{'序号':<4} | {'开始时间 (s)':<12} | {'结束时间 (s)':<12} | {'时长 (s)':<10} | {'类型'}")
331
+ print("-" * 60)
332
+
333
+ current_time = 0.0
334
+
335
+ for i, ((seg_data, sr), is_sil) in enumerate(segments):
336
+ # 计算当前片段的时长
337
+ duration = len(seg_data) / sr
338
+ start_time = current_time
339
+ end_time = start_time + duration
340
+
341
+ # 更新时间游标
342
+ current_time = end_time
343
+
344
+ # 格式化输出
345
+ tag = "[静音]" if is_sil else "[语音]"
346
+ print(f"{i:<4} | {start_time:<12.3f} | {end_time:<12.3f} | {duration:<10.3f} | {tag}")
RvcPyInfer/cli.py ADDED
@@ -0,0 +1,112 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from .InferProviders import InferProviders
5
+ from .RvcContext import RvcContext
6
+
7
+
8
+ def str_to_provider(provider_str: str) -> InferProviders:
9
+ """将命令行字符串映射为 InferProviders 枚举"""
10
+ if provider_str == "default":
11
+ return InferProviders.default()
12
+ elif provider_str == "cuda":
13
+ return InferProviders.CUDA
14
+ elif provider_str == "dml":
15
+ return InferProviders.DML
16
+ elif provider_str == "ort_cpu":
17
+ return InferProviders.ORT_CPU
18
+ elif provider_str == "openvino_auto":
19
+ return InferProviders.OPENVINO_AUTO
20
+ elif provider_str == "openvino_cpu":
21
+ return InferProviders.OPENVINO_CPU
22
+ elif provider_str == "openvino_gpu":
23
+ return InferProviders.OPENVINO_GPU
24
+ else:
25
+ raise ValueError(f"未知的 Provider: {provider_str}")
26
+
27
+ def main() -> None:
28
+ parser = argparse.ArgumentParser(description="RVC 语音转换命令行工具")
29
+
30
+ # --- 必填参数 ---
31
+ parser.add_argument("--vec-model", type=str, required=True, help="ContentVec 特征提取模型路径 (.onnx)")
32
+ parser.add_argument("--gen-model", type=str, required=True, help="RVC 生成模型路径 (.onnx)")
33
+ parser.add_argument("--gen-model-sr", type=int, required=True, help="生成模型采样率 (如 40000, 48000)")
34
+ parser.add_argument("-i", "--inputs", nargs="+", type=str, required=True, help="输入音频文件路径 (可传入多个)")
35
+ parser.add_argument("-o", "--outputs", nargs="+", type=str, required=True, help="输出音频文件路径 (数量必须与输入一致)")
36
+
37
+ # --- 推理引擎 ---
38
+ parser.add_argument("--provider", type=str, default="default",
39
+ choices=["default", "cuda", "dml", "ort_cpu", "openvino_auto", "openvino_cpu", "openvino_gpu"],
40
+ help="推理后端 (默认自动检测)")
41
+
42
+ # --- 基础设置 ---
43
+ parser.add_argument("--sid", type=int, default=0, help="说话人 ID")
44
+ parser.add_argument("--seed", type=int, default=1234, help="随机种子")
45
+
46
+ # --- 特征索引 ---
47
+ parser.add_argument("--index-path", type=str, default=None, help="特征索引文件路径")
48
+ parser.add_argument("--index-rate", type=float, default=0.33, help="特征索引比率 (0-1)")
49
+ parser.add_argument("--index-k", type=int, default=8, help="检索的最近特征数量")
50
+
51
+ # --- f0 提取 ---
52
+ parser.add_argument("--f0-method", type=str, default="dio", help="f0 提取算法 (如 dio, pm, rmvpe, fcpe 等)")
53
+ parser.add_argument("--pitch", type=float, default=0, help="升降调 (半音)")
54
+ parser.add_argument("--f0-min", type=float, default=50, help="最低 f0")
55
+ parser.add_argument("--f0-max", type=float, default=1100, help="最高 f0")
56
+
57
+ # --- 强制切片与交叉淡化 ---
58
+ parser.add_argument("--slice-max-len", type=int, default=30, help="最大切片长度 (秒)")
59
+ parser.add_argument("--slice-overlap-len", type=int, default=5, help="切片交叉淡化长度 (秒)")
60
+
61
+ # --- 静音切片 ---
62
+ parser.add_argument("--silence-thresh-db", type=float, default=-40, help="静音阈值")
63
+ parser.add_argument("--silence-min-duration", type=float, default=800.0, help="最小静音时长")
64
+ parser.add_argument("--silence-max-transition", type=float, default=100.0, help="最大过渡时长")
65
+
66
+ args = parser.parse_args()
67
+
68
+ # 校验输入输出数量
69
+ if len(args.inputs) != len(args.outputs):
70
+ print("错误: 输入音频数量与输出音频数量不一致!", file=sys.stderr)
71
+ sys.exit(1)
72
+
73
+ try:
74
+ # 1. 初始化推理 Provider 和 Context
75
+ print(f"正在初始化推理环境,使用 Provider: {args.provider} ...")
76
+ providers = str_to_provider(args.provider)
77
+ context = RvcContext(providers=providers)
78
+
79
+ # 2. 构建 InferTask
80
+ print("正在构建推理任务...")
81
+ task = context.build_task(
82
+ args.vec_model,
83
+ args.gen_model,
84
+ args.gen_model_sr,
85
+ *args.inputs,
86
+ sid=args.sid,
87
+ seed=args.seed,
88
+ index_path=args.index_path,
89
+ index_rate=args.index_rate,
90
+ index_k=args.index_k,
91
+ f0extract_algorithm=args.f0_method,
92
+ f0_up_semitone=args.pitch,
93
+ f0_min=args.f0_min,
94
+ f0_max=args.f0_max,
95
+ slice_max_len=args.slice_max_len,
96
+ slice_overlap_len=args.slice_overlap_len,
97
+ silence_thresh_db=args.silence_thresh_db,
98
+ silence_min_silence_duration_ms=args.silence_min_duration,
99
+ silence_max_transition_ms=args.silence_max_transition,
100
+ )
101
+
102
+ # 3. 执行推理并保存
103
+ print("开始推理,请稍候...")
104
+ task.run_and_save(*args.outputs)
105
+ print(f"推理完成!音频已保存至: {', '.join(args.outputs)}")
106
+
107
+ except Exception as e:
108
+ print(f"推理过程中发生错误: {e}", file=sys.stderr)
109
+ sys.exit(1)
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,2 @@
1
+ class InferEnvError(Exception):
2
+ pass
@@ -0,0 +1,2 @@
1
+ class NotSupportedAlgorithmError(Exception):
2
+ pass
RvcPyInfer/f0_utils.py ADDED
@@ -0,0 +1,125 @@
1
+ import math
2
+ from collections.abc import Callable
3
+ from typing import overload
4
+
5
+ import numpy as np
6
+ import pyworld as pw
7
+ from numpy.typing import NDArray
8
+
9
+ from .error.NotSupportedAlgorithmError import NotSupportedAlgorithmError
10
+ from .type_alist import Audio, F0ExtractAlgorithm
11
+
12
+
13
+ def interpolate_f0[T: np.floating](f0: NDArray[T]) -> NDArray[T]:
14
+ """
15
+ 对F0进行插值处理
16
+ """
17
+ nonzero_idx = np.where(f0 > 0.0)[0]
18
+ if len(nonzero_idx) == 0:
19
+ return f0
20
+ all_idx = np.arange(len(f0))
21
+ ip_data = np.interp(all_idx, nonzero_idx, f0[nonzero_idx])
22
+ ip_data = ip_data.astype(f0.dtype)
23
+ return ip_data
24
+
25
+ def dio(audio: Audio,
26
+ p_len: int,
27
+ f0_min: float = 50.0,
28
+ f0_max: float = 1100.0,
29
+ allowed_range: float = 0.1,
30
+ channels_in_octave: float = 2.0) -> NDArray[np.float32]:
31
+ data, sr = audio
32
+ data = data.astype(np.float64)
33
+ frame_period = len(data) * 1000 / sr / p_len
34
+ f0, t = pw.dio( # pyright: ignore[reportAttributeAccessIssue]
35
+ data,
36
+ sr,
37
+ f0_floor=f0_min,
38
+ f0_ceil=f0_max,
39
+ frame_period=frame_period,
40
+ allowed_range=allowed_range,
41
+ channels_in_octave=channels_in_octave
42
+ )
43
+ f0 = pw.stonemask( # pyright: ignore[reportAttributeAccessIssue]
44
+ data,
45
+ f0, t,
46
+ sr
47
+ )
48
+ assert isinstance(f0, np.ndarray) and isinstance(t, np.ndarray), "pyworld 输出类型应是 NDArray" # 给类型注解看的
49
+ f0, t = f0.astype(np.float32), t.astype(np.float32)
50
+ f0 = np.round(f0, 1) # 不知道,但原项目就是这么做了,我只是一个做 onnx 推理的我怎么知道为什么要这样做
51
+ f0 = interpolate_f0(f0)
52
+ # 一般来说不太可能会发生长度不匹配的情况,但防御性编程
53
+ pad_len = p_len - len(f0)
54
+ if pad_len == 0:
55
+ return f0
56
+ elif pad_len > 0:
57
+ return np.pad(f0, [0, pad_len], mode="constant", constant_values=f0[-1])
58
+ else:
59
+ return f0[:p_len]
60
+
61
+
62
+ def harvest(audio: Audio,
63
+ p_len: int,
64
+ f0_min: float = 50.0,
65
+ f0_max: float = 1100.0) -> NDArray[np.float32]:
66
+ data, sr = audio
67
+ data = data.astype(np.float64)
68
+ frame_period = len(data) * 1000 / sr / p_len
69
+ f0, t = pw.harvest( # pyright: ignore[reportAttributeAccessIssue]
70
+ data,
71
+ sr,
72
+ f0_floor=f0_min,
73
+ f0_ceil=f0_max,
74
+ frame_period=frame_period
75
+ )
76
+ f0 = pw.stonemask( # pyright: ignore[reportAttributeAccessIssue]
77
+ data,
78
+ f0, t,
79
+ sr
80
+ )
81
+ assert isinstance(f0, np.ndarray) and isinstance(t, np.ndarray), "pyworld 输出类型应是 NDArray" # 给类型注解看的
82
+ f0, t = f0.astype(np.float32), t.astype(np.float32)
83
+ f0 = interpolate_f0(f0)
84
+ # 一般来说不太可能会发生长度不匹配的情况,但防御性编程
85
+ pad_len = p_len - len(f0)
86
+ if pad_len == 0:
87
+ return f0
88
+ elif pad_len > 0:
89
+ return np.pad(f0, [0, pad_len], mode="constant", constant_values=f0[-1])
90
+ else:
91
+ return f0[:p_len]
92
+
93
+ def build_f0extract_func(algorithm: F0ExtractAlgorithm,
94
+ f0_min: float = 50.0,
95
+ f0_max: float = 1100.0,
96
+ allowed_range: float = 0.1,
97
+ channels_in_octave: float = 2.0) -> Callable[[Audio, int], NDArray[np.float32]]:
98
+ if algorithm == "dio":
99
+ return lambda a, p_l: dio(a, p_l, f0_min=f0_min, f0_max=f0_max, allowed_range=allowed_range, channels_in_octave=channels_in_octave)
100
+ elif algorithm == "harvest":
101
+ return lambda a, p_l: harvest(a, p_l, f0_min=f0_min, f0_max=f0_max)
102
+ else:
103
+ raise NotSupportedAlgorithmError(f"不支持的 f0 提取算法: {algorithm}")
104
+
105
+ def apply_rise_tone[T: np.floating](f0: NDArray[T], up_semitone: float) -> NDArray[T]:
106
+ return f0 * 2.0 ** (up_semitone / 12.0)
107
+
108
+ @overload
109
+ def f0_to_mel(f0: float) -> float:
110
+ ...
111
+ @overload
112
+ def f0_to_mel[T: np.floating](f0: NDArray[T]) -> NDArray[T]:
113
+ ...
114
+ def f0_to_mel[T: np.floating](f0: float | NDArray[T]) -> float | NDArray[T]:
115
+ if isinstance(f0, float):
116
+ return 1127.0 * math.log(1 + f0 / 700.0)
117
+ else:
118
+ return 1127 * np.log(1 + f0 / 700)
119
+
120
+ def normalized_mel[T: np.floating](mel: NDArray[T], mel_min: float = f0_to_mel(50.0), mel_max: float = f0_to_mel(1100.0)) -> NDArray[T]:
121
+ mel[mel > 0.0] = (mel[mel > 0.0] - mel_min) * 254.0 / (
122
+ mel_max - mel_min
123
+ ) + 1.0
124
+ mel = np.clip(mel, 1, 255)
125
+ return mel
@@ -0,0 +1,19 @@
1
+ import numpy as np
2
+ from numpy.typing import NDArray
3
+
4
+ from ..path_utils import path
5
+ from ..type_alist import PathLike
6
+
7
+
8
+ class RvcFeatIndex:
9
+ def __init__(self, index: PathLike) -> None:
10
+ import faiss # pyright: ignore[reportMissingImports]
11
+ self.faiss_index = faiss.read_index(str(path(index).resolve()))
12
+ self.faiss_data: NDArray = self.faiss_index.reconstruct_n(0, self.faiss_index.ntotal)
13
+
14
+ def apply_index(self, feats: NDArray[np.float32], index_rate: float = 0.66, k: int = 8) -> NDArray[np.float32]:
15
+ score, ix = self.faiss_index.search(feats, k=k)
16
+ weight = np.square(1 / score)
17
+ weight /= weight.sum(axis=1, keepdims=True)
18
+ index_feats = np.sum(self.faiss_data[ix] * np.expand_dims(weight, axis=2), axis=1)
19
+ return index_feats * index_rate + (1 - index_rate) * feats
@@ -0,0 +1,28 @@
1
+ # pyright: reportUnusedImport=false
2
+
3
+ import warnings
4
+
5
+ from .warn.InferEnvWarn import InferEnvWarn
6
+
7
+ try:
8
+ import onnxruntime # pyright: ignore[reportMissingImports] # noqa: F401
9
+ HAS_ORT = True
10
+ except ImportError:
11
+ HAS_ORT = False
12
+
13
+ try:
14
+ import openvino # pyright: ignore[reportMissingImports] # noqa: F401
15
+ HAS_OPENVINO = True
16
+ except ImportError:
17
+ HAS_OPENVINO = False
18
+
19
+ HAS_ONE = HAS_ORT or HAS_OPENVINO
20
+
21
+ if not HAS_ONE:
22
+ warnings.warn("没有任意一个支持的推理引擎,请至少安装一个可选模块", InferEnvWarn)
23
+
24
+ try:
25
+ import faiss # pyright: ignore[reportMissingImports] # noqa: F401
26
+ HAS_FAISS = True
27
+ except ImportError:
28
+ HAS_FAISS = False
@@ -0,0 +1,38 @@
1
+ import numpy as np
2
+ from numpy.typing import NDArray
3
+
4
+ from ..audio.audio_utils import reSR
5
+ from ..error.InferEnvError import InferEnvError
6
+ from ..InferProviders import InferProviders
7
+ from ..path_utils import path as pathf
8
+ from ..type_alist import Audio, PathLike
9
+ from .model_loader import load_model
10
+
11
+
12
+ class ContentVec:
13
+ def __init__(self, path: PathLike, providers: InferProviders) -> None:
14
+ self.session, self.compiled_model = load_model(pathf(path), providers)
15
+
16
+ def infer(self, audio: Audio) -> NDArray[np.float32]:
17
+ data, sr = audio
18
+ assert len(data.shape) == 1, "输入音频应为单通道"
19
+ data, sr = reSR(audio, target_sr=16000)
20
+ data = data.reshape(1, 1, -1)
21
+ input = {
22
+ "source": data
23
+ }
24
+ logits = None
25
+ if self.session is not None:
26
+ logits = self.session.run(
27
+ output_names=["embed"],
28
+ input_feed=input)[0]
29
+ assert isinstance(logits, np.ndarray), "ort 输出应为 NDArray"
30
+ elif self.compiled_model is not None:
31
+ infer_request = self.compiled_model.create_infer_request()
32
+ result_dict = infer_request.infer(inputs=input)
33
+ logits = result_dict["embed"]
34
+ del infer_request, result_dict
35
+ else:
36
+ raise InferEnvError("没有任意一个支持的推理引擎,请至少安装一个可选模块")
37
+ # (1, frames, embed_size)
38
+ return np.repeat(logits, 2, axis=1).squeeze(axis=0).astype(np.float32) # 因为模型帧长为 320, 这里复制一份到 160