@memef1f1y/dsh-plugin-tts 0.6.0

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.
Files changed (44) hide show
  1. package/.github/workflows/test.yml +33 -0
  2. package/ANALYSIS.md +205 -0
  3. package/LICENSE +21 -0
  4. package/PR-REVIEW.md +75 -0
  5. package/README.md +297 -0
  6. package/README.zh.md +263 -0
  7. package/Snipaste_2026-08-16_13-28-10.png +0 -0
  8. package/Snipaste_2026-08-16_13-28-23.png +0 -0
  9. package/Snipaste_2026-08-16_13-28-37.png +0 -0
  10. package/Snipaste_2026-08-16_13-29-01.png +0 -0
  11. package/Snipaste_2026-08-16_13-29-11.png +0 -0
  12. package/TODO.md +23 -0
  13. package/cordis.patch.yml +4 -0
  14. package/docs/CLOUD-GUIDE.md +216 -0
  15. package/docs/COSYVOICE-GUIDE.md +216 -0
  16. package/docs/EDGE-GUIDE.md +166 -0
  17. package/docs/HOST-CONTRACT.md +36 -0
  18. package/docs/INDEX-TTS2-GUIDE.md +208 -0
  19. package/docs/RVC-GUIDE.md +390 -0
  20. package/docs/USER-GUIDE.md +334 -0
  21. package/docs/acceptance-checklist.md +148 -0
  22. package/docs/adaptive-chunked-playback.md +194 -0
  23. package/docs/macos-test-prompt.md +230 -0
  24. package/docs/windows-gpu-verify-prompt.md +151 -0
  25. package/lib/client.js +7036 -0
  26. package/lib/index.mjs +3288 -0
  27. package/logo.png +0 -0
  28. package/package.json +52 -0
  29. package/pnpm-workspace.yaml +7 -0
  30. package/rvc-server.py +564 -0
  31. package/tests/README.md +45 -0
  32. package/tests/client-load.mjs +393 -0
  33. package/tests/e2e-compact-index.mjs +86 -0
  34. package/tests/e2e-index-ab.mjs +138 -0
  35. package/tests/e2e-real-rvc.mjs +122 -0
  36. package/tests/i18n-keys.mjs +82 -0
  37. package/tests/mock-registry.mjs +50 -0
  38. package/tests/package-runtime-patch.mjs +88 -0
  39. package/tests/rvc-server-live.mjs +230 -0
  40. package/tests/smoke.mjs +1107 -0
  41. package/tools/gen-ui-preview.mjs +174 -0
  42. package/tools/mac-verify.sh +158 -0
  43. package/tools/make-pack.mjs +155 -0
  44. package/tools/package-runtime.py +214 -0
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python3
2
+ """package-runtime.py — build a portable RVC inference runtime for dsh-plugin-tts.
3
+
4
+ Strategy: copy the PROVEN package set from an existing RVC WebUI install
5
+ (runtime/ python + infer/ + hubert/rmvpe assets + ffmpeg), then optionally
6
+ upgrade torch/torchaudio to a CUDA build that supports the local GPU. This keeps
7
+ the dependency set that is known to work together and fixes the one weak point:
8
+ old torch builds (e.g. cu118 / torch 2.0) cannot run Blackwell (RTX 50-series,
9
+ sm_120) at full speed — cu128 builds include sm_120 kernels.
10
+
11
+ Works on Windows / Linux / macOS (--platform auto detects; the Linux/macOS
12
+ paths are written blind — no CI machine, test before shipping).
13
+
14
+ Result (portable folder):
15
+ <out>/
16
+ ├── runtime/ # copied python env (torch optionally upgraded)
17
+ ├── infer/ # RVC inference core
18
+ ├── assets/hubert/ # hubert_base.pt
19
+ ├── assets/rmvpe/ # rmvpe.pt
20
+ ├── ffmpeg(.exe) # copied when available
21
+ ├── rvc-server.py
22
+ └── 启动服务.bat / start-rvc-server.sh
23
+
24
+ Run rvc-server.py from the portable root — it auto-detects the env
25
+ (runtime/python) and the RVC core (infer/ next to it), no WebUI needed.
26
+
27
+ Usage (Windows):
28
+ python tools/package-runtime.py ^
29
+ --rvc-dir "E:\\AI\\RVC20240604Nvidia\\RVC20240604Nvidia" ^
30
+ --out "D:\\rvc-portable" [--torch cu128] [--torch-version 2.7.0]
31
+ Usage (Linux/macOS):
32
+ python3 tools/package-runtime.py --rvc-dir /opt/RVC --out ~/rvc-portable --skip-torch
33
+ """
34
+ import argparse
35
+ import os
36
+ import shutil
37
+ import subprocess
38
+ import sys
39
+
40
+
41
+ def detect_platform(value):
42
+ if value and value != "auto":
43
+ return value
44
+ if sys.platform.startswith("win"):
45
+ return "windows"
46
+ if sys.platform.startswith("darwin"):
47
+ return "darwin"
48
+ return "linux"
49
+
50
+
51
+ def dir_gb(p):
52
+ if not os.path.isdir(p):
53
+ return 0.0
54
+ total = 0
55
+ for root, _, files in os.walk(p):
56
+ for f in files:
57
+ try:
58
+ total += os.path.getsize(os.path.join(root, f))
59
+ except OSError:
60
+ pass
61
+ return total / (1024 ** 3)
62
+
63
+
64
+ def copytree(src, dst):
65
+ if os.path.exists(dst):
66
+ shutil.rmtree(dst, ignore_errors=True)
67
+ shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
68
+
69
+
70
+ def patch_infer_for_pyav(infer_dir):
71
+ """macOS-only: make RVC's infer/lib/audio.py work with modern PyAV.
72
+
73
+ The RVC infer code calls ``av.open(fp, "rb")`` / ``av.open(fp, "wb", ...)``.
74
+ Old PyAV (bundled by the Windows RVC WebUI runtime) accepted those modes,
75
+ but every PyAV >= 10 rejects them for file-like objects (only 'r'/'w'/None
76
+ are allowed), and macOS arm64 wheels only exist for these newer versions.
77
+ Without this fix /convert fails with "ValueError: mode must be 'r', 'w',
78
+ or None, got: rb". This rewrites the copied (packaged) audio.py in place;
79
+ it never touches the user's RVC install.
80
+ """
81
+ audio = os.path.join(infer_dir, "lib", "audio.py")
82
+ if not os.path.exists(audio):
83
+ print("[package] (no infer/lib/audio.py — skip PyAV patch)", flush=True)
84
+ return
85
+ with open(audio, "r", encoding="utf-8") as f:
86
+ src = f.read()
87
+ new = src.replace('av.open(i, "rb")', 'av.open(i, "r")').replace(
88
+ 'av.open(o, "wb", format=format)', 'av.open(o, "w", format=format)'
89
+ )
90
+ if new == src:
91
+ print("[package] (infer/lib/audio.py already PyAV-compatible)", flush=True)
92
+ return
93
+ with open(audio, "w", encoding="utf-8") as f:
94
+ f.write(new)
95
+ print("[package] patched infer/lib/audio.py: av.open 'rb'->'r', 'wb'->'w'", flush=True)
96
+
97
+
98
+ def step(msg, fn):
99
+ print("[package] %s ..." % msg, flush=True)
100
+ fn()
101
+ print("[package] done", flush=True)
102
+
103
+
104
+ def main():
105
+ ap = argparse.ArgumentParser(description="Build a portable RVC runtime for dsh-plugin-tts")
106
+ ap.add_argument("--rvc-dir", required=True, help="existing RVC WebUI install root")
107
+ ap.add_argument("--out", default="rvc-portable", help="output portable folder")
108
+ ap.add_argument("--platform", default="auto", choices=["auto", "windows", "linux", "darwin"],
109
+ help="target platform (auto = this machine)")
110
+ ap.add_argument("--skip-torch", action="store_true",
111
+ help="skip the torch upgrade (portable works with the copied torch; "
112
+ "cu128 is only needed for full speed on RTX 50-series/Blackwell)")
113
+ ap.add_argument("--torch", default="cu128", choices=["cu118", "cu121", "cu124", "cu126", "cu128"])
114
+ ap.add_argument("--torch-version", default="2.7.0")
115
+ ap.add_argument("--index-url", default="https://download.pytorch.org/whl",
116
+ help="pytorch wheel index (use a mirror if the official CDN is slow, "
117
+ "e.g. https://mirrors.aliyun.com/pytorch-wheels)")
118
+ a = ap.parse_args()
119
+
120
+ platform = detect_platform(a.platform)
121
+ is_win = platform == "windows"
122
+ print("[package] target platform: %s" % platform, flush=True)
123
+
124
+ rvc = os.path.abspath(a.rvc_dir)
125
+ out = os.path.abspath(a.out)
126
+ runtime = os.path.join(rvc, "runtime")
127
+ if not os.path.isdir(os.path.join(rvc, "infer")):
128
+ sys.exit("ERROR: %s is not an RVC install (no infer/)" % rvc)
129
+ if not os.path.isdir(runtime):
130
+ sys.exit("ERROR: %s has no runtime/ python" % rvc)
131
+
132
+ py_rel = os.path.join("runtime", "python.exe") if is_win else os.path.join("runtime", "bin", "python")
133
+
134
+ os.makedirs(out, exist_ok=True)
135
+ torch_spec = "%s+%s" % (a.torch_version, a.torch)
136
+
137
+ step("copy runtime (%.1f GB)" % dir_gb(runtime), lambda: copytree(runtime, os.path.join(out, "runtime")))
138
+ step("copy infer/", lambda: copytree(os.path.join(rvc, "infer"), os.path.join(out, "infer")))
139
+ # macOS-only: the packaged infer must tolerate modern PyAV (see the helper).
140
+ if platform == "darwin":
141
+ step("patch infer/lib/audio.py for modern PyAV (macOS)", lambda: patch_infer_for_pyav(os.path.join(out, "infer")))
142
+ if os.path.isdir(os.path.join(rvc, "assets", "hubert")):
143
+ step("copy assets/hubert", lambda: copytree(os.path.join(rvc, "assets", "hubert"), os.path.join(out, "assets", "hubert")))
144
+ if os.path.isdir(os.path.join(rvc, "assets", "rmvpe")):
145
+ step("copy assets/rmvpe", lambda: copytree(os.path.join(rvc, "assets", "rmvpe"), os.path.join(out, "assets", "rmvpe")))
146
+
147
+ # ffmpeg: Windows uses the RVC-bundled ffmpeg.exe; elsewhere try the system
148
+ # binary so the folder stays self-contained, otherwise fall back to PATH.
149
+ ffmpeg_src = os.path.join(rvc, "ffmpeg.exe") if is_win else shutil.which("ffmpeg")
150
+ if ffmpeg_src and os.path.exists(ffmpeg_src):
151
+ fname = os.path.basename(ffmpeg_src)
152
+ step("copy %s" % fname, lambda: shutil.copy2(ffmpeg_src, os.path.join(out, fname)))
153
+ else:
154
+ print("[package] WARN: no bundled ffmpeg found — rvc-server will fall back to system ffmpeg", flush=True)
155
+
156
+ here = os.path.dirname(os.path.abspath(__file__))
157
+ server_src = os.path.join(os.path.dirname(here), "rvc-server.py")
158
+ if os.path.exists(server_src):
159
+ step("copy rvc-server.py", lambda: shutil.copy2(server_src, os.path.join(out, "rvc-server.py")))
160
+ else:
161
+ print("[package] WARN: rvc-server.py not found next to tools/ — copy it manually", flush=True)
162
+
163
+ py = os.path.join(out, py_rel)
164
+ if not os.path.exists(py):
165
+ print("[package] WARN: %s not found — check the runtime layout for platform %s" % (py_rel, platform), flush=True)
166
+
167
+ # torch upgrade: pointless on macOS (no NVIDIA CUDA); auto-skip with a note.
168
+ if platform == "darwin" and not a.skip_torch:
169
+ print("[package] note: skipping torch upgrade on macOS (no CUDA); the copied torch is kept", flush=True)
170
+ elif a.skip_torch:
171
+ print("[package] skipping torch upgrade (--skip-torch); the portable runtime "
172
+ "works with the copied torch (cu128 only matters for RTX 50-series speed)", flush=True)
173
+ else:
174
+ def upgrade_torch():
175
+ # PyPI stays the primary index (for torch's deps like typing-extensions);
176
+ # the pytorch index is added for the pinned +cu128 build. The local
177
+ # version tag (==2.7.0+cu128) only matches cu128 wheels, so pip cannot
178
+ # pick the CPU build from PyPI.
179
+ cmd = [py, "-m", "pip", "install", "--upgrade", "--no-input",
180
+ "--extra-index-url", "%s/%s" % (a.index_url.rstrip("/"), a.torch),
181
+ "torch==%s" % torch_spec, "torchaudio==%s" % torch_spec]
182
+ print("[package] running: %s" % " ".join(cmd), flush=True)
183
+ subprocess.check_call(cmd)
184
+ step("upgrade torch -> %s" % torch_spec, upgrade_torch)
185
+
186
+ if is_win:
187
+ launcher = os.path.join(out, "启动服务.bat")
188
+ with open(launcher, "w", encoding="gbk", errors="replace") as f:
189
+ f.write("@echo off\r\n"
190
+ "cd /d \"%~dp0\"\r\n"
191
+ "echo Starting dsh-plugin-tts RVC server on http://127.0.0.1:4892 ...\r\n"
192
+ "start \"dsh-tts-rvc\" \"%~dp0runtime\\python.exe\" \"%~dp0rvc-server.py\" --port 4892\r\n")
193
+ print("[package] wrote %s" % launcher, flush=True)
194
+ else:
195
+ launcher = os.path.join(out, "start-rvc-server.sh")
196
+ with open(launcher, "w", encoding="utf-8") as f:
197
+ f.write("#!/bin/sh\n"
198
+ "cd \"$(dirname \"$0\")\"\n"
199
+ "echo 'Starting dsh-plugin-tts RVC server on http://127.0.0.1:4892 ...'\n"
200
+ "exec \"$PWD/runtime/bin/python\" \"$PWD/rvc-server.py\" --port 4892\n")
201
+ os.chmod(launcher, 0o755)
202
+ print("[package] wrote %s (chmod +x)" % launcher, flush=True)
203
+
204
+ if os.path.exists(py):
205
+ print("[package] verifying torch...", flush=True)
206
+ subprocess.check_call([py, "-c",
207
+ "import torch; print('torch', torch.__version__, 'cuda', torch.cuda.is_available(), "
208
+ "'cap', torch.cuda.get_device_capability(0) if torch.cuda.is_available() else None)"])
209
+ total = dir_gb(out)
210
+ print("[package] OK. Portable runtime ready: %s (%.1f GB)" % (out, total), flush=True)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()