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,26 @@
1
+ from collections import OrderedDict
2
+ from collections.abc import Callable
3
+
4
+
5
+ class ModelSimplePool[TKey, TModel]:
6
+ def __init__(self, factory: Callable[[TKey], TModel], permanent_size: int = 1) -> None:
7
+ self._factory = factory
8
+ self._pools = OrderedDict[TKey, TModel]()
9
+ self._permanent_size = permanent_size
10
+
11
+ def remove(self, key: TKey) -> None:
12
+ del self._pools[key]
13
+
14
+ def clear(self) -> None:
15
+ self._pools.clear()
16
+
17
+ def get(self, key: TKey) -> TModel:
18
+ model = self._pools.get(key)
19
+ if model is not None:
20
+ self._pools.move_to_end(key)
21
+ else:
22
+ model = self._factory(key)
23
+ self._pools[key] = model
24
+ if len(self._pools) > self._permanent_size:
25
+ self._pools.popitem(last=False)
26
+ return model
@@ -0,0 +1,51 @@
1
+ import numpy as np
2
+ from numpy.typing import NDArray
3
+
4
+ from ..error.InferEnvError import InferEnvError
5
+ from ..InferProviders import InferProviders
6
+ from ..path_utils import path as pathf
7
+ from ..type_alist import Audio, PathLike
8
+ from .model_loader import load_model
9
+
10
+
11
+ class RvcGen:
12
+ def __init__(self, path: PathLike, model_out_sr: int, providers: InferProviders) -> None:
13
+ self.sr = model_out_sr
14
+ self.session, self.compiled_model = load_model(pathf(path), providers)
15
+ if self.session is not None:
16
+ self.channels = next(filter(lambda arg: arg.name == "rnd", self.session.get_inputs())).shape[1]
17
+ elif self.compiled_model is not None:
18
+ self.channels = next(filter(lambda arg: arg.any_name == "rnd", self.compiled_model.inputs)).partial_shape[1].get_length()
19
+
20
+ def infer(self, phone: NDArray[np.float32], mel_pitch: NDArray[np.int64], f0_pitch: NDArray[np.float32], sid: int = 0, seed: int | None = 1234) -> Audio:
21
+ assert phone.shape[0] == mel_pitch.shape[0] == f0_pitch.shape[0], "帧数应统一"
22
+ phone_len = phone.shape[0]
23
+ ds = np.array([sid], dtype=np.int64)
24
+ rnd = np.random.default_rng(seed).standard_normal((1, self.channels, phone_len), dtype=np.float32)
25
+ phone = phone[None, ...]
26
+ mel_pitch = mel_pitch.reshape(1, -1)
27
+ f0_pitch = f0_pitch.reshape(1, -1)
28
+
29
+ model_input = {
30
+ "rnd": rnd,
31
+ "ds": ds,
32
+ "phone": phone,
33
+ "phone_lengths": np.array([phone_len]),
34
+ "pitch": mel_pitch,
35
+ "pitchf": f0_pitch
36
+ }
37
+ if self.session is not None:
38
+ output = self.session.run(
39
+ output_names=["audio"],
40
+ input_feed=model_input
41
+ )[0]
42
+ elif self.compiled_model is not None:
43
+ infer_request = self.compiled_model.create_infer_request()
44
+ result_dict = infer_request.infer(inputs=model_input)
45
+ output = result_dict["audio"]
46
+ del infer_request, result_dict
47
+ else:
48
+ raise InferEnvError("没有任意一个支持的推理引擎,请至少安装一个可选模块")
49
+ # (1, sampling)
50
+ assert isinstance(output, np.ndarray), "输出应为 NDArray"
51
+ return output.squeeze(axis=1).squeeze(axis=0).astype(np.float32), self.sr
@@ -0,0 +1,130 @@
1
+ import hashlib
2
+ import json
3
+ import os
4
+ import random
5
+ import re
6
+ import subprocess
7
+ import uuid
8
+ from datetime import datetime
9
+ from importlib.resources import files
10
+
11
+ from ...path_utils import path
12
+ from ...type_alist import PathLike
13
+
14
+ const_tips = """# ----------------------------------------------------------------
15
+ # 这是自动生成的临时脚本,若您看见工具未能正常删除的话,您可以手动删除它
16
+ # 您当然可以尝试运行了,甚至保存下来,脚本引用的是绝对路径
17
+ # ----------------------------------------------------------------
18
+
19
+ """
20
+
21
+ class Exporter:
22
+ def __init__(self, root: PathLike, model: PathLike, export_target: PathLike, runtime: PathLike | None = None) -> None:
23
+ self.root = path(root).resolve()
24
+ self.model = path(model).resolve()
25
+ self.target = path(export_target).resolve()
26
+ if runtime is None:
27
+ self.runtime = self.root / "runtime" / "python.exe"
28
+ else:
29
+ self.runtime = path(runtime).resolve()
30
+ self.parse_prefix = self.build_parse_prefix()
31
+ template = files("RvcPyInfer.resource.template").joinpath("export_onnx.py").read_text(encoding="utf-8")
32
+ self.template_digest = hashlib.sha256(template.encode("utf-8"))
33
+ self.script = self.format_template(template)
34
+
35
+ @staticmethod
36
+ def build_parse_prefix() -> str:
37
+ alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
38
+ tmp: list[str] = []
39
+ for _ in range(8):
40
+ tmp.append(alphabet[random.randint(0, len(alphabet) - 1)])
41
+ return "".join(tmp)
42
+
43
+ def format_template(self, template: str) -> str:
44
+ model = str(self.model).replace("\\", "/")
45
+ target = str(self.target).replace("\\", "/")
46
+ root = str(self.root).replace("\\", "/")
47
+ template = template.replace("/./..{code.InsertionPoint.0}.././", f'{model}')
48
+ template = template.replace("/./..{code.InsertionPoint.1}.././", target)
49
+ template = template.replace("/./..{code.InsertionPoint.2}.././", self.parse_prefix)
50
+ template = template.replace("/./..{code.InsertionPoint.3}.././", root)
51
+
52
+ args_digest = hashlib.sha256(f"model: {model}, target: {target}, root: {root}".encode())
53
+
54
+ gen_info = [
55
+ "\n"
56
+ "# ----------------------------------------------------------------\n"
57
+ "# -------- 脚本元信息 --------\n"
58
+ f"# 该脚本生成于: {datetime.now()}\n",
59
+ f"# 模板摘要: {self.template_digest.hexdigest()}\n"
60
+ f"# 非随机参数摘要: {args_digest.hexdigest()}\n"
61
+ f"# 模型: {model}\n"
62
+ f"# 目标: {target}\n"
63
+ f"# Rvc 项目根目录: {root}\n"
64
+ f"# 解析前缀: {self.parse_prefix}\n"
65
+ f"# 系统执行所用的 runtime: {self.runtime}\n"
66
+ "# ----------------------------------------------------------------\n"
67
+ "\n"
68
+ ]
69
+
70
+ gen_info = ''.join(gen_info)
71
+
72
+ return const_tips + gen_info + template
73
+
74
+ def parse_stdout(self, stdout: str) -> dict[str, str | int]:
75
+ lines = stdout.split('\n')
76
+ info = {}
77
+ for line in lines:
78
+ line = line.strip()
79
+ if line.startswith(self.parse_prefix):
80
+ match = re.match(r'^\/\.\/\.\.(.*)\.\.\/\.\/$', line[len(self.parse_prefix):])
81
+ if match is None:
82
+ continue
83
+ else:
84
+ json_info = match.group(1)
85
+ info |= json.loads(json_info)
86
+ return info
87
+
88
+ def export(self) -> None:
89
+ tmp_py_path = (self.root / str(uuid.uuid4())).with_suffix(".py")
90
+ with open(tmp_py_path, mode="+w", encoding="utf-8") as f:
91
+ f.write(self.script)
92
+ try:
93
+ result = subprocess.run(
94
+ args=[str(self.runtime), str(tmp_py_path)],
95
+ capture_output=True,
96
+ text=True,
97
+ timeout=120,
98
+ cwd=self.root
99
+ )
100
+ finally:
101
+ try:
102
+ if tmp_py_path.exists():
103
+ os.remove(tmp_py_path)
104
+ except Exception:
105
+ ...
106
+ info = self.parse_stdout(result.stdout)
107
+ sr = info.get("sr")
108
+ if sr is not None:
109
+ print(f"模型采样率: {sr}")
110
+
111
+ if result.returncode == 0:
112
+ print("导出成功!")
113
+ return
114
+ else:
115
+ msg = info.get("msg")
116
+ if msg is None:
117
+ print("导出失败")
118
+ else:
119
+ if isinstance(msg, str):
120
+ msg = msg.replace("\\\"", "\"").replace("\\n", "\n")
121
+ print(f"导出失败: {msg}")
122
+ print("-------- stdout --------")
123
+ print(result.stdout)
124
+ print("------------------------")
125
+ print()
126
+ print("-------- stderr --------")
127
+ print(result.stderr)
128
+ print("------------------------")
129
+ print()
130
+ print(f"return code: {result.returncode}")
@@ -0,0 +1,104 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from .direct_read_sr import direct_read_sr
6
+ from .Exporter import Exporter
7
+
8
+
9
+ def export_command(args) -> None:
10
+ """处理 export 子命令的具体逻辑"""
11
+ root_path = Path(args.root).resolve()
12
+ model_path = Path(args.model).resolve()
13
+ target_path = Path(args.target).resolve()
14
+ runtime_path = Path(args.runtime).resolve() if args.runtime else None
15
+
16
+ # 前置检查
17
+ if not root_path.is_dir():
18
+ print(f"[错误] 根目录不存在或不是文件夹: {root_path}")
19
+ sys.exit(1)
20
+
21
+ if not model_path.is_file():
22
+ print(f"[错误] 找不到模型文件: {model_path}")
23
+ sys.exit(1)
24
+
25
+ # 确保输出目录存在
26
+ if not target_path.parent.exists():
27
+ print(f"[提示] 创建输出目录: {target_path.parent}")
28
+ target_path.parent.mkdir(parents=True, exist_ok=True)
29
+
30
+ # 执行导出
31
+ try:
32
+ print(">>> 开始初始化导出器...")
33
+ exporter = Exporter(
34
+ root=root_path,
35
+ model=model_path,
36
+ export_target=target_path,
37
+ runtime=runtime_path
38
+ )
39
+
40
+ print(">>> 正在执行导出流程,请耐心等待...")
41
+ exporter.export()
42
+
43
+ except FileNotFoundError as e:
44
+ print(f"\n[错误] 找不到必要的文件: {e}")
45
+ print("请检查模板文件 (export_onnx.py) 是否存在于 resource/template 目录下,或运行时路径是否正确。")
46
+ sys.exit(1)
47
+ except Exception as e:
48
+ print(f"\n[错误] 导出过程中发生未预期的异常: {e}")
49
+ import traceback
50
+ traceback.print_exc()
51
+ sys.exit(1)
52
+
53
+ def show_sr_command(args) -> None:
54
+ """处理 show-sr 子命令的具体逻辑"""
55
+ model_path = Path(args.model).resolve()
56
+
57
+ if not model_path.is_file():
58
+ print(f"[错误] 找不到模型文件: {model_path}")
59
+ sys.exit(1)
60
+
61
+ sr = direct_read_sr(model_path)
62
+ print(f"模型采样率是: {sr}")
63
+
64
+ def main() -> None:
65
+ # 主解析器
66
+ parser = argparse.ArgumentParser(
67
+ description="RvcPyInfer 命令行工具",
68
+ formatter_class=argparse.RawTextHelpFormatter
69
+ )
70
+
71
+ # 创建子命令解析器
72
+ subparsers = parser.add_subparsers(dest="command", help="可用的子命令", required=True)
73
+
74
+ # 注册 export 子命令
75
+ export_parser = subparsers.add_parser(
76
+ "export",
77
+ help="导出 RVC 模型为 ONNX 格式",
78
+ description="将 RVC .pth 模型导出为 ONNX 格式"
79
+ )
80
+ # export 子命令的参数
81
+ export_parser.add_argument("-r", "--root", type=str, required=True, help="RVC 项目根目录路径")
82
+ export_parser.add_argument("-m", "--model", type=str, required=True, help="输入的 RVC 模型路径")
83
+ export_parser.add_argument("-t", "--target", type=str, required=True, help="导出的 ONNX 模型目标路径")
84
+ export_parser.add_argument("--runtime", type=str, default=None, help="自定义 Python 运行时路径 (默认: <root>/runtime/python.exe)")
85
+ # 绑定执行函数
86
+ export_parser.set_defaults(func=export_command)
87
+
88
+ # 注册 show-sr 子命令
89
+ show_sr_parser = subparsers.add_parser(
90
+ "show-sr",
91
+ help="直接查看 RVC 模型的输出采样率",
92
+ description="查看 RVC 模型的输出采样率"
93
+ )
94
+ show_sr_parser.add_argument("-m", "--model", type=str, required=True, help="输入的 RVC 模型路径")
95
+ show_sr_parser.set_defaults(func=show_sr_command)
96
+
97
+ # 解析参数
98
+ args = parser.parse_args()
99
+
100
+ # 调用对应的处理函数
101
+ args.func(args)
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -0,0 +1,109 @@
1
+ import collections
2
+ import io
3
+ import pickle
4
+ import zipfile
5
+ from typing import Any
6
+
7
+ from ...type_alist import PathLike
8
+
9
+
10
+ class DroppedObject:
11
+ """
12
+ 哨兵值:静默吸收所有参数和状态,代表被丢弃的不安全对象。
13
+ 采用单例模式,所有被丢弃的对象都将指向这同一个实例。
14
+ """
15
+ _instance = None
16
+
17
+ def __new__(cls, *args: Any, **kwargs: Any) -> "DroppedObject":
18
+ if cls._instance is None:
19
+ cls._instance = super().__new__(cls)
20
+ return cls._instance
21
+
22
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
23
+ # 吸收所有 pickle 传入的构造参数
24
+ pass
25
+
26
+ def __setstate__(self, state: Any) -> None:
27
+ # 吸收所有 pickle 传入的属性状态
28
+ pass
29
+
30
+ def __repr__(self) -> str:
31
+ return "<DroppedObject>"
32
+
33
+ def __bool__(self) -> bool:
34
+ # 方便在 if 判断中识别:if val is DROPPED: ...
35
+ return False
36
+
37
+ class SafeStandardUnpickler(pickle.Unpickler):
38
+ """遇到非白名单类型时,静默替换为 DroppedObject 哨兵"""
39
+
40
+ SAFE_CLASSES = {
41
+ 'builtins': {
42
+ v.__name__: v for v in [
43
+ type(None), type(...), type(NotImplemented),
44
+ int, float, complex, bool, str, bytes, bytearray, memoryview,
45
+ list, tuple, dict, set, frozenset, range, slice, object
46
+ ]
47
+ },
48
+ 'collections': {
49
+ 'OrderedDict': collections.OrderedDict,
50
+ 'defaultdict': collections.defaultdict,
51
+ 'deque': collections.deque,
52
+ 'Counter': collections.Counter,
53
+ },
54
+ }
55
+
56
+ def find_class(self, module: str, name: str):
57
+ if module in self.SAFE_CLASSES and name in self.SAFE_CLASSES[module]:
58
+ return self.SAFE_CLASSES[module][name]
59
+
60
+ # 静默丢弃:返回哨兵类,不抛出异常
61
+ return DroppedObject
62
+
63
+ def persistent_load(self, pid: Any) -> DroppedObject:
64
+ """
65
+ PyTorch 用 persistent_id 机制存储 tensor 的 storage。
66
+ 我们不需要 tensor 数据,直接返回哨兵值。
67
+ """
68
+ return DroppedObject()
69
+
70
+ def safe_load_pth(filepath: PathLike) -> Any:
71
+ """
72
+ 安全加载 PyTorch .pth/.pt 文件。
73
+ 自动处理 ZIP 格式(PyTorch 1.6+)和旧版原始 pickle 格式。
74
+ """
75
+ with open(filepath, 'rb') as f:
76
+ magic = f.read(2)
77
+ f.seek(0)
78
+
79
+ if magic == b'PK':
80
+ # ===== ZIP 格式(PyTorch 1.6+)=====
81
+ # .pth 文件本质是一个 ZIP,里面包含:
82
+ # archive/data.pkl ← 结构信息(我们要的)
83
+ # archive/data/0 ← tensor 原始数据(不需要)
84
+ with zipfile.ZipFile(filepath, 'r') as zf:
85
+ # 找到 .pkl 文件
86
+ pkl_names = [n for n in zf.namelist()
87
+ if n.endswith('.pkl')]
88
+ if not pkl_names:
89
+ raise ValueError(
90
+ f"ZIP 格式的 .pth 文件中未找到 .pkl 文件,"
91
+ f"包含的文件: {zf.namelist()}"
92
+ )
93
+ pkl_data = zf.read(pkl_names[0])
94
+ return SafeStandardUnpickler(io.BytesIO(pkl_data)).load()
95
+ else:
96
+ # ===== 旧版原始 pickle 格式 =====
97
+ return SafeStandardUnpickler(f).load()
98
+
99
+ def direct_read_sr(model: PathLike) -> int:
100
+ loaded_data = safe_load_pth(model)
101
+ config = loaded_data.get("config")
102
+ if config is None:
103
+ raise ValueError("找不到 config")
104
+ if len(config) < 18:
105
+ raise ValueError("config 长度不足 18")
106
+ sr = config[17]
107
+ if not isinstance(sr, int):
108
+ raise ValueError("采样率不是整数")
109
+ return sr
@@ -0,0 +1,41 @@
1
+ import warnings
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..error.InferEnvError import InferEnvError
6
+ from ..InferProviders import InferProviders
7
+ from ..warn.InferModelWarn import InferModelWarn
8
+
9
+ if TYPE_CHECKING:
10
+ from ..infer_env import HAS_OPENVINO, HAS_ORT
11
+ if HAS_ORT:
12
+ import onnxruntime as ort # pyright: ignore[reportMissingImports]
13
+ if HAS_OPENVINO:
14
+ import openvino # pyright: ignore[reportMissingImports]
15
+
16
+ def load_model(path: Path, providers: InferProviders) -> "tuple[ort.InferenceSession | None, openvino.CompiledModel | None]": # pyright: ignore[reportAttributeAccessIssue]
17
+ session = None
18
+ compiled_model = None
19
+ if providers.is_ort():
20
+ import onnxruntime as ort # pyright: ignore[reportMissingImports]
21
+ session = ort.InferenceSession(
22
+ path.with_suffix(".onnx"), providers=providers.get_onnx_provider()
23
+ )
24
+ elif providers.is_ov():
25
+ from openvino import convert_model, save_model # pyright: ignore[reportMissingImports]
26
+
27
+ from ..ov.OVCoreSingleton import core
28
+ if (path.parent / path.stem).with_suffix(".xml").exists():
29
+ model = core.read_model(str((path.parent / path.stem).with_suffix(".xml").resolve()))
30
+ compiled_model = core.compile_model(model=model, device_name=providers.get_openvino_device())
31
+ elif (path.parent / path.stem).with_suffix(".onnx").exists():
32
+ warnings.warn("您正在尝试使用 OpenVINO 运行 onnx,模型将自动转换并存档", InferModelWarn)
33
+ model = convert_model(str((path.parent / path.stem).with_suffix(".onnx").resolve()))
34
+ save_model(model, str((path.parent / path.stem).with_suffix(".xml").resolve()), compress_to_fp16=False)
35
+ compiled_model = core.compile_model(model=model, device_name=providers.get_openvino_device())
36
+ else:
37
+ raise FileNotFoundError(f"找不到模型: {path.parent / path.stem}")
38
+ else:
39
+ raise InferEnvError("没有任意一个支持的推理引擎或提供者 Flag 错误,请至少安装一个可选模块")
40
+
41
+ return session, compiled_model
@@ -0,0 +1,3 @@
1
+ import openvino as ov # pyright: ignore[reportMissingImports]
2
+
3
+ core = ov.Core()
@@ -0,0 +1,10 @@
1
+ from pathlib import Path
2
+
3
+ from .type_alist import PathLike
4
+
5
+
6
+ def path(path: PathLike) -> Path:
7
+ if isinstance(path, str):
8
+ return Path(path)
9
+ else:
10
+ return path