@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.
Files changed (37) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +51 -51
  2. package/.github/ISSUE_TEMPLATE/config.yml +2 -2
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -28
  4. package/.github/pull_request_template.md +7 -7
  5. package/.github/workflows/contrib-triage.yml +66 -66
  6. package/.github/workflows/linked-issue.yml +21 -21
  7. package/.github/workflows/publish.yml +63 -0
  8. package/.github/workflows/stale.yml +30 -30
  9. package/.github/workflows/test.yml +33 -33
  10. package/.github/workflows/welcome.yml +22 -22
  11. package/ANALYSIS.md +205 -205
  12. package/PR-REVIEW.md +74 -74
  13. package/README.md +335 -335
  14. package/TODO.md +34 -34
  15. package/docs/HOST-CONTRACT.md +36 -36
  16. package/docs/RVC-GUIDE.md +390 -390
  17. package/docs/USER-GUIDE.md +389 -389
  18. package/docs/acceptance-checklist.md +147 -147
  19. package/docs/adaptive-chunked-playback.md +194 -194
  20. package/docs/macos-test-prompt.md +230 -230
  21. package/docs/windows-gpu-verify-prompt.md +151 -151
  22. package/lib/client.js +7164 -7164
  23. package/lib/index.mjs +3374 -3374
  24. package/package.json +51 -51
  25. package/rvc-server.py +564 -564
  26. package/tests/README.md +45 -45
  27. package/tests/client-load.mjs +406 -406
  28. package/tests/e2e-compact-index.mjs +86 -86
  29. package/tests/e2e-index-ab.mjs +138 -138
  30. package/tests/e2e-real-rvc.mjs +122 -122
  31. package/tests/i18n-keys.mjs +82 -82
  32. package/tests/package-runtime-patch.mjs +88 -88
  33. package/tests/rvc-server-live.mjs +230 -230
  34. package/tests/smoke.mjs +1284 -1284
  35. package/tools/gen-ui-preview.mjs +174 -174
  36. package/tools/mac-verify.sh +158 -158
  37. 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);
@@ -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);