@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,86 +1,86 @@
|
|
|
1
|
-
// One-off real end-to-end for the compact-index tool.
|
|
2
|
-
//
|
|
3
|
-
// [DEPRECATED in favor of tests/rvc-server-live.mjs] This one-off test hardcodes
|
|
4
|
-
// Windows paths (E:/AI/.../azusa-test.pth) and is not portable. The standard,
|
|
5
|
-
// environment-aware live scenario + boundary suite is `tests/rvc-server-live.mjs`
|
|
6
|
-
// (npm run test:live). This file is kept for Windows reference only.
|
|
7
|
-
|
|
8
|
-
// Requires the local rvc-server (NEW code with /compact-index) on 4892.
|
|
9
|
-
// node tests/e2e-compact-index.mjs
|
|
10
|
-
import { createServer } from 'node:http';
|
|
11
|
-
|
|
12
|
-
const BASE = 'http://127.0.0.1:4892';
|
|
13
|
-
const RVC_DIR = 'E:/AI/RVC20240604Nvidia/RVC20240604Nvidia';
|
|
14
|
-
const MODEL = `${RVC_DIR}/assets/weights/azusa-test.pth`;
|
|
15
|
-
const INDEX = `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2.index`;
|
|
16
|
-
const OUT_DIR = `${RVC_DIR}/assets/indices`;
|
|
17
|
-
|
|
18
|
-
function post(route, payload, timeoutMs = 300000) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
const ctrl = new AbortController();
|
|
21
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
22
|
-
fetch(BASE + route, {
|
|
23
|
-
method: 'POST',
|
|
24
|
-
headers: { 'Content-Type': 'application/json' },
|
|
25
|
-
body: JSON.stringify(payload),
|
|
26
|
-
signal: ctrl.signal
|
|
27
|
-
}).then(async r => {
|
|
28
|
-
clearTimeout(timer);
|
|
29
|
-
const data = await r.json().catch(() => null);
|
|
30
|
-
if (!r.ok) reject(new Error(`HTTP ${r.status}: ${(data && (data.message || data.error)) || r.statusText}`));
|
|
31
|
-
else resolve(data);
|
|
32
|
-
}).catch(e => { clearTimeout(timer); reject(e); });
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function miniWav(seconds = 2, sr = 40000) {
|
|
37
|
-
const n = sr * seconds;
|
|
38
|
-
const dataSize = n * 2;
|
|
39
|
-
const buf = Buffer.alloc(44 + dataSize);
|
|
40
|
-
buf.write('RIFF', 0);
|
|
41
|
-
buf.writeUInt32LE(36 + dataSize, 4);
|
|
42
|
-
buf.write('WAVE', 8);
|
|
43
|
-
buf.write('fmt ', 12);
|
|
44
|
-
buf.writeUInt32LE(16, 16);
|
|
45
|
-
buf.writeUInt16LE(1, 20);
|
|
46
|
-
buf.writeUInt16LE(1, 22);
|
|
47
|
-
buf.writeUInt32LE(sr, 24);
|
|
48
|
-
buf.writeUInt32LE(sr * 2, 28);
|
|
49
|
-
buf.writeUInt16LE(2, 32);
|
|
50
|
-
buf.writeUInt16LE(16, 34);
|
|
51
|
-
buf.write('data', 36);
|
|
52
|
-
buf.writeUInt32LE(dataSize, 40);
|
|
53
|
-
return buf;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const results = [];
|
|
57
|
-
function check(name, ok, detail) {
|
|
58
|
-
results.push({ name, ok });
|
|
59
|
-
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 1) build compact indexes
|
|
63
|
-
const t0 = Date.now();
|
|
64
|
-
const c2k = await post('/compact-index', { index: INDEX, target_vectors: 2000 });
|
|
65
|
-
console.log(`compact 2k -> ${c2k.path.split(/[\\/]/).pop()} ${(c2k.size / 1048576).toFixed(1)}MB (${c2k.reduction_pct}% smaller, ${((Date.now() - t0) / 1000).toFixed(1)}s)`);
|
|
66
|
-
check('compact 2k built', c2k.ok && c2k.vectors === 2000 && c2k.size > 1000000 && c2k.size < 10000000, `size=${c2k.size}`);
|
|
67
|
-
const c10k = await post('/compact-index', { index: INDEX, target_vectors: 10000 });
|
|
68
|
-
console.log(`compact 10k -> ${c10k.path.split(/[\\/]/).pop()} ${(c10k.size / 1048576).toFixed(1)}MB (${c10k.reduction_pct}% smaller)`);
|
|
69
|
-
check('compact 10k built', c10k.ok && c10k.vectors === 10000 && c10k.size > 20000000 && c10k.size < 50000000, `size=${c10k.size}`);
|
|
70
|
-
|
|
71
|
-
// 2) load the model + compact index, convert a clip -> must yield wav
|
|
72
|
-
await post('/load', { model: MODEL, index: c2k.path });
|
|
73
|
-
const cv = await post('/convert', {
|
|
74
|
-
audio_base64: miniWav(2).toString('base64'),
|
|
75
|
-
params: { spk_id: 0, f0_method: 'rmvpe', index_rate: 0.75, resample_sr: 40000 }
|
|
76
|
-
}, 180000);
|
|
77
|
-
const wav = Buffer.from(cv.audio_base64 || '', 'base64');
|
|
78
|
-
check('convert with compact index works', cv.audio_base64 && wav.length > 1000 && wav.slice(0, 4).toString() === 'RIFF', `bytes=${wav.length} sr=${cv.sample_rate}`);
|
|
79
|
-
|
|
80
|
-
// 3) restore the original index on the server
|
|
81
|
-
await post('/load', { model: MODEL, index: INDEX });
|
|
82
|
-
console.log('restored original index on server');
|
|
83
|
-
|
|
84
|
-
const failed = results.filter(r => !r.ok);
|
|
85
|
-
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
|
|
86
|
-
process.exit(failed.length === 0 ? 0 : 1);
|
|
1
|
+
// One-off real end-to-end for the compact-index tool.
|
|
2
|
+
//
|
|
3
|
+
// [DEPRECATED in favor of tests/rvc-server-live.mjs] This one-off test hardcodes
|
|
4
|
+
// Windows paths (E:/AI/.../azusa-test.pth) and is not portable. The standard,
|
|
5
|
+
// environment-aware live scenario + boundary suite is `tests/rvc-server-live.mjs`
|
|
6
|
+
// (npm run test:live). This file is kept for Windows reference only.
|
|
7
|
+
|
|
8
|
+
// Requires the local rvc-server (NEW code with /compact-index) on 4892.
|
|
9
|
+
// node tests/e2e-compact-index.mjs
|
|
10
|
+
import { createServer } from 'node:http';
|
|
11
|
+
|
|
12
|
+
const BASE = 'http://127.0.0.1:4892';
|
|
13
|
+
const RVC_DIR = 'E:/AI/RVC20240604Nvidia/RVC20240604Nvidia';
|
|
14
|
+
const MODEL = `${RVC_DIR}/assets/weights/azusa-test.pth`;
|
|
15
|
+
const INDEX = `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2.index`;
|
|
16
|
+
const OUT_DIR = `${RVC_DIR}/assets/indices`;
|
|
17
|
+
|
|
18
|
+
function post(route, payload, timeoutMs = 300000) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const ctrl = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
22
|
+
fetch(BASE + route, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify(payload),
|
|
26
|
+
signal: ctrl.signal
|
|
27
|
+
}).then(async r => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
const data = await r.json().catch(() => null);
|
|
30
|
+
if (!r.ok) reject(new Error(`HTTP ${r.status}: ${(data && (data.message || data.error)) || r.statusText}`));
|
|
31
|
+
else resolve(data);
|
|
32
|
+
}).catch(e => { clearTimeout(timer); reject(e); });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function miniWav(seconds = 2, sr = 40000) {
|
|
37
|
+
const n = sr * seconds;
|
|
38
|
+
const dataSize = n * 2;
|
|
39
|
+
const buf = Buffer.alloc(44 + dataSize);
|
|
40
|
+
buf.write('RIFF', 0);
|
|
41
|
+
buf.writeUInt32LE(36 + dataSize, 4);
|
|
42
|
+
buf.write('WAVE', 8);
|
|
43
|
+
buf.write('fmt ', 12);
|
|
44
|
+
buf.writeUInt32LE(16, 16);
|
|
45
|
+
buf.writeUInt16LE(1, 20);
|
|
46
|
+
buf.writeUInt16LE(1, 22);
|
|
47
|
+
buf.writeUInt32LE(sr, 24);
|
|
48
|
+
buf.writeUInt32LE(sr * 2, 28);
|
|
49
|
+
buf.writeUInt16LE(2, 32);
|
|
50
|
+
buf.writeUInt16LE(16, 34);
|
|
51
|
+
buf.write('data', 36);
|
|
52
|
+
buf.writeUInt32LE(dataSize, 40);
|
|
53
|
+
return buf;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const results = [];
|
|
57
|
+
function check(name, ok, detail) {
|
|
58
|
+
results.push({ name, ok });
|
|
59
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 1) build compact indexes
|
|
63
|
+
const t0 = Date.now();
|
|
64
|
+
const c2k = await post('/compact-index', { index: INDEX, target_vectors: 2000 });
|
|
65
|
+
console.log(`compact 2k -> ${c2k.path.split(/[\\/]/).pop()} ${(c2k.size / 1048576).toFixed(1)}MB (${c2k.reduction_pct}% smaller, ${((Date.now() - t0) / 1000).toFixed(1)}s)`);
|
|
66
|
+
check('compact 2k built', c2k.ok && c2k.vectors === 2000 && c2k.size > 1000000 && c2k.size < 10000000, `size=${c2k.size}`);
|
|
67
|
+
const c10k = await post('/compact-index', { index: INDEX, target_vectors: 10000 });
|
|
68
|
+
console.log(`compact 10k -> ${c10k.path.split(/[\\/]/).pop()} ${(c10k.size / 1048576).toFixed(1)}MB (${c10k.reduction_pct}% smaller)`);
|
|
69
|
+
check('compact 10k built', c10k.ok && c10k.vectors === 10000 && c10k.size > 20000000 && c10k.size < 50000000, `size=${c10k.size}`);
|
|
70
|
+
|
|
71
|
+
// 2) load the model + compact index, convert a clip -> must yield wav
|
|
72
|
+
await post('/load', { model: MODEL, index: c2k.path });
|
|
73
|
+
const cv = await post('/convert', {
|
|
74
|
+
audio_base64: miniWav(2).toString('base64'),
|
|
75
|
+
params: { spk_id: 0, f0_method: 'rmvpe', index_rate: 0.75, resample_sr: 40000 }
|
|
76
|
+
}, 180000);
|
|
77
|
+
const wav = Buffer.from(cv.audio_base64 || '', 'base64');
|
|
78
|
+
check('convert with compact index works', cv.audio_base64 && wav.length > 1000 && wav.slice(0, 4).toString() === 'RIFF', `bytes=${wav.length} sr=${cv.sample_rate}`);
|
|
79
|
+
|
|
80
|
+
// 3) restore the original index on the server
|
|
81
|
+
await post('/load', { model: MODEL, index: INDEX });
|
|
82
|
+
console.log('restored original index on server');
|
|
83
|
+
|
|
84
|
+
const failed = results.filter(r => !r.ok);
|
|
85
|
+
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
|
|
86
|
+
process.exit(failed.length === 0 ? 0 : 1);
|
package/tests/e2e-index-ab.mjs
CHANGED
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
// One-off real A/B: how much does the index actually change the output?
|
|
2
|
-
//
|
|
3
|
-
// [DEPRECATED in favor of tests/rvc-server-live.mjs] This one-off test hardcodes
|
|
4
|
-
// Windows paths (E:/AI/.../azusa-test.pth) and is not portable. The standard,
|
|
5
|
-
// environment-aware live scenario + boundary suite is `tests/rvc-server-live.mjs`
|
|
6
|
-
// (npm run test:live). This file is kept for Windows reference only.
|
|
7
|
-
|
|
8
|
-
// Same real Edge-TTS base audio converted under three configs on the local
|
|
9
|
-
// rvc-server (4892, NEW code): index-free vs full index vs compact-2k index.
|
|
10
|
-
// node tests/e2e-index-ab.mjs
|
|
11
|
-
import * as plugin from '../lib/index.mjs';
|
|
12
|
-
|
|
13
|
-
const routes = [];
|
|
14
|
-
const ctx = {
|
|
15
|
-
get(name) {
|
|
16
|
-
if (name === 'webServer') {
|
|
17
|
-
return {
|
|
18
|
-
register(route) {
|
|
19
|
-
routes.push(route);
|
|
20
|
-
return () => {
|
|
21
|
-
const i = routes.indexOf(route);
|
|
22
|
-
if (i >= 0) routes.splice(i, 1);
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
return undefined;
|
|
28
|
-
},
|
|
29
|
-
effect() {}
|
|
30
|
-
};
|
|
31
|
-
plugin.apply(ctx);
|
|
32
|
-
const speakRoute = routes.find((r) => r.kind === 'exact' && r.path === '/dsh-tts-api/speak');
|
|
33
|
-
const audioRoute = routes.find((r) => r.kind === 'prefix' && r.path === '/dsh-tts-audio');
|
|
34
|
-
|
|
35
|
-
function mockReq(url, body) {
|
|
36
|
-
const chunks = body === undefined ? [] : [body];
|
|
37
|
-
return {
|
|
38
|
-
url,
|
|
39
|
-
[Symbol.asyncIterator]() {
|
|
40
|
-
let i = 0;
|
|
41
|
-
return {
|
|
42
|
-
next: () => (i < chunks.length ? Promise.resolve({ value: chunks[i++], done: false }) : Promise.resolve({ done: true }))
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function mockRes() {
|
|
48
|
-
return { headersSent: false, head: null, body: null, writeHead(c, h) { this.head = { c, h }; }, end(b) { this.body = b; } };
|
|
49
|
-
}
|
|
50
|
-
async function call(route, req, res) { await route.handler(req, res); return res; }
|
|
51
|
-
|
|
52
|
-
const BASE = 'http://127.0.0.1:4892';
|
|
53
|
-
const RVC_DIR = 'E:/AI/RVC20240604Nvidia/RVC20240604Nvidia';
|
|
54
|
-
const MODEL = `${RVC_DIR}/assets/weights/azusa-test.pth`;
|
|
55
|
-
const FULL = `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2.index`;
|
|
56
|
-
const COMPACT = process.env.AB_INDEX
|
|
57
|
-
|| `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2_compact_2000.index`;
|
|
58
|
-
|
|
59
|
-
function post(route, payload, timeoutMs = 300000) {
|
|
60
|
-
return new Promise((resolve, reject) => {
|
|
61
|
-
const ctrl = new AbortController();
|
|
62
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
63
|
-
fetch(BASE + route, {
|
|
64
|
-
method: 'POST',
|
|
65
|
-
headers: { 'Content-Type': 'application/json' },
|
|
66
|
-
body: JSON.stringify(payload),
|
|
67
|
-
signal: ctrl.signal
|
|
68
|
-
}).then(async r => {
|
|
69
|
-
clearTimeout(timer);
|
|
70
|
-
const data = await r.json().catch(() => null);
|
|
71
|
-
if (!r.ok) reject(new Error(`HTTP ${r.status}: ${(data && (data.message || data.error)) || r.statusText}`));
|
|
72
|
-
else resolve(data);
|
|
73
|
-
}).catch(e => { clearTimeout(timer); reject(e); });
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function toSamples(buf) {
|
|
78
|
-
// wav -> int16 mono samples (best effort: skip 44-byte header)
|
|
79
|
-
const data = buf.subarray(44);
|
|
80
|
-
const n = Math.floor(data.length / 2);
|
|
81
|
-
const out = new Float32Array(n);
|
|
82
|
-
for (let i = 0; i < n; i++) out[i] = data.readInt16LE(i * 2);
|
|
83
|
-
return out;
|
|
84
|
-
}
|
|
85
|
-
function compare(name, a, b) {
|
|
86
|
-
const na = a.length, nb = b.length;
|
|
87
|
-
if (na !== nb) {
|
|
88
|
-
console.log(` ${name}: LENGTH DIFFERS ${na} vs ${nb} (${Math.abs(na - nb)} samples)`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
let sum = 0, max = 0;
|
|
92
|
-
for (let i = 0; i < na; i++) {
|
|
93
|
-
const d = Math.abs(a[i] - b[i]);
|
|
94
|
-
sum += d;
|
|
95
|
-
if (d > max) max = d;
|
|
96
|
-
}
|
|
97
|
-
const mean = sum / na;
|
|
98
|
-
let rms = 0;
|
|
99
|
-
for (let i = 0; i < na; i++) rms += a[i] * a[i];
|
|
100
|
-
rms = Math.sqrt(rms / na);
|
|
101
|
-
console.log(` ${name}: meanAbsDiff=${mean.toFixed(1)} (${((mean / (rms || 1)) * 100).toFixed(1)}% of RMS), max=${max}, rms=${rms.toFixed(0)}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 1) real speech base audio via the plugin's Edge provider
|
|
105
|
-
const res = mockRes();
|
|
106
|
-
await speakRoute.handler(mockReq('/dsh-tts-api/speak', JSON.stringify({
|
|
107
|
-
text: '这是一段用来测试不同索引对音色还原度影响的真实语音,请你仔细听。',
|
|
108
|
-
voice: 'zh-CN-XiaoxuanNeural'
|
|
109
|
-
})), res);
|
|
110
|
-
const parsed = JSON.parse(res.body);
|
|
111
|
-
if (!parsed.url) { console.error('edge speak failed:', parsed); process.exit(1); }
|
|
112
|
-
const ares = mockRes();
|
|
113
|
-
await audioRoute.handler(mockReq(parsed.url), ares);
|
|
114
|
-
const baseMp3 = Buffer.isBuffer(ares.body) ? ares.body : Buffer.from(ares.body ?? '');
|
|
115
|
-
console.log(`base audio: ${(baseMp3.length / 1024).toFixed(0)} KB mp3`);
|
|
116
|
-
|
|
117
|
-
// 2) three conversions of the SAME base
|
|
118
|
-
await post('/load', { model: MODEL, index: FULL });
|
|
119
|
-
const z = await post('/convert', { audio_base64: baseMp3.toString('base64'), params: { index_rate: 0 } }, 180000);
|
|
120
|
-
console.log('config A (index-free): converted');
|
|
121
|
-
const f = await post('/convert', { audio_base64: baseMp3.toString('base64'), params: { index_rate: 0.75 } }, 180000);
|
|
122
|
-
console.log('config B (full index, 0.75): converted');
|
|
123
|
-
await post('/load', { model: MODEL, index: COMPACT });
|
|
124
|
-
const c = await post('/convert', { audio_base64: baseMp3.toString('base64'), params: { index_rate: 0.75 } }, 180000);
|
|
125
|
-
console.log('config C (compact 2k, 0.75): converted');
|
|
126
|
-
await post('/load', { model: MODEL, index: FULL }); // restore
|
|
127
|
-
|
|
128
|
-
const za = toSamples(Buffer.from(z.audio_base64, 'base64'));
|
|
129
|
-
const fa = toSamples(Buffer.from(f.audio_base64, 'base64'));
|
|
130
|
-
const ca = toSamples(Buffer.from(c.audio_base64, 'base64'));
|
|
131
|
-
console.log(`lengths: A=${za.length} B=${fa.length} C=${ca.length}`);
|
|
132
|
-
console.log('A vs B (index-free vs full index):');
|
|
133
|
-
compare(' ', za, fa);
|
|
134
|
-
console.log('B vs C (full index vs compact 2k):');
|
|
135
|
-
compare(' ', fa, ca);
|
|
136
|
-
console.log('A vs C (index-free vs compact 2k):');
|
|
137
|
-
compare(' ', za, ca);
|
|
138
|
-
process.exit(0);
|
|
1
|
+
// One-off real A/B: how much does the index actually change the output?
|
|
2
|
+
//
|
|
3
|
+
// [DEPRECATED in favor of tests/rvc-server-live.mjs] This one-off test hardcodes
|
|
4
|
+
// Windows paths (E:/AI/.../azusa-test.pth) and is not portable. The standard,
|
|
5
|
+
// environment-aware live scenario + boundary suite is `tests/rvc-server-live.mjs`
|
|
6
|
+
// (npm run test:live). This file is kept for Windows reference only.
|
|
7
|
+
|
|
8
|
+
// Same real Edge-TTS base audio converted under three configs on the local
|
|
9
|
+
// rvc-server (4892, NEW code): index-free vs full index vs compact-2k index.
|
|
10
|
+
// node tests/e2e-index-ab.mjs
|
|
11
|
+
import * as plugin from '../lib/index.mjs';
|
|
12
|
+
|
|
13
|
+
const routes = [];
|
|
14
|
+
const ctx = {
|
|
15
|
+
get(name) {
|
|
16
|
+
if (name === 'webServer') {
|
|
17
|
+
return {
|
|
18
|
+
register(route) {
|
|
19
|
+
routes.push(route);
|
|
20
|
+
return () => {
|
|
21
|
+
const i = routes.indexOf(route);
|
|
22
|
+
if (i >= 0) routes.splice(i, 1);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
},
|
|
29
|
+
effect() {}
|
|
30
|
+
};
|
|
31
|
+
plugin.apply(ctx);
|
|
32
|
+
const speakRoute = routes.find((r) => r.kind === 'exact' && r.path === '/dsh-tts-api/speak');
|
|
33
|
+
const audioRoute = routes.find((r) => r.kind === 'prefix' && r.path === '/dsh-tts-audio');
|
|
34
|
+
|
|
35
|
+
function mockReq(url, body) {
|
|
36
|
+
const chunks = body === undefined ? [] : [body];
|
|
37
|
+
return {
|
|
38
|
+
url,
|
|
39
|
+
[Symbol.asyncIterator]() {
|
|
40
|
+
let i = 0;
|
|
41
|
+
return {
|
|
42
|
+
next: () => (i < chunks.length ? Promise.resolve({ value: chunks[i++], done: false }) : Promise.resolve({ done: true }))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function mockRes() {
|
|
48
|
+
return { headersSent: false, head: null, body: null, writeHead(c, h) { this.head = { c, h }; }, end(b) { this.body = b; } };
|
|
49
|
+
}
|
|
50
|
+
async function call(route, req, res) { await route.handler(req, res); return res; }
|
|
51
|
+
|
|
52
|
+
const BASE = 'http://127.0.0.1:4892';
|
|
53
|
+
const RVC_DIR = 'E:/AI/RVC20240604Nvidia/RVC20240604Nvidia';
|
|
54
|
+
const MODEL = `${RVC_DIR}/assets/weights/azusa-test.pth`;
|
|
55
|
+
const FULL = `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2.index`;
|
|
56
|
+
const COMPACT = process.env.AB_INDEX
|
|
57
|
+
|| `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2_compact_2000.index`;
|
|
58
|
+
|
|
59
|
+
function post(route, payload, timeoutMs = 300000) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const ctrl = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
63
|
+
fetch(BASE + route, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
signal: ctrl.signal
|
|
68
|
+
}).then(async r => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
const data = await r.json().catch(() => null);
|
|
71
|
+
if (!r.ok) reject(new Error(`HTTP ${r.status}: ${(data && (data.message || data.error)) || r.statusText}`));
|
|
72
|
+
else resolve(data);
|
|
73
|
+
}).catch(e => { clearTimeout(timer); reject(e); });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toSamples(buf) {
|
|
78
|
+
// wav -> int16 mono samples (best effort: skip 44-byte header)
|
|
79
|
+
const data = buf.subarray(44);
|
|
80
|
+
const n = Math.floor(data.length / 2);
|
|
81
|
+
const out = new Float32Array(n);
|
|
82
|
+
for (let i = 0; i < n; i++) out[i] = data.readInt16LE(i * 2);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function compare(name, a, b) {
|
|
86
|
+
const na = a.length, nb = b.length;
|
|
87
|
+
if (na !== nb) {
|
|
88
|
+
console.log(` ${name}: LENGTH DIFFERS ${na} vs ${nb} (${Math.abs(na - nb)} samples)`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let sum = 0, max = 0;
|
|
92
|
+
for (let i = 0; i < na; i++) {
|
|
93
|
+
const d = Math.abs(a[i] - b[i]);
|
|
94
|
+
sum += d;
|
|
95
|
+
if (d > max) max = d;
|
|
96
|
+
}
|
|
97
|
+
const mean = sum / na;
|
|
98
|
+
let rms = 0;
|
|
99
|
+
for (let i = 0; i < na; i++) rms += a[i] * a[i];
|
|
100
|
+
rms = Math.sqrt(rms / na);
|
|
101
|
+
console.log(` ${name}: meanAbsDiff=${mean.toFixed(1)} (${((mean / (rms || 1)) * 100).toFixed(1)}% of RMS), max=${max}, rms=${rms.toFixed(0)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1) real speech base audio via the plugin's Edge provider
|
|
105
|
+
const res = mockRes();
|
|
106
|
+
await speakRoute.handler(mockReq('/dsh-tts-api/speak', JSON.stringify({
|
|
107
|
+
text: '这是一段用来测试不同索引对音色还原度影响的真实语音,请你仔细听。',
|
|
108
|
+
voice: 'zh-CN-XiaoxuanNeural'
|
|
109
|
+
})), res);
|
|
110
|
+
const parsed = JSON.parse(res.body);
|
|
111
|
+
if (!parsed.url) { console.error('edge speak failed:', parsed); process.exit(1); }
|
|
112
|
+
const ares = mockRes();
|
|
113
|
+
await audioRoute.handler(mockReq(parsed.url), ares);
|
|
114
|
+
const baseMp3 = Buffer.isBuffer(ares.body) ? ares.body : Buffer.from(ares.body ?? '');
|
|
115
|
+
console.log(`base audio: ${(baseMp3.length / 1024).toFixed(0)} KB mp3`);
|
|
116
|
+
|
|
117
|
+
// 2) three conversions of the SAME base
|
|
118
|
+
await post('/load', { model: MODEL, index: FULL });
|
|
119
|
+
const z = await post('/convert', { audio_base64: baseMp3.toString('base64'), params: { index_rate: 0 } }, 180000);
|
|
120
|
+
console.log('config A (index-free): converted');
|
|
121
|
+
const f = await post('/convert', { audio_base64: baseMp3.toString('base64'), params: { index_rate: 0.75 } }, 180000);
|
|
122
|
+
console.log('config B (full index, 0.75): converted');
|
|
123
|
+
await post('/load', { model: MODEL, index: COMPACT });
|
|
124
|
+
const c = await post('/convert', { audio_base64: baseMp3.toString('base64'), params: { index_rate: 0.75 } }, 180000);
|
|
125
|
+
console.log('config C (compact 2k, 0.75): converted');
|
|
126
|
+
await post('/load', { model: MODEL, index: FULL }); // restore
|
|
127
|
+
|
|
128
|
+
const za = toSamples(Buffer.from(z.audio_base64, 'base64'));
|
|
129
|
+
const fa = toSamples(Buffer.from(f.audio_base64, 'base64'));
|
|
130
|
+
const ca = toSamples(Buffer.from(c.audio_base64, 'base64'));
|
|
131
|
+
console.log(`lengths: A=${za.length} B=${fa.length} C=${ca.length}`);
|
|
132
|
+
console.log('A vs B (index-free vs full index):');
|
|
133
|
+
compare(' ', za, fa);
|
|
134
|
+
console.log('B vs C (full index vs compact 2k):');
|
|
135
|
+
compare(' ', fa, ca);
|
|
136
|
+
console.log('A vs C (index-free vs compact 2k):');
|
|
137
|
+
compare(' ', za, ca);
|
|
138
|
+
process.exit(0);
|