@memef1f1y/dsh-plugin-tts 0.6.5 → 0.6.7
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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +51 -51
- package/.github/ISSUE_TEMPLATE/config.yml +2 -2
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -28
- package/.github/pull_request_template.md +7 -7
- package/.github/workflows/contrib-triage.yml +66 -66
- package/.github/workflows/linked-issue.yml +21 -21
- package/.github/workflows/publish.yml +63 -0
- package/.github/workflows/stale.yml +30 -30
- package/.github/workflows/test.yml +33 -33
- package/.github/workflows/welcome.yml +22 -22
- package/ANALYSIS.md +205 -205
- package/PR-REVIEW.md +74 -74
- package/README.md +335 -335
- package/TODO.md +34 -34
- package/docs/HOST-CONTRACT.md +36 -36
- package/docs/RVC-GUIDE.md +390 -390
- package/docs/USER-GUIDE.md +389 -389
- package/docs/acceptance-checklist.md +147 -147
- package/docs/adaptive-chunked-playback.md +194 -194
- package/docs/macos-test-prompt.md +230 -230
- package/docs/windows-gpu-verify-prompt.md +151 -151
- package/lib/client.js +7164 -7164
- package/lib/index.mjs +3374 -3374
- package/package.json +51 -51
- package/rvc-server.py +564 -564
- package/tests/README.md +45 -45
- package/tests/client-load.mjs +406 -406
- package/tests/e2e-compact-index.mjs +86 -86
- package/tests/e2e-index-ab.mjs +138 -138
- package/tests/e2e-real-rvc.mjs +122 -122
- package/tests/i18n-keys.mjs +82 -82
- package/tests/package-runtime-patch.mjs +88 -88
- package/tests/rvc-server-live.mjs +230 -230
- package/tests/smoke.mjs +1284 -1284
- package/tools/gen-ui-preview.mjs +174 -174
- package/tools/mac-verify.sh +158 -158
- package/tools/package-runtime.py +214 -214
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
# 自适应分块渐进播放方案(Adaptive Chunked Progressive Playback)
|
|
2
|
-
|
|
3
|
-
> RVC 长文本朗读的"先全部转换、后整体播放"改造成"边转换、边播放"的设计与实现说明。
|
|
4
|
-
> 核心思想:**性能开销摊在用户本机**,不同机器自动校准块大小与预热数量,做到"即点即听、长文不卡顿"。
|
|
5
|
-
|
|
6
|
-
## 1. 问题
|
|
7
|
-
|
|
8
|
-
RVC 是**变声**而非 TTS:输入音频长度 = 输出音频长度,所以"长文本朗读"必须
|
|
9
|
-
先把整段文本用 Edge TTS 合成底噪,再交给 RVC 转换。旧链路是:
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
整段文本 → Edge 合成整段底噪 → RVC 整段转换 → 返回 → 播放
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
对短句(≤12 秒音频)这是最优解;但对长回复(数百字、几分钟音频)会变成:
|
|
16
|
-
|
|
17
|
-
- 用户点击后**干等**:合成 + 转换整段(几十秒甚至几分钟)完成才出声;
|
|
18
|
-
- 期间没有任何反馈,体验像"卡死";
|
|
19
|
-
- 内存峰值高(整段音频一次性进出 RVC)。
|
|
20
|
-
|
|
21
|
-
曾考虑"只读前 20 字"来缩短等待 —— 但这会**丢掉正文内容**,被否决。
|
|
22
|
-
正确方向是:内容一字不丢,但**让第一块尽快出声,其余在播放的同时后台合成**。
|
|
23
|
-
|
|
24
|
-
## 2. 方案总览
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
Edge 合成整段底噪? 不 —— 改成:
|
|
28
|
-
1) 文本按句切块(每块 ≈ 10-20 秒音频)
|
|
29
|
-
2) 先转换预热块(2-4 块)→ 立即返回给前端开始播放
|
|
30
|
-
3) 播放第 n 块的同时,后台转换第 n+1、n+2 块(转换/播放重叠)
|
|
31
|
-
4) 前端队列快见底时向 Host 拉取下一块,无缝续播
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
关键比率:**速度比 ratio = 转换耗时 / 音频时长**。
|
|
35
|
-
|
|
36
|
-
- GPU(RTX 5070 实测):15 秒音频 ≈ 3-4 秒转换 → ratio ≈ 0.25
|
|
37
|
-
→ 转换永远追得上播放,队列不会饿死,**天然无缝**。
|
|
38
|
-
- CPU 用户:ratio 可能 > 1 → 必须缩小块、加大预热,把"卡顿点"压到最少。
|
|
39
|
-
|
|
40
|
-
## 3. 自适应校准(probe + 分档)
|
|
41
|
-
|
|
42
|
-
首次使用长文本 RVC 时,Host 做一次 **5 秒探测**:
|
|
43
|
-
|
|
44
|
-
1. Edge 合成一段固定探测文本(≈3.6 秒音频);
|
|
45
|
-
2. 本机 RVC 转换,测出 `ratio = 转换耗时 / 音频秒数`(含每块 Edge 合成耗时,
|
|
46
|
-
与真实分块流水线口径一致,偏保守);
|
|
47
|
-
3. 按分档表决定 `chunkSec`(每块音频秒数)与 `prewarm`(先转换几块再开播);
|
|
48
|
-
4. 结果按配置指纹持久化到 `~/.dsh/tts-rvc/calibration.json`:
|
|
49
|
-
- **7 天有效**,dsh 重启后直接复用,连那一次 ~7s 探测都省掉;
|
|
50
|
-
- 同时记录 `device`(RVC 服务 `/health` 上报的 GPU 名);换显卡/切 CPU 时
|
|
51
|
-
自动重新探测,不会拿旧的 GPU 数据给 CPU 用;
|
|
52
|
-
- 探测失败不覆盖磁盘上的旧有效条目(会话内 2 分钟用保守档兜底)。
|
|
53
|
-
|
|
54
|
-
| ratio | 块大小 | 预热块数 | 适用 |
|
|
55
|
-
|---|---|---|---|
|
|
56
|
-
| ≤ 0.4 | 20 秒 | 2 | 强 GPU,几乎无缝 |
|
|
57
|
-
| 0.4 – 0.6 | 15 秒 | 2 | 中端 GPU |
|
|
58
|
-
| 0.6 – 0.9 | 10 秒 | 3 | 入门 GPU / 快 CPU |
|
|
59
|
-
| > 0.9 | 6 秒 | 4 | CPU,尽量平滑 |
|
|
60
|
-
| 探测失败 | 10 秒 | 3 | 保守兜底(60 秒内不重复探测) |
|
|
61
|
-
|
|
62
|
-
块大小换算:中文 ≈ 3.6 字/秒 → `maxChars ≈ chunkSec × 3.6`(拉丁文本按 12 字/秒)。
|
|
63
|
-
|
|
64
|
-
### 为什么分档而不是固定值?
|
|
65
|
-
|
|
66
|
-
同一套代码要在"4090 用户"和"核显用户"上都成立。固定大块在 CPU 上会频繁断流,
|
|
67
|
-
固定小块在 GPU 上是浪费。**按本机实测速度自适应**才是"即选即用"的根基。
|
|
68
|
-
|
|
69
|
-
## 4. 文本切块
|
|
70
|
-
|
|
71
|
-
`splitText(text, maxChars)`:
|
|
72
|
-
|
|
73
|
-
1. 按句号/感叹/问号/分号(含中英文)切句;
|
|
74
|
-
2. 超长句再按逗号/顿号切段;
|
|
75
|
-
3. 仍超长的硬切(每段 ≤ maxChars);
|
|
76
|
-
4. 尽量让每块落在语义边界,避免在句中切断导致听感断裂。
|
|
77
|
-
5. **原子 token 保护**:URL / 邮箱 / 小数 / 版本号(如 `3.14`、`v2.0.1`)先换成无标点
|
|
78
|
-
placeholder 再切分——句子级切分不会把小数里的 `.` 误当句号拆开;硬切(`safeEnd`)会
|
|
79
|
-
滑到最近空白/标点边界,且绝不切断 placeholder(长 URL 保持整段读出)。
|
|
80
|
-
6. **孤儿块合并**:末尾不足 8 字的极短句并入前一块(合并后 ≤ maxChars),避免"结巴"听感。
|
|
81
|
-
|
|
82
|
-
## 5. 协议:任务队列(Host 侧)
|
|
83
|
-
|
|
84
|
-
`POST /dsh-tts-api/speak`(RVC + Edge 底噪 + 预估 > 12 秒时):
|
|
85
|
-
|
|
86
|
-
```jsonc
|
|
87
|
-
// 请求不变;响应变为:
|
|
88
|
-
{ "jobId": "j1", "chunks": ["/dsh-tts-audio/c1", "/dsh-tts-audio/c2"], "total": 6,
|
|
89
|
-
"ratio": 0.24, "chunkSec": 20 }
|
|
90
|
-
// chunks 只含"预热块";总块数在 total
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
`GET /dsh-tts-api/rvc-next?job=j1`(前端逐块拉取):
|
|
94
|
-
|
|
95
|
-
```jsonc
|
|
96
|
-
{ "url": "/dsh-tts-audio/c3", "more": true } // 还有后续
|
|
97
|
-
{ "done": true } // 已到末尾
|
|
98
|
-
{ "error": "后续段落合成失败:..." } // 某块失败
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Host 内部:
|
|
102
|
-
|
|
103
|
-
- 每 job 一个**串行转换链**(`job.tail`),并发拉取自动排队,RVC 服务不被并发打爆;
|
|
104
|
-
- 每块 = Edge 合成该块文本 → RVC 转换 → 临时 wav → 音频路由 URL;
|
|
105
|
-
- job 惰性回收:完成 2 分钟后 / 创建 10 分钟后清理,上限 50 个;
|
|
106
|
-
- 单块失败不影响已缓冲的块,只把错误带给前端,前端提示后停止。
|
|
107
|
-
|
|
108
|
-
## 6. 前端渐进播放(无感衔接)
|
|
109
|
-
|
|
110
|
-
`playChunks(jobId, chunks, total, token)` 使用 **Web Audio 精确调度**:
|
|
111
|
-
|
|
112
|
-
- 每个块 `fetch → decodeAudioData` 成 AudioBuffer(保持 2 块解码余量),
|
|
113
|
-
按采样时钟**首尾相接调度**:`src.start(prevEnd)`(prevEnd 由已调度缓冲时长累加),
|
|
114
|
-
块间零事件抖动、零重载延迟;
|
|
115
|
-
- **服务端裁剪每块边缘填充静音**(rvc-server `/convert` 输出前 `trim_edges`):
|
|
116
|
-
实测每块头 138ms / 尾 538ms 纯静音(Edge TTS + RVC 填充),裁掉并各保留
|
|
117
|
-
20ms / 120ms 自然气息,块间不再有 ~680ms 死寂;
|
|
118
|
-
- 停止/打断:`speakToken` 全局令牌 + 源列表 `stop()`(waCleanup),立即静音;
|
|
119
|
-
- **进度可见**:`shared.chunkProgress = { index, total }` 随块播放更新——朗读按钮
|
|
120
|
-
tooltip 与试听面板显示「第 x/y 段 · 边播边合成」(绝不静默丢内容);
|
|
121
|
-
- 失败:某块合成失败 → 红字提示并停止;无 Web Audio 环境降级为双 `<audio>` ping-pong。
|
|
122
|
-
|
|
123
|
-
> 备注:`vc_single` 返回 **int16**,`trim_edges` 先归一化为 float [-1,1] 再处理,
|
|
124
|
-
> 否则 `sf.write` 会把 int16 样本值当 [-1,1] 幅值整体削波(曾导致输出 99% 满幅失真)。
|
|
125
|
-
|
|
126
|
-
## 7. RVC 服务端配套
|
|
127
|
-
|
|
128
|
-
`rvc-server.py` 本次新增:
|
|
129
|
-
|
|
130
|
-
1. **`/health` 上报 `gpu_name` / `vram_gb`** —— 校准持久化的设备指纹来源;
|
|
131
|
-
2. **faiss 索引缓存**:RVC pipeline 每次转换都会 `faiss.read_index` 重新读一遍
|
|
132
|
-
~400MB 索引文件(每块一次,代价可观)。现在按路径缓存加载好的 Index 对象,
|
|
133
|
-
`/load` 时清空 —— 分块模式下每块省掉一次 400MB 磁盘读。
|
|
134
|
-
|
|
135
|
-
## 8. 边界与回退
|
|
136
|
-
|
|
137
|
-
| 情况 | 行为 |
|
|
138
|
-
|---|---|
|
|
139
|
-
| 短文本(≤12 秒) | 不进队列,走原来的单 URL 链路(零额外开销) |
|
|
140
|
-
| 探测失败 | 保守档(10 秒 / 预热 3),会话内 2 分钟不重复探测 |
|
|
141
|
-
| 某块转换失败 | 已缓冲的继续播,后续报错并停止 |
|
|
142
|
-
| 停止 / 换消息 / 切会话 | 令牌失效,队列立即退出,无残留播放 |
|
|
143
|
-
| 刷新页面 | 队列随页面消失;Host job 按时回收 |
|
|
144
|
-
|
|
145
|
-
## 10. 实测(RTX 5070 + azusa-test)
|
|
146
|
-
|
|
147
|
-
| 场景 | 结果 |
|
|
148
|
-
|---|---|
|
|
149
|
-
| 短句链路 | ~4.6-5.4s(含 Edge 合成) |
|
|
150
|
-
| 长文本首响 | 探测 + 预热 2 块 ≈ 6-7s(**此后 7 天内从 calibration.json 直接复用,不再探测**) |
|
|
151
|
-
| 长文本全链 | 6/6 块取完,迟到块 RIFF 476KB;块间无感知停顿 |
|
|
152
|
-
| 索引缓存收益 | ratio 0.62 → 0.48(省掉每块 400MB 索引重读),档位 10s/3 → 15s/2 |
|
|
153
|
-
|
|
154
|
-
## 11. Phase 2 预留
|
|
155
|
-
|
|
156
|
-
- 音色包注册表 + 下载 UI(版权干净音色)→ 已包含在下方"已完成"(含私有仓库 rvc-for-tts 示例);
|
|
157
|
-
- 便携运行时打包 → **已完成(v1)**:`tools/package-runtime.py` 复制已验证环境 + 可选
|
|
158
|
-
torch cu128 升级(RTX 50 系提速,官方 CDN 慢时用国内镜像);本机实测转换正常。
|
|
159
|
-
|
|
160
|
-
> 已完成:
|
|
161
|
-
> - `calibration.json` 落盘(`~/.dsh/tts-rvc/`,7 天有效 + 设备指纹,跨会话复用探测结果);
|
|
162
|
-
> - **紧凑索引生成工具**(设置面板「索引路径」旁「压缩」按钮):408MB 原索引
|
|
163
|
-
> 子采样重建为**与源同度量**的精确 flat 索引(RVC 训练默认 L2,pipeline 的
|
|
164
|
-
> `square(1/score)` 加权即按 L2 设计),2k→5.9MB(-98.5%)、10k→29.3MB(-92.5%),
|
|
165
|
-
> RVC 管线零改动兼容,实测转换正常;音色包分发不再受索引体积阻碍;
|
|
166
|
-
> - **音色包注册表 + 下载 UI**:清单格式(schema 2:模型 + indexes 变体数组 + sha256 +
|
|
167
|
-
> 版权/作者/版本,url 支持相对路径),Host 代理清单与下载(规避 CORS)、sha256 逐一校验后
|
|
168
|
-
> 安装到 `~/.dsh/tts-rvc/packs/<id>/`,自动填入模型/索引并应用底噪/f0/index_rate 默认值;
|
|
169
|
-
> 失败自动清理残留;mock 仓库 `tests/mock-registry.mjs` 供本地测试;私有音色仓库
|
|
170
|
-
> `github.com/1624318455/rvc-for-tts`(azusa-test 模型 + 2k/10k/20k 紧凑索引);
|
|
171
|
-
> - **便携运行时打包 v1**:`tools/package-runtime.py`(--skip-torch 零下载即可用;
|
|
172
|
-
> 可选 cu128 升级适配 Blackwell,官方 CDN 慢时用 aliyun 镜像)。
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## English summary
|
|
177
|
-
|
|
178
|
-
RVC is voice **conversion** (input length == output length), not TTS, so a long
|
|
179
|
-
read must first synthesize the whole base audio with Edge TTS. Instead of
|
|
180
|
-
converting everything before playing (long silent wait), the plugin now:
|
|
181
|
-
|
|
182
|
-
1. **Probes** the local machine once (convert a ~3.6s clip, measure
|
|
183
|
-
`ratio = convert_time / audio_seconds`); results persist to
|
|
184
|
-
`~/.dsh/tts-rvc/calibration.json` (7-day validity, GPU-name fingerprint,
|
|
185
|
-
re-probes on device change), so the probe is paid only once per config;
|
|
186
|
-
2. Picks **chunk size (6–20s) and prewarm count (2–4)** from a ratio tier table;
|
|
187
|
-
3. Splits the text into sentence-aligned chunks, converts the prewarm chunks,
|
|
188
|
-
and returns a **job queue** immediately;
|
|
189
|
-
4. The client **plays while prefetching**: each next chunk is converted during
|
|
190
|
-
the current chunk's playback (`GET /rvc-next`), so long reads stream
|
|
191
|
-
seamlessly on GPU and degrade gracefully on CPU (smaller chunks, more
|
|
192
|
-
prewarm);
|
|
193
|
-
5. The RVC server reports GPU name/VRAM in `/health` and **caches the faiss
|
|
194
|
-
index** so each chunk no longer re-reads the ~400MB index file.
|
|
1
|
+
# 自适应分块渐进播放方案(Adaptive Chunked Progressive Playback)
|
|
2
|
+
|
|
3
|
+
> RVC 长文本朗读的"先全部转换、后整体播放"改造成"边转换、边播放"的设计与实现说明。
|
|
4
|
+
> 核心思想:**性能开销摊在用户本机**,不同机器自动校准块大小与预热数量,做到"即点即听、长文不卡顿"。
|
|
5
|
+
|
|
6
|
+
## 1. 问题
|
|
7
|
+
|
|
8
|
+
RVC 是**变声**而非 TTS:输入音频长度 = 输出音频长度,所以"长文本朗读"必须
|
|
9
|
+
先把整段文本用 Edge TTS 合成底噪,再交给 RVC 转换。旧链路是:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
整段文本 → Edge 合成整段底噪 → RVC 整段转换 → 返回 → 播放
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
对短句(≤12 秒音频)这是最优解;但对长回复(数百字、几分钟音频)会变成:
|
|
16
|
+
|
|
17
|
+
- 用户点击后**干等**:合成 + 转换整段(几十秒甚至几分钟)完成才出声;
|
|
18
|
+
- 期间没有任何反馈,体验像"卡死";
|
|
19
|
+
- 内存峰值高(整段音频一次性进出 RVC)。
|
|
20
|
+
|
|
21
|
+
曾考虑"只读前 20 字"来缩短等待 —— 但这会**丢掉正文内容**,被否决。
|
|
22
|
+
正确方向是:内容一字不丢,但**让第一块尽快出声,其余在播放的同时后台合成**。
|
|
23
|
+
|
|
24
|
+
## 2. 方案总览
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Edge 合成整段底噪? 不 —— 改成:
|
|
28
|
+
1) 文本按句切块(每块 ≈ 10-20 秒音频)
|
|
29
|
+
2) 先转换预热块(2-4 块)→ 立即返回给前端开始播放
|
|
30
|
+
3) 播放第 n 块的同时,后台转换第 n+1、n+2 块(转换/播放重叠)
|
|
31
|
+
4) 前端队列快见底时向 Host 拉取下一块,无缝续播
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
关键比率:**速度比 ratio = 转换耗时 / 音频时长**。
|
|
35
|
+
|
|
36
|
+
- GPU(RTX 5070 实测):15 秒音频 ≈ 3-4 秒转换 → ratio ≈ 0.25
|
|
37
|
+
→ 转换永远追得上播放,队列不会饿死,**天然无缝**。
|
|
38
|
+
- CPU 用户:ratio 可能 > 1 → 必须缩小块、加大预热,把"卡顿点"压到最少。
|
|
39
|
+
|
|
40
|
+
## 3. 自适应校准(probe + 分档)
|
|
41
|
+
|
|
42
|
+
首次使用长文本 RVC 时,Host 做一次 **5 秒探测**:
|
|
43
|
+
|
|
44
|
+
1. Edge 合成一段固定探测文本(≈3.6 秒音频);
|
|
45
|
+
2. 本机 RVC 转换,测出 `ratio = 转换耗时 / 音频秒数`(含每块 Edge 合成耗时,
|
|
46
|
+
与真实分块流水线口径一致,偏保守);
|
|
47
|
+
3. 按分档表决定 `chunkSec`(每块音频秒数)与 `prewarm`(先转换几块再开播);
|
|
48
|
+
4. 结果按配置指纹持久化到 `~/.dsh/tts-rvc/calibration.json`:
|
|
49
|
+
- **7 天有效**,dsh 重启后直接复用,连那一次 ~7s 探测都省掉;
|
|
50
|
+
- 同时记录 `device`(RVC 服务 `/health` 上报的 GPU 名);换显卡/切 CPU 时
|
|
51
|
+
自动重新探测,不会拿旧的 GPU 数据给 CPU 用;
|
|
52
|
+
- 探测失败不覆盖磁盘上的旧有效条目(会话内 2 分钟用保守档兜底)。
|
|
53
|
+
|
|
54
|
+
| ratio | 块大小 | 预热块数 | 适用 |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| ≤ 0.4 | 20 秒 | 2 | 强 GPU,几乎无缝 |
|
|
57
|
+
| 0.4 – 0.6 | 15 秒 | 2 | 中端 GPU |
|
|
58
|
+
| 0.6 – 0.9 | 10 秒 | 3 | 入门 GPU / 快 CPU |
|
|
59
|
+
| > 0.9 | 6 秒 | 4 | CPU,尽量平滑 |
|
|
60
|
+
| 探测失败 | 10 秒 | 3 | 保守兜底(60 秒内不重复探测) |
|
|
61
|
+
|
|
62
|
+
块大小换算:中文 ≈ 3.6 字/秒 → `maxChars ≈ chunkSec × 3.6`(拉丁文本按 12 字/秒)。
|
|
63
|
+
|
|
64
|
+
### 为什么分档而不是固定值?
|
|
65
|
+
|
|
66
|
+
同一套代码要在"4090 用户"和"核显用户"上都成立。固定大块在 CPU 上会频繁断流,
|
|
67
|
+
固定小块在 GPU 上是浪费。**按本机实测速度自适应**才是"即选即用"的根基。
|
|
68
|
+
|
|
69
|
+
## 4. 文本切块
|
|
70
|
+
|
|
71
|
+
`splitText(text, maxChars)`:
|
|
72
|
+
|
|
73
|
+
1. 按句号/感叹/问号/分号(含中英文)切句;
|
|
74
|
+
2. 超长句再按逗号/顿号切段;
|
|
75
|
+
3. 仍超长的硬切(每段 ≤ maxChars);
|
|
76
|
+
4. 尽量让每块落在语义边界,避免在句中切断导致听感断裂。
|
|
77
|
+
5. **原子 token 保护**:URL / 邮箱 / 小数 / 版本号(如 `3.14`、`v2.0.1`)先换成无标点
|
|
78
|
+
placeholder 再切分——句子级切分不会把小数里的 `.` 误当句号拆开;硬切(`safeEnd`)会
|
|
79
|
+
滑到最近空白/标点边界,且绝不切断 placeholder(长 URL 保持整段读出)。
|
|
80
|
+
6. **孤儿块合并**:末尾不足 8 字的极短句并入前一块(合并后 ≤ maxChars),避免"结巴"听感。
|
|
81
|
+
|
|
82
|
+
## 5. 协议:任务队列(Host 侧)
|
|
83
|
+
|
|
84
|
+
`POST /dsh-tts-api/speak`(RVC + Edge 底噪 + 预估 > 12 秒时):
|
|
85
|
+
|
|
86
|
+
```jsonc
|
|
87
|
+
// 请求不变;响应变为:
|
|
88
|
+
{ "jobId": "j1", "chunks": ["/dsh-tts-audio/c1", "/dsh-tts-audio/c2"], "total": 6,
|
|
89
|
+
"ratio": 0.24, "chunkSec": 20 }
|
|
90
|
+
// chunks 只含"预热块";总块数在 total
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`GET /dsh-tts-api/rvc-next?job=j1`(前端逐块拉取):
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
{ "url": "/dsh-tts-audio/c3", "more": true } // 还有后续
|
|
97
|
+
{ "done": true } // 已到末尾
|
|
98
|
+
{ "error": "后续段落合成失败:..." } // 某块失败
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Host 内部:
|
|
102
|
+
|
|
103
|
+
- 每 job 一个**串行转换链**(`job.tail`),并发拉取自动排队,RVC 服务不被并发打爆;
|
|
104
|
+
- 每块 = Edge 合成该块文本 → RVC 转换 → 临时 wav → 音频路由 URL;
|
|
105
|
+
- job 惰性回收:完成 2 分钟后 / 创建 10 分钟后清理,上限 50 个;
|
|
106
|
+
- 单块失败不影响已缓冲的块,只把错误带给前端,前端提示后停止。
|
|
107
|
+
|
|
108
|
+
## 6. 前端渐进播放(无感衔接)
|
|
109
|
+
|
|
110
|
+
`playChunks(jobId, chunks, total, token)` 使用 **Web Audio 精确调度**:
|
|
111
|
+
|
|
112
|
+
- 每个块 `fetch → decodeAudioData` 成 AudioBuffer(保持 2 块解码余量),
|
|
113
|
+
按采样时钟**首尾相接调度**:`src.start(prevEnd)`(prevEnd 由已调度缓冲时长累加),
|
|
114
|
+
块间零事件抖动、零重载延迟;
|
|
115
|
+
- **服务端裁剪每块边缘填充静音**(rvc-server `/convert` 输出前 `trim_edges`):
|
|
116
|
+
实测每块头 138ms / 尾 538ms 纯静音(Edge TTS + RVC 填充),裁掉并各保留
|
|
117
|
+
20ms / 120ms 自然气息,块间不再有 ~680ms 死寂;
|
|
118
|
+
- 停止/打断:`speakToken` 全局令牌 + 源列表 `stop()`(waCleanup),立即静音;
|
|
119
|
+
- **进度可见**:`shared.chunkProgress = { index, total }` 随块播放更新——朗读按钮
|
|
120
|
+
tooltip 与试听面板显示「第 x/y 段 · 边播边合成」(绝不静默丢内容);
|
|
121
|
+
- 失败:某块合成失败 → 红字提示并停止;无 Web Audio 环境降级为双 `<audio>` ping-pong。
|
|
122
|
+
|
|
123
|
+
> 备注:`vc_single` 返回 **int16**,`trim_edges` 先归一化为 float [-1,1] 再处理,
|
|
124
|
+
> 否则 `sf.write` 会把 int16 样本值当 [-1,1] 幅值整体削波(曾导致输出 99% 满幅失真)。
|
|
125
|
+
|
|
126
|
+
## 7. RVC 服务端配套
|
|
127
|
+
|
|
128
|
+
`rvc-server.py` 本次新增:
|
|
129
|
+
|
|
130
|
+
1. **`/health` 上报 `gpu_name` / `vram_gb`** —— 校准持久化的设备指纹来源;
|
|
131
|
+
2. **faiss 索引缓存**:RVC pipeline 每次转换都会 `faiss.read_index` 重新读一遍
|
|
132
|
+
~400MB 索引文件(每块一次,代价可观)。现在按路径缓存加载好的 Index 对象,
|
|
133
|
+
`/load` 时清空 —— 分块模式下每块省掉一次 400MB 磁盘读。
|
|
134
|
+
|
|
135
|
+
## 8. 边界与回退
|
|
136
|
+
|
|
137
|
+
| 情况 | 行为 |
|
|
138
|
+
|---|---|
|
|
139
|
+
| 短文本(≤12 秒) | 不进队列,走原来的单 URL 链路(零额外开销) |
|
|
140
|
+
| 探测失败 | 保守档(10 秒 / 预热 3),会话内 2 分钟不重复探测 |
|
|
141
|
+
| 某块转换失败 | 已缓冲的继续播,后续报错并停止 |
|
|
142
|
+
| 停止 / 换消息 / 切会话 | 令牌失效,队列立即退出,无残留播放 |
|
|
143
|
+
| 刷新页面 | 队列随页面消失;Host job 按时回收 |
|
|
144
|
+
|
|
145
|
+
## 10. 实测(RTX 5070 + azusa-test)
|
|
146
|
+
|
|
147
|
+
| 场景 | 结果 |
|
|
148
|
+
|---|---|
|
|
149
|
+
| 短句链路 | ~4.6-5.4s(含 Edge 合成) |
|
|
150
|
+
| 长文本首响 | 探测 + 预热 2 块 ≈ 6-7s(**此后 7 天内从 calibration.json 直接复用,不再探测**) |
|
|
151
|
+
| 长文本全链 | 6/6 块取完,迟到块 RIFF 476KB;块间无感知停顿 |
|
|
152
|
+
| 索引缓存收益 | ratio 0.62 → 0.48(省掉每块 400MB 索引重读),档位 10s/3 → 15s/2 |
|
|
153
|
+
|
|
154
|
+
## 11. Phase 2 预留
|
|
155
|
+
|
|
156
|
+
- 音色包注册表 + 下载 UI(版权干净音色)→ 已包含在下方"已完成"(含私有仓库 rvc-for-tts 示例);
|
|
157
|
+
- 便携运行时打包 → **已完成(v1)**:`tools/package-runtime.py` 复制已验证环境 + 可选
|
|
158
|
+
torch cu128 升级(RTX 50 系提速,官方 CDN 慢时用国内镜像);本机实测转换正常。
|
|
159
|
+
|
|
160
|
+
> 已完成:
|
|
161
|
+
> - `calibration.json` 落盘(`~/.dsh/tts-rvc/`,7 天有效 + 设备指纹,跨会话复用探测结果);
|
|
162
|
+
> - **紧凑索引生成工具**(设置面板「索引路径」旁「压缩」按钮):408MB 原索引
|
|
163
|
+
> 子采样重建为**与源同度量**的精确 flat 索引(RVC 训练默认 L2,pipeline 的
|
|
164
|
+
> `square(1/score)` 加权即按 L2 设计),2k→5.9MB(-98.5%)、10k→29.3MB(-92.5%),
|
|
165
|
+
> RVC 管线零改动兼容,实测转换正常;音色包分发不再受索引体积阻碍;
|
|
166
|
+
> - **音色包注册表 + 下载 UI**:清单格式(schema 2:模型 + indexes 变体数组 + sha256 +
|
|
167
|
+
> 版权/作者/版本,url 支持相对路径),Host 代理清单与下载(规避 CORS)、sha256 逐一校验后
|
|
168
|
+
> 安装到 `~/.dsh/tts-rvc/packs/<id>/`,自动填入模型/索引并应用底噪/f0/index_rate 默认值;
|
|
169
|
+
> 失败自动清理残留;mock 仓库 `tests/mock-registry.mjs` 供本地测试;私有音色仓库
|
|
170
|
+
> `github.com/1624318455/rvc-for-tts`(azusa-test 模型 + 2k/10k/20k 紧凑索引);
|
|
171
|
+
> - **便携运行时打包 v1**:`tools/package-runtime.py`(--skip-torch 零下载即可用;
|
|
172
|
+
> 可选 cu128 升级适配 Blackwell,官方 CDN 慢时用 aliyun 镜像)。
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## English summary
|
|
177
|
+
|
|
178
|
+
RVC is voice **conversion** (input length == output length), not TTS, so a long
|
|
179
|
+
read must first synthesize the whole base audio with Edge TTS. Instead of
|
|
180
|
+
converting everything before playing (long silent wait), the plugin now:
|
|
181
|
+
|
|
182
|
+
1. **Probes** the local machine once (convert a ~3.6s clip, measure
|
|
183
|
+
`ratio = convert_time / audio_seconds`); results persist to
|
|
184
|
+
`~/.dsh/tts-rvc/calibration.json` (7-day validity, GPU-name fingerprint,
|
|
185
|
+
re-probes on device change), so the probe is paid only once per config;
|
|
186
|
+
2. Picks **chunk size (6–20s) and prewarm count (2–4)** from a ratio tier table;
|
|
187
|
+
3. Splits the text into sentence-aligned chunks, converts the prewarm chunks,
|
|
188
|
+
and returns a **job queue** immediately;
|
|
189
|
+
4. The client **plays while prefetching**: each next chunk is converted during
|
|
190
|
+
the current chunk's playback (`GET /rvc-next`), so long reads stream
|
|
191
|
+
seamlessly on GPU and degrade gracefully on CPU (smaller chunks, more
|
|
192
|
+
prewarm);
|
|
193
|
+
5. The RVC server reports GPU name/VRAM in `/health` and **caches the faiss
|
|
194
|
+
index** so each chunk no longer re-reads the ~400MB index file.
|