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,108 @@
1
+ from enum import Flag, auto
2
+
3
+ from .error.InferEnvError import InferEnvError
4
+
5
+
6
+ class InferProviders(Flag):
7
+ NONE = auto() # 仅用于内部方便处理
8
+
9
+ # ort
10
+ ORT_CPU = auto()
11
+ FORCE_CUDA = auto()
12
+ FORCE_DML = auto()
13
+ CUDA = ORT_CPU | FORCE_CUDA
14
+ DML = ORT_CPU | FORCE_DML
15
+
16
+ # openvino
17
+ OPENVINO_AUTO = auto()
18
+ OPENVINO_CPU = auto()
19
+ OPENVINO_GPU = auto()
20
+ OPENVINO_NPU = auto()
21
+ OPENVINO_OPTIONAL_MULTI = auto()
22
+ OPENVINO_OPTIONAL_HETERO = auto()
23
+ OPENVINO_ALL = OPENVINO_CPU | OPENVINO_GPU | OPENVINO_NPU | OPENVINO_OPTIONAL_MULTI
24
+
25
+ def is_ort(self) -> bool:
26
+ return bool((InferProviders.ORT_CPU | InferProviders.CUDA | InferProviders.DML) & self)
27
+ def is_ov(self) -> bool:
28
+ return bool((InferProviders.OPENVINO_CPU | InferProviders.OPENVINO_GPU | InferProviders.OPENVINO_NPU | InferProviders.OPENVINO_AUTO) & self)
29
+
30
+ def get_onnx_provider(self) -> list[str]:
31
+ res = []
32
+ if self.FORCE_CUDA in self:
33
+ res.append("CUDAExecutionProvider")
34
+ if self.FORCE_DML in self:
35
+ res.append("DmlExecutionProvider")
36
+ if self.ORT_CPU in self:
37
+ res.append("CPUExecutionProvider")
38
+ return res
39
+
40
+ def get_openvino_device(self) -> str:
41
+ devices: list[str] = []
42
+ if self.OPENVINO_GPU in self:
43
+ # 现场找全
44
+ from openvino import Core # pyright: ignore[reportMissingImports]
45
+ devices.extend(filter(lambda name: name.startswith("GPU"), Core().available_devices))
46
+ if self.OPENVINO_NPU in self:
47
+ devices.append("NPU")
48
+ if self.OPENVINO_CPU in self:
49
+ devices.append("CPU")
50
+ devices_str = ",".join(devices)
51
+ if self.OPENVINO_AUTO in self:
52
+ if len(devices) != 0:
53
+ return f"AUTO:{devices_str}"
54
+ else:
55
+ return "AUTO"
56
+ elif self.OPENVINO_OPTIONAL_MULTI in self:
57
+ return f"MULTI:{devices_str}"
58
+ elif self.OPENVINO_OPTIONAL_HETERO in self:
59
+ return f"HETERO:{devices_str}"
60
+ else:
61
+ return devices[0]
62
+
63
+ @staticmethod
64
+ def default() -> "InferProviders":
65
+ available = InferProviders.currently_available()
66
+
67
+ if InferProviders.FORCE_CUDA in available:
68
+ return InferProviders.CUDA
69
+ elif InferProviders.FORCE_DML in available:
70
+ return InferProviders.DML
71
+ elif InferProviders.OPENVINO_CPU in available:
72
+ # 有时候自动的异构并行反而比纯 CPU 还慢,这里保守点
73
+ return InferProviders.OPENVINO_CPU
74
+ elif InferProviders.ORT_CPU in available:
75
+ return InferProviders.ORT_CPU
76
+ else:
77
+ raise InferEnvError("没有任意一个支持的推理引擎,请至少安装一个可选模块")
78
+
79
+ @staticmethod
80
+ def currently_available() -> "InferProviders":
81
+ from .infer_env import HAS_ONE, HAS_OPENVINO, HAS_ORT
82
+ res = InferProviders.NONE
83
+
84
+ if HAS_ORT:
85
+ import onnxruntime as ort # pyright: ignore[reportMissingImports]
86
+ available_providers = ort.get_available_providers()
87
+ if "CPUExecutionProvider" in available_providers:
88
+ res = res | InferProviders.ORT_CPU
89
+ if "CUDAExecutionProvider" in available_providers:
90
+ res = res | InferProviders.FORCE_CUDA
91
+ if "DmlExecutionProvider" in available_providers:
92
+ res = res | InferProviders.FORCE_DML
93
+ if HAS_OPENVINO:
94
+ from openvino import Core # pyright: ignore[reportMissingImports]
95
+ devices = Core().available_devices
96
+ res = res | InferProviders.OPENVINO_AUTO | InferProviders.OPENVINO_OPTIONAL_MULTI | InferProviders.OPENVINO_OPTIONAL_HETERO
97
+ if "CPU" in devices:
98
+ res = res | InferProviders.OPENVINO_CPU
99
+ if any(i.startswith("GPU") for i in devices):
100
+ res = res | InferProviders.OPENVINO_GPU
101
+ if "NPU" in devices:
102
+ res = res | InferProviders.OPENVINO_NPU
103
+ if not HAS_ONE:
104
+ raise InferEnvError("没有任意一个支持的推理引擎,请至少安装一个可选模块")
105
+
106
+ assert res != InferProviders.NONE, "我算是知道了,你们各个都身怀绝技"
107
+ res = res & ~InferProviders.NONE
108
+ return res
@@ -0,0 +1,210 @@
1
+ from collections.abc import Callable
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+ import soundfile
7
+ from numpy.typing import NDArray
8
+
9
+ from .audio.audio_utils import crossfade, reSR, rms_frame_match, split_by_max_len_with_overlap, split_by_silence
10
+ from .f0_utils import apply_rise_tone, build_f0extract_func, f0_to_mel, normalized_mel
11
+ from .type_alist import Audio, AudioLike, F0ExtractAlgorithm, F0ExtractAlgorithmList, FileLike
12
+
13
+ if TYPE_CHECKING:
14
+ from .RvcContext import RvcContext
15
+
16
+ class InferTask:
17
+ audios: list[Audio]
18
+ def __init__(self,
19
+ # -- 必填 --
20
+ context: "RvcContext",
21
+ vec_model: Path,
22
+ gen_model: tuple[Path, int],
23
+ *audios: AudioLike,
24
+
25
+ # 可选扩展
26
+ sid: int = 0,
27
+ seed: int | None = 1234,
28
+
29
+ # -- 特征索引 --
30
+ index_path: Path | None = None,
31
+ index_rate: float = 0.33,
32
+ index_k: int = 8, # 检索的最近特征数量
33
+
34
+ # -- f0 提取 --
35
+ f0extract_algorithm: F0ExtractAlgorithm,
36
+ f0_up_semitone: float = 0,
37
+ f0_min: float = 50,
38
+ f0_max: float = 1100,
39
+
40
+ # -- 强制切片与交叉淡化 --
41
+ slice_max_len: int = 30,
42
+ slice_overlap_len: int = 5,
43
+
44
+ # -- 静音切片 --
45
+ silence_frame_len: int = 20,
46
+ silence_hop_len: int = 10,
47
+ silence_thresh_db: float = -40,
48
+ silence_min_silence_duration_ms: float = 800.0,
49
+ silence_max_transition_ms: float = 100.0,
50
+
51
+ # -- RMS 包络匹配 --
52
+ rms_match_frame_len: int = 20,
53
+ rms_match_hop_len: int = 10,
54
+ rms_match_mix: float = 1.0, # 一般不用改
55
+ ) -> None:
56
+ self.context = context
57
+ self.vec = vec_model.resolve()
58
+ self.gen = (gen_model[0].resolve(), gen_model[1])
59
+ self.audio_likes = list(audios)
60
+ self.sid = sid
61
+ self.seed = seed
62
+ self.index_path = index_path
63
+ self.index_rate = index_rate
64
+ self.index_k = index_k
65
+ self.f0extract_algorithm = f0extract_algorithm
66
+ self.f0_up_semitone = f0_up_semitone
67
+ self.f0_min = f0_min
68
+ self.f0_max = f0_max
69
+ self.slice_max_len = slice_max_len
70
+ self.slice_overlap_len = slice_overlap_len
71
+ self.silence_frame_len = silence_frame_len
72
+ self.silence_hop_len = silence_hop_len
73
+ self.silence_thresh_db = silence_thresh_db
74
+ self.silence_min_silence_duration_ms = silence_min_silence_duration_ms
75
+ self.silence_max_transition_ms = silence_max_transition_ms
76
+ self.rms_match_frame_len = rms_match_frame_len
77
+ self.rms_match_hop_len = rms_match_hop_len
78
+ self.rms_match_mix = rms_match_mix
79
+
80
+ def read(self) -> None:
81
+ self.audios = []
82
+ for audio_like in self.audio_likes:
83
+ if isinstance(audio_like, tuple) and len(audio_like) == 2 and isinstance(audio_like[0], np.ndarray) and isinstance(audio_like[1], int):
84
+ self.audios.append(audio_like)
85
+ else:
86
+ assert not isinstance(audio_like, tuple) # 这个类型检查真的很奇怪有没有
87
+ data, sr = soundfile.read(
88
+ audio_like, dtype="float32"
89
+ )
90
+ data = data.astype(np.float32)
91
+ if len(data.shape) != 1:
92
+ data = data.mean(axis=1, dtype=np.float32)
93
+ self.audios.append((data, sr))
94
+ del self.audio_likes
95
+
96
+ def core_chunk_infer(self, audio: Audio, f0extract_func: Callable[[Audio, int], NDArray[np.float32]]) -> Audio:
97
+ model = self.context._vec_pool.get(self.vec)
98
+ feats = model.infer(audio)
99
+
100
+ f0 = f0extract_func(audio, feats.shape[0]) # 这里是没有批处理维度的
101
+ f0 = apply_rise_tone(f0, self.f0_up_semitone)
102
+
103
+ mel = normalized_mel(f0_to_mel(f0), f0_to_mel(self.f0_min), f0_to_mel(self.f0_max))
104
+ mel = np.rint(mel).astype(np.int64)
105
+
106
+ if self.index_rate > 1e-6 and self.index_path is not None and self.context._index_pool is not None:
107
+ index = self.context._index_pool.get(self.index_path)
108
+ feats = index.apply_index(feats, self.index_rate, self.index_k)
109
+
110
+ model = self.context._gen_pool.get(self.gen)
111
+ res = model.infer(
112
+ phone=feats,
113
+ f0_pitch=f0, mel_pitch=mel,
114
+ sid=self.sid, seed=self.seed
115
+ )
116
+
117
+ res_d, res_sr = res
118
+ source_d, source_sr = audio
119
+
120
+ source_len = len(source_d)
121
+ res_len = source_len * res_sr // source_sr
122
+
123
+ pad_len = res_len - len(res_d)
124
+ if pad_len == 0:
125
+ return res
126
+ elif pad_len < 0:
127
+ return (res_d[:res_len], res_sr)
128
+ else:
129
+ return (
130
+ np.pad(res_d, [0, pad_len], mode="constant"),
131
+ res_sr
132
+ )
133
+
134
+ def chunk_infer(self, audio: Audio, f0extract_func: Callable[[Audio, int], NDArray[np.float32]]) -> Audio:
135
+ splited = split_by_max_len_with_overlap( # 就算不足一个切片长也不会报错哒
136
+ audio,
137
+ max_len=self.slice_max_len,
138
+ overlap_len=self.slice_overlap_len
139
+ )
140
+ del audio # 节约点内存
141
+ def handle(c):
142
+ return self.core_chunk_infer(c, f0extract_func)
143
+ infer = [handle(i) for i in splited]
144
+ res = crossfade(
145
+ infer,
146
+ overlap_len=self.slice_overlap_len
147
+ )
148
+ return res
149
+
150
+ def gen_infer(self, callback: Callable[[int, Audio], None]) -> None:
151
+ assert self.f0extract_algorithm in F0ExtractAlgorithmList, f"{self.f0extract_algorithm} 不是受支持的 f0 提取算法"
152
+ func = build_f0extract_func(self.f0extract_algorithm, self.f0_min, self.f0_max) # pyright: ignore[reportArgumentType]
153
+ for i, audio in enumerate(self.audios):
154
+ splited = split_by_silence(
155
+ audio,
156
+ frame_len=self.silence_frame_len,
157
+ hop_len=self.silence_hop_len,
158
+ silence_thresh_db=self.silence_thresh_db,
159
+ min_silence_duration_ms=self.silence_min_silence_duration_ms,
160
+ max_transition_ms=self.silence_max_transition_ms
161
+ )
162
+ chunks: list[Audio] = []
163
+ for chunk, is_sil in splited:
164
+ if is_sil:
165
+ chunks.append(reSR(chunk, target_sr=self.gen[1])) # 内部判断相等会直接返回
166
+ else:
167
+ chunks.append(self.chunk_infer(chunk, func))
168
+ datas = [data for data, sr in chunks]
169
+ res = (np.concatenate(datas), self.gen[1])
170
+ res = rms_frame_match(
171
+ source=res,
172
+ target=reSR(audio, target_sr=res[1]),
173
+ frame_len=self.rms_match_frame_len,
174
+ hop_len=self.rms_match_hop_len,
175
+ mix=self.rms_match_mix
176
+ )
177
+ callback(i, res)
178
+
179
+ def run(self) -> list[Audio]:
180
+ self.read()
181
+
182
+ res = []
183
+ self.gen_infer(
184
+ lambda _, a: res.append(a)
185
+ )
186
+ return res
187
+
188
+ def run_and_save(self, *output: FileLike,
189
+ subtype: str | None = None,
190
+ format: str | None = None):
191
+ self.read()
192
+
193
+ if len(self.audios) != len(output):
194
+ raise ValueError(f"音频数量 {len(self.audios)} 与输出数量 {len(output)} 不匹配")
195
+ def save(i: int, a: Audio) -> None:
196
+ d, s = a
197
+ soundfile.write(output[i],
198
+ d, s,
199
+ subtype=subtype,
200
+ format=format)
201
+ self.gen_infer(
202
+ save
203
+ )
204
+
205
+ def run_and_callback(self,
206
+ callback: Callable[[int, Audio], None]) -> None:
207
+ self.read()
208
+ self.gen_infer(
209
+ callback=callback
210
+ )
@@ -0,0 +1,125 @@
1
+ import warnings
2
+ from pathlib import Path
3
+
4
+ from .index.RvcFeatIndex import RvcFeatIndex
5
+ from .InferProviders import InferProviders
6
+ from .InferTask import InferTask
7
+ from .onnx.ContentVec import ContentVec
8
+ from .onnx.ModelSimplePool import ModelSimplePool
9
+ from .onnx.RvcGen import RvcGen
10
+ from .path_utils import path
11
+ from .type_alist import AudioLike, F0ExtractAlgorithm, PathLike
12
+ from .warn.InferEnvWarn import InferEnvWarn
13
+
14
+
15
+ # 请注意,RvcContext 并不是线程安全的
16
+ class RvcContext:
17
+ def __init__(self,
18
+ providers: InferProviders = InferProviders.default(),
19
+ vec_pool_permanent_size: int = 1,
20
+ gen_pool_permanent_size: int = 2,
21
+ index_pool_permanent_size: int = 2) -> None:
22
+ self._providers = providers
23
+ self._vec_pool = ModelSimplePool[Path, ContentVec](
24
+ lambda p: ContentVec(p.resolve(), self._providers),
25
+ vec_pool_permanent_size
26
+ )
27
+ self._gen_pool = ModelSimplePool[tuple[Path, int], RvcGen](
28
+ lambda args: RvcGen(args[0].resolve(), args[1], self._providers),
29
+ gen_pool_permanent_size,
30
+ )
31
+
32
+ from .infer_env import HAS_FAISS
33
+ if HAS_FAISS:
34
+ self._index_pool = ModelSimplePool[Path, RvcFeatIndex](
35
+ lambda p: RvcFeatIndex(p.resolve()),
36
+ index_pool_permanent_size
37
+ )
38
+ else:
39
+ self._index_pool = None
40
+
41
+ def clear_pool(self) -> None:
42
+ self._vec_pool.clear()
43
+ self._gen_pool.clear()
44
+
45
+ def build_task(self,
46
+ # -- 必填 --
47
+ vec_model: PathLike,
48
+ gen_model: PathLike,
49
+ gen_model_sr: int,
50
+ *audios: AudioLike,
51
+
52
+ # 可选扩展
53
+ sid: int = 0,
54
+ seed: int | None = 1234,
55
+
56
+ # -- 特征索引 --
57
+ index_path: PathLike | None = None,
58
+ index_rate: float = 0.33,
59
+ index_k: int = 8, # 检索的最近特征数量
60
+
61
+ # -- f0 提取 --
62
+ f0extract_algorithm: F0ExtractAlgorithm = "dio",
63
+ f0_up_semitone: float = 0,
64
+ f0_min: float = 50,
65
+ f0_max: float = 1100,
66
+
67
+ # -- 强制切片与交叉淡化 --
68
+ slice_max_len: int = 30,
69
+ slice_overlap_len: int = 5,
70
+
71
+ # -- 静音切片 --
72
+ silence_frame_len: int = 20,
73
+ silence_hop_len: int = 10,
74
+ silence_thresh_db: float = -40,
75
+ silence_min_silence_duration_ms: float = 800.0,
76
+ silence_max_transition_ms: float = 100.0,
77
+
78
+ # -- RMS 包络匹配 --
79
+ rms_match_frame_len: int = 20,
80
+ rms_match_hop_len: int = 10,
81
+ rms_match_mix: float = 1.0, # 一般不用改
82
+ ) -> "InferTask":
83
+ vec = path(vec_model)
84
+ gen = path(gen_model)
85
+
86
+ if not vec.exists():
87
+ raise FileNotFoundError(f"找不到模型: {vec}")
88
+ if not gen.exists():
89
+ raise FileNotFoundError(f"找不到模型: {gen}")
90
+
91
+ if index_path is not None:
92
+ from .infer_env import HAS_FAISS
93
+ if not HAS_FAISS:
94
+ warnings.warn("您正在使用特征索引,但并未安装 faiss。如果您想使用特征索引请安装 [index] 可选依赖", InferEnvWarn)
95
+ index_path = None
96
+ else:
97
+ index_path = path(index_path)
98
+ if not index_path.exists():
99
+ raise FileNotFoundError(f"找不到特征索引: {index_path}")
100
+
101
+ return InferTask(
102
+ self,
103
+ vec,
104
+ (gen, gen_model_sr),
105
+ *audios,
106
+ sid=sid,
107
+ seed=seed,
108
+ index_path=index_path,
109
+ index_rate=index_rate,
110
+ index_k=index_k,
111
+ f0extract_algorithm=f0extract_algorithm,
112
+ f0_up_semitone=f0_up_semitone,
113
+ f0_min=f0_min,
114
+ f0_max=f0_max,
115
+ slice_max_len=slice_max_len,
116
+ slice_overlap_len=slice_overlap_len,
117
+ silence_frame_len=silence_frame_len,
118
+ silence_hop_len=silence_hop_len,
119
+ silence_thresh_db=silence_thresh_db,
120
+ silence_min_silence_duration_ms=silence_min_silence_duration_ms,
121
+ silence_max_transition_ms=silence_max_transition_ms,
122
+ rms_match_frame_len=rms_match_frame_len,
123
+ rms_match_hop_len=rms_match_hop_len,
124
+ rms_match_mix=rms_match_mix,
125
+ )
RvcPyInfer/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ from . import f0_utils
2
+ from ._version import __version__ # noqa: F401
3
+ from .audio import audio_utils
4
+ from .cli import main as ModelInferCLI
5
+ from .error.InferEnvError import InferEnvError
6
+ from .error.NotSupportedAlgorithmError import NotSupportedAlgorithmError
7
+ from .InferProviders import InferProviders
8
+ from .InferTask import InferTask
9
+ from .onnx.ContentVec import ContentVec
10
+ from .onnx.export import Exporter as OnnxExporter
11
+ from .onnx.export.cli import main as ModelExportToolCLI
12
+ from .onnx.export.direct_read_sr import direct_read_sr
13
+ from .onnx.ModelSimplePool import ModelSimplePool
14
+ from .onnx.RvcGen import RvcGen
15
+ from .RvcContext import RvcContext
16
+ from .type_alist import Audio, AudioLike, F0ExtractAlgorithm, F0ExtractAlgorithmList, FileLike, PathLike
17
+ from .warn.InferEnvWarn import InferEnvWarn
18
+ from .warn.InferModelWarn import InferModelWarn
19
+ from .warn.InferWarn import InferWarn
20
+
21
+ # 用户肯定不会希望我的 __version__ 去污染 ta 的版本号
22
+
23
+ __all__ = [
24
+ "Audio", "AudioLike", "PathLike", "FileLike", "F0ExtractAlgorithm", "F0ExtractAlgorithmList",
25
+ "InferEnvError",
26
+ "NotSupportedAlgorithmError",
27
+ "InferWarn",
28
+ "InferEnvWarn",
29
+ "InferModelWarn",
30
+ "f0_utils",
31
+ "audio_utils",
32
+ "InferProviders",
33
+ "RvcContext",
34
+ "InferTask",
35
+ "OnnxExporter",
36
+ "direct_read_sr",
37
+ "ModelExportToolCLI",
38
+ "ContentVec",
39
+ "RvcGen",
40
+ "ModelSimplePool",
41
+ "ModelInferCLI"
42
+ ]
RvcPyInfer/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main as infer_main
2
+
3
+ if __name__ == "__main__":
4
+ infer_main()
RvcPyInfer/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"