@memef1f1y/dsh-plugin-tts 0.6.4 → 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 -23
- 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 -3371
- package/package.json +51 -52
- 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
package/tests/e2e-real-rvc.mjs
CHANGED
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
// One-off real end-to-end: plugin Host -> real rvc-server (azusa-test) chain.
|
|
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
|
-
// Verifies Edge base synth + real RVC conversion + audio route serving.
|
|
9
|
-
import * as plugin from '../lib/index.mjs';
|
|
10
|
-
|
|
11
|
-
const routes = [];
|
|
12
|
-
const ctx = {
|
|
13
|
-
get(name) {
|
|
14
|
-
if (name === 'webServer') {
|
|
15
|
-
return {
|
|
16
|
-
register(route) {
|
|
17
|
-
routes.push(route);
|
|
18
|
-
return () => {
|
|
19
|
-
const i = routes.indexOf(route);
|
|
20
|
-
if (i >= 0) routes.splice(i, 1);
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
26
|
-
},
|
|
27
|
-
effect() {}
|
|
28
|
-
};
|
|
29
|
-
plugin.apply(ctx);
|
|
30
|
-
|
|
31
|
-
const speakRoute = routes.find((r) => r.kind === 'exact' && r.path === '/dsh-tts-api/speak');
|
|
32
|
-
const audioRoute = routes.find((r) => r.kind === 'prefix' && r.path === '/dsh-tts-audio');
|
|
33
|
-
|
|
34
|
-
function mockReq(url, body) {
|
|
35
|
-
const chunks = body === undefined ? [] : [body];
|
|
36
|
-
return {
|
|
37
|
-
url,
|
|
38
|
-
[Symbol.asyncIterator]() {
|
|
39
|
-
let i = 0;
|
|
40
|
-
return {
|
|
41
|
-
next: () => (i < chunks.length ? Promise.resolve({ value: chunks[i++], done: false }) : Promise.resolve({ done: true }))
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function mockRes() {
|
|
47
|
-
return { headersSent: false, head: null, body: null, writeHead(c, h) { this.head = { c, h }; }, end(b) { this.body = b; } };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const RVC_DIR = 'E:/AI/RVC20240604Nvidia/RVC20240604Nvidia';
|
|
51
|
-
const custom = {
|
|
52
|
-
baseUrl: 'http://127.0.0.1:4892',
|
|
53
|
-
model: `${RVC_DIR}/assets/weights/azusa-test.pth`,
|
|
54
|
-
index: `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2.index`,
|
|
55
|
-
baseVoice: 'zh-CN-YunyangNeural',
|
|
56
|
-
f0Method: 'rmvpe',
|
|
57
|
-
indexRate: 0.75
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const t0 = Date.now();
|
|
61
|
-
const res = mockRes();
|
|
62
|
-
try {
|
|
63
|
-
await speakRoute.handler(
|
|
64
|
-
mockReq('/dsh-tts-api/speak', JSON.stringify({ text: '这是通过插件链路转换出的 azusa 音色,用来验证端到端流程。', provider: 'rvc', custom })),
|
|
65
|
-
res
|
|
66
|
-
);
|
|
67
|
-
} catch (e) {
|
|
68
|
-
console.error('speak handler threw:', e);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
console.log('res.head:', JSON.stringify(res.head));
|
|
72
|
-
console.log('res.body type:', typeof res.body, res.body === null ? 'null' : String(res.body).slice(0, 200));
|
|
73
|
-
if (!res.body) process.exit(1);
|
|
74
|
-
const parsed = JSON.parse(res.body);
|
|
75
|
-
console.log(`speak -> ${res.head.c} ${parsed.url ?? parsed.error} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
|
|
76
|
-
if (!parsed.url) process.exit(1);
|
|
77
|
-
|
|
78
|
-
const ares = mockRes();
|
|
79
|
-
await audioRoute.handler(mockReq(parsed.url), ares);
|
|
80
|
-
const bytes = Buffer.isBuffer(ares.body) ? ares.body : Buffer.from(ares.body ?? '');
|
|
81
|
-
console.log(`audio -> ${ares.head.c} ${bytes.length} bytes, head=${bytes.slice(0, 4).toString()}`);
|
|
82
|
-
if (!(bytes.length > 1000 && bytes.slice(0, 4).toString() === 'RIFF')) process.exit(1);
|
|
83
|
-
|
|
84
|
-
// ---- adaptive chunked progressive playback (long text) ----
|
|
85
|
-
const nextRoute = routes.find((r) => r.kind === 'exact' && r.path === '/dsh-tts-api/rvc-next');
|
|
86
|
-
if (!nextRoute) {
|
|
87
|
-
console.error('rvc-next route missing');
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
const longText = '这是一段用来验证自适应分块渐进播放的长文本,每一块都会先由 Edge 合成,再交给本地 RVC 服务转换,播放的同时后台继续合成后面的段落,从而做到长文朗读不卡顿。'.repeat(3);
|
|
91
|
-
const t1 = Date.now();
|
|
92
|
-
const longRes = mockRes();
|
|
93
|
-
await speakRoute.handler(
|
|
94
|
-
mockReq('/dsh-tts-api/speak', JSON.stringify({ text: longText, provider: 'rvc', custom })),
|
|
95
|
-
longRes
|
|
96
|
-
);
|
|
97
|
-
const longParsed = JSON.parse(longRes.body);
|
|
98
|
-
console.log(`long speak -> ${longRes.head.c} jobId=${longParsed.jobId ?? ''} chunks=${longParsed.chunks?.length ?? 0} total=${longParsed.total ?? 0} ratio=${longParsed.ratio ?? 'n/a'} chunkSec=${longParsed.chunkSec ?? 'n/a'} (${((Date.now() - t1) / 1000).toFixed(1)}s)`);
|
|
99
|
-
if (!longParsed.jobId || !Array.isArray(longParsed.chunks) || longParsed.chunks.length < 2) process.exit(1);
|
|
100
|
-
let fetched = longParsed.chunks.length;
|
|
101
|
-
let more = true;
|
|
102
|
-
const chunkStarts = [];
|
|
103
|
-
while (more) {
|
|
104
|
-
const nr = mockRes();
|
|
105
|
-
await nextRoute.handler(mockReq(`/dsh-tts-api/rvc-next?job=${longParsed.jobId}`), nr);
|
|
106
|
-
const np = JSON.parse(nr.body);
|
|
107
|
-
if (np && np.url) {
|
|
108
|
-
fetched++;
|
|
109
|
-
chunkStarts.push(np.url);
|
|
110
|
-
}
|
|
111
|
-
more = !!(np && np.more);
|
|
112
|
-
}
|
|
113
|
-
console.log(`rvc-next drained: fetched=${fetched} total=${longParsed.total} (${((Date.now() - t1) / 1000).toFixed(1)}s)`);
|
|
114
|
-
if (fetched !== longParsed.total) process.exit(1);
|
|
115
|
-
// verify one late chunk serves wav
|
|
116
|
-
const lastRes = mockRes();
|
|
117
|
-
await audioRoute.handler(mockReq(chunkStarts[chunkStarts.length - 1]), lastRes);
|
|
118
|
-
const lastBytes = Buffer.isBuffer(lastRes.body) ? lastRes.body : Buffer.from(lastRes.body ?? '');
|
|
119
|
-
console.log(`late chunk audio -> ${lastRes.head.c} ${lastBytes.length} bytes, head=${lastBytes.slice(0, 4).toString()}`);
|
|
120
|
-
if (!(lastBytes.length > 1000 && lastBytes.slice(0, 4).toString() === 'RIFF')) process.exit(1);
|
|
121
|
-
|
|
122
|
-
process.exit(0);
|
|
1
|
+
// One-off real end-to-end: plugin Host -> real rvc-server (azusa-test) chain.
|
|
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
|
+
// Verifies Edge base synth + real RVC conversion + audio route serving.
|
|
9
|
+
import * as plugin from '../lib/index.mjs';
|
|
10
|
+
|
|
11
|
+
const routes = [];
|
|
12
|
+
const ctx = {
|
|
13
|
+
get(name) {
|
|
14
|
+
if (name === 'webServer') {
|
|
15
|
+
return {
|
|
16
|
+
register(route) {
|
|
17
|
+
routes.push(route);
|
|
18
|
+
return () => {
|
|
19
|
+
const i = routes.indexOf(route);
|
|
20
|
+
if (i >= 0) routes.splice(i, 1);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
},
|
|
27
|
+
effect() {}
|
|
28
|
+
};
|
|
29
|
+
plugin.apply(ctx);
|
|
30
|
+
|
|
31
|
+
const speakRoute = routes.find((r) => r.kind === 'exact' && r.path === '/dsh-tts-api/speak');
|
|
32
|
+
const audioRoute = routes.find((r) => r.kind === 'prefix' && r.path === '/dsh-tts-audio');
|
|
33
|
+
|
|
34
|
+
function mockReq(url, body) {
|
|
35
|
+
const chunks = body === undefined ? [] : [body];
|
|
36
|
+
return {
|
|
37
|
+
url,
|
|
38
|
+
[Symbol.asyncIterator]() {
|
|
39
|
+
let i = 0;
|
|
40
|
+
return {
|
|
41
|
+
next: () => (i < chunks.length ? Promise.resolve({ value: chunks[i++], done: false }) : Promise.resolve({ done: true }))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mockRes() {
|
|
47
|
+
return { headersSent: false, head: null, body: null, writeHead(c, h) { this.head = { c, h }; }, end(b) { this.body = b; } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const RVC_DIR = 'E:/AI/RVC20240604Nvidia/RVC20240604Nvidia';
|
|
51
|
+
const custom = {
|
|
52
|
+
baseUrl: 'http://127.0.0.1:4892',
|
|
53
|
+
model: `${RVC_DIR}/assets/weights/azusa-test.pth`,
|
|
54
|
+
index: `${RVC_DIR}/assets/indices/azusa-test_IVF3317_Flat_nprobe_1_azusa-test_v2.index`,
|
|
55
|
+
baseVoice: 'zh-CN-YunyangNeural',
|
|
56
|
+
f0Method: 'rmvpe',
|
|
57
|
+
indexRate: 0.75
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const t0 = Date.now();
|
|
61
|
+
const res = mockRes();
|
|
62
|
+
try {
|
|
63
|
+
await speakRoute.handler(
|
|
64
|
+
mockReq('/dsh-tts-api/speak', JSON.stringify({ text: '这是通过插件链路转换出的 azusa 音色,用来验证端到端流程。', provider: 'rvc', custom })),
|
|
65
|
+
res
|
|
66
|
+
);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error('speak handler threw:', e);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
console.log('res.head:', JSON.stringify(res.head));
|
|
72
|
+
console.log('res.body type:', typeof res.body, res.body === null ? 'null' : String(res.body).slice(0, 200));
|
|
73
|
+
if (!res.body) process.exit(1);
|
|
74
|
+
const parsed = JSON.parse(res.body);
|
|
75
|
+
console.log(`speak -> ${res.head.c} ${parsed.url ?? parsed.error} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
|
|
76
|
+
if (!parsed.url) process.exit(1);
|
|
77
|
+
|
|
78
|
+
const ares = mockRes();
|
|
79
|
+
await audioRoute.handler(mockReq(parsed.url), ares);
|
|
80
|
+
const bytes = Buffer.isBuffer(ares.body) ? ares.body : Buffer.from(ares.body ?? '');
|
|
81
|
+
console.log(`audio -> ${ares.head.c} ${bytes.length} bytes, head=${bytes.slice(0, 4).toString()}`);
|
|
82
|
+
if (!(bytes.length > 1000 && bytes.slice(0, 4).toString() === 'RIFF')) process.exit(1);
|
|
83
|
+
|
|
84
|
+
// ---- adaptive chunked progressive playback (long text) ----
|
|
85
|
+
const nextRoute = routes.find((r) => r.kind === 'exact' && r.path === '/dsh-tts-api/rvc-next');
|
|
86
|
+
if (!nextRoute) {
|
|
87
|
+
console.error('rvc-next route missing');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const longText = '这是一段用来验证自适应分块渐进播放的长文本,每一块都会先由 Edge 合成,再交给本地 RVC 服务转换,播放的同时后台继续合成后面的段落,从而做到长文朗读不卡顿。'.repeat(3);
|
|
91
|
+
const t1 = Date.now();
|
|
92
|
+
const longRes = mockRes();
|
|
93
|
+
await speakRoute.handler(
|
|
94
|
+
mockReq('/dsh-tts-api/speak', JSON.stringify({ text: longText, provider: 'rvc', custom })),
|
|
95
|
+
longRes
|
|
96
|
+
);
|
|
97
|
+
const longParsed = JSON.parse(longRes.body);
|
|
98
|
+
console.log(`long speak -> ${longRes.head.c} jobId=${longParsed.jobId ?? ''} chunks=${longParsed.chunks?.length ?? 0} total=${longParsed.total ?? 0} ratio=${longParsed.ratio ?? 'n/a'} chunkSec=${longParsed.chunkSec ?? 'n/a'} (${((Date.now() - t1) / 1000).toFixed(1)}s)`);
|
|
99
|
+
if (!longParsed.jobId || !Array.isArray(longParsed.chunks) || longParsed.chunks.length < 2) process.exit(1);
|
|
100
|
+
let fetched = longParsed.chunks.length;
|
|
101
|
+
let more = true;
|
|
102
|
+
const chunkStarts = [];
|
|
103
|
+
while (more) {
|
|
104
|
+
const nr = mockRes();
|
|
105
|
+
await nextRoute.handler(mockReq(`/dsh-tts-api/rvc-next?job=${longParsed.jobId}`), nr);
|
|
106
|
+
const np = JSON.parse(nr.body);
|
|
107
|
+
if (np && np.url) {
|
|
108
|
+
fetched++;
|
|
109
|
+
chunkStarts.push(np.url);
|
|
110
|
+
}
|
|
111
|
+
more = !!(np && np.more);
|
|
112
|
+
}
|
|
113
|
+
console.log(`rvc-next drained: fetched=${fetched} total=${longParsed.total} (${((Date.now() - t1) / 1000).toFixed(1)}s)`);
|
|
114
|
+
if (fetched !== longParsed.total) process.exit(1);
|
|
115
|
+
// verify one late chunk serves wav
|
|
116
|
+
const lastRes = mockRes();
|
|
117
|
+
await audioRoute.handler(mockReq(chunkStarts[chunkStarts.length - 1]), lastRes);
|
|
118
|
+
const lastBytes = Buffer.isBuffer(lastRes.body) ? lastRes.body : Buffer.from(lastRes.body ?? '');
|
|
119
|
+
console.log(`late chunk audio -> ${lastRes.head.c} ${lastBytes.length} bytes, head=${lastBytes.slice(0, 4).toString()}`);
|
|
120
|
+
if (!(lastBytes.length > 1000 && lastBytes.slice(0, 4).toString() === 'RIFF')) process.exit(1);
|
|
121
|
+
|
|
122
|
+
process.exit(0);
|
package/tests/i18n-keys.mjs
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
// i18n regression: every t("key") used in lib/client.js must exist in both zh
|
|
2
|
-
// and en dictionaries; both dictionaries must be identical in key sets; and
|
|
3
|
-
// no CJK UI literal may remain outside the dictionary.
|
|
4
|
-
import { readFileSync } from 'node:fs';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
|
|
8
|
-
const client = readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'client.js'), 'utf8');
|
|
9
|
-
const results = [];
|
|
10
|
-
function check(name, ok, detail) { results.push({ name, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); }
|
|
11
|
-
|
|
12
|
-
function extractDict(lang) {
|
|
13
|
-
const lineMarker = ` ${lang}: {`;
|
|
14
|
-
const start = client.indexOf(lineMarker);
|
|
15
|
-
if (start < 0) throw new Error('no dict.' + lang);
|
|
16
|
-
const bodyStart = client.indexOf('{', start);
|
|
17
|
-
let i = bodyStart, depth = 0;
|
|
18
|
-
for (; i < client.length; i++) {
|
|
19
|
-
if (client[i] === '{') depth++;
|
|
20
|
-
else if (client[i] === '}') { depth--; if (depth === 0) break; }
|
|
21
|
-
}
|
|
22
|
-
const block = client.slice(bodyStart, i + 1);
|
|
23
|
-
const re = /"([^"]+)":\s*"((?:[^"\\]|\\.)*)"/g;
|
|
24
|
-
const out = {};
|
|
25
|
-
let m;
|
|
26
|
-
while ((m = re.exec(block))) out[m[1]] = m[2];
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
let zh, en, zhKeys, enKeys;
|
|
30
|
-
try {
|
|
31
|
-
zh = extractDict('zh'); en = extractDict('en');
|
|
32
|
-
zhKeys = new Set(Object.keys(zh)); enKeys = new Set(Object.keys(en));
|
|
33
|
-
check('i18n dictionaries exist & non-empty', zhKeys.size > 0 && enKeys.size > 0, `zh=${zhKeys.size} en=${enKeys.size}`);
|
|
34
|
-
const missingEn = [...zhKeys].filter(k => !enKeys.has(k));
|
|
35
|
-
const missingZh = [...enKeys].filter(k => !zhKeys.has(k));
|
|
36
|
-
check('zh and en have identical key sets', missingEn.length === 0 && missingZh.length === 0,
|
|
37
|
-
missingEn.length ? 'en missing: ' + missingEn.join(',') : (missingZh.length ? 'zh missing: ' + missingZh.join(',') : undefined));
|
|
38
|
-
} catch (e) {
|
|
39
|
-
check('i18n dictionaries parse', false, String(e.message));
|
|
40
|
-
zhKeys = new Set(); enKeys = new Set();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// every real t("key") usage (word-boundary t call) must be defined in both
|
|
44
|
-
const used = new Set();
|
|
45
|
-
const tRe = /(^|[^A-Za-z0-9_$.])t\(\s*"([^"]+)"\s*(?:\)|,)/g;
|
|
46
|
-
let mm;
|
|
47
|
-
while ((mm = tRe.exec(client))) used.add(mm[2]);
|
|
48
|
-
const missingDef = [...used].filter(k => !zhKeys.has(k) || !enKeys.has(k));
|
|
49
|
-
check('all t("key") usages have zh+en definitions', missingDef.length === 0, missingDef.length ? 'missing: ' + missingDef.join(',') : `${used.size} keys used`);
|
|
50
|
-
|
|
51
|
-
// no CJK literal remains outside the dictionary (UI fully localized)
|
|
52
|
-
const i18nEnd = client.lastIndexOf('// ------------------------------------------------------------ /i18n');
|
|
53
|
-
const render = client.slice(i18nEnd + 1);
|
|
54
|
-
const cjkRe = /"[^"\n]*[\u4e00-\u9fff][^"\n]*"/g;
|
|
55
|
-
const cjk = [];
|
|
56
|
-
let c;
|
|
57
|
-
while ((c = cjkRe.exec(render))) {
|
|
58
|
-
const lineStart = render.lastIndexOf('\n', c.index) + 1;
|
|
59
|
-
const line = render.slice(lineStart, c.index);
|
|
60
|
-
if (!line.trim().startsWith('//')) cjk.push(c[0]);
|
|
61
|
-
}
|
|
62
|
-
check('no CJK UI literal left outside dictionary', cjk.length === 0, cjk.length ? cjk.slice(0, 10).join(' | ') : undefined);
|
|
63
|
-
|
|
64
|
-
// dictionary has no dead keys (excluding voice.*/baseVoice.* data labels and lang labels used only in the switcher)
|
|
65
|
-
// host.* keys are resolved DYNAMICALLY (hostErrText(t(i18n.code)) and
|
|
66
|
-
// t("host.phase." + phaseKey)) so they won't appear as literal t("host.x")
|
|
67
|
-
// calls; they are part of the host<->client i18n contract. Exclude them here.
|
|
68
|
-
// cloud.usage.* tier keys are likewise dynamic (t("cloud.usage." + tier)).
|
|
69
|
-
const dead = [...zhKeys].filter(k =>
|
|
70
|
-
used.has(k) ? false
|
|
71
|
-
: /^voice\.|^baseVoice\.|^host\.|^cloud\.usage\.(standard|wavenet|neural2|chirp3)$/.test(k) ? false
|
|
72
|
-
: true);
|
|
73
|
-
check('dictionary has no dead keys (excluding voice/baseVoice/host data)', dead.length === 0, dead.length ? 'dead: ' + dead.join(',') : undefined);
|
|
74
|
-
// host.* keys form a complete bi-dictionary set (already covered by the
|
|
75
|
-
// identical-key-sets check); also ensure the handful the client references
|
|
76
|
-
// statically are present.
|
|
77
|
-
const hostStatic = ["host.phase.model", "host.phase.index", "host.phase.prepare"];
|
|
78
|
-
check('host.* phase keys present in both langs', hostStatic.every(k => zhKeys.has(k) && enKeys.has(k)));
|
|
79
|
-
|
|
80
|
-
const failed = results.filter(r => !r.ok);
|
|
81
|
-
console.log(`\n${results.length - failed.length}/${results.length} i18n checks passed`);
|
|
82
|
-
process.exit(failed.length === 0 ? 0 : 1);
|
|
1
|
+
// i18n regression: every t("key") used in lib/client.js must exist in both zh
|
|
2
|
+
// and en dictionaries; both dictionaries must be identical in key sets; and
|
|
3
|
+
// no CJK UI literal may remain outside the dictionary.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const client = readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'client.js'), 'utf8');
|
|
9
|
+
const results = [];
|
|
10
|
+
function check(name, ok, detail) { results.push({ name, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); }
|
|
11
|
+
|
|
12
|
+
function extractDict(lang) {
|
|
13
|
+
const lineMarker = ` ${lang}: {`;
|
|
14
|
+
const start = client.indexOf(lineMarker);
|
|
15
|
+
if (start < 0) throw new Error('no dict.' + lang);
|
|
16
|
+
const bodyStart = client.indexOf('{', start);
|
|
17
|
+
let i = bodyStart, depth = 0;
|
|
18
|
+
for (; i < client.length; i++) {
|
|
19
|
+
if (client[i] === '{') depth++;
|
|
20
|
+
else if (client[i] === '}') { depth--; if (depth === 0) break; }
|
|
21
|
+
}
|
|
22
|
+
const block = client.slice(bodyStart, i + 1);
|
|
23
|
+
const re = /"([^"]+)":\s*"((?:[^"\\]|\\.)*)"/g;
|
|
24
|
+
const out = {};
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = re.exec(block))) out[m[1]] = m[2];
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
let zh, en, zhKeys, enKeys;
|
|
30
|
+
try {
|
|
31
|
+
zh = extractDict('zh'); en = extractDict('en');
|
|
32
|
+
zhKeys = new Set(Object.keys(zh)); enKeys = new Set(Object.keys(en));
|
|
33
|
+
check('i18n dictionaries exist & non-empty', zhKeys.size > 0 && enKeys.size > 0, `zh=${zhKeys.size} en=${enKeys.size}`);
|
|
34
|
+
const missingEn = [...zhKeys].filter(k => !enKeys.has(k));
|
|
35
|
+
const missingZh = [...enKeys].filter(k => !zhKeys.has(k));
|
|
36
|
+
check('zh and en have identical key sets', missingEn.length === 0 && missingZh.length === 0,
|
|
37
|
+
missingEn.length ? 'en missing: ' + missingEn.join(',') : (missingZh.length ? 'zh missing: ' + missingZh.join(',') : undefined));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
check('i18n dictionaries parse', false, String(e.message));
|
|
40
|
+
zhKeys = new Set(); enKeys = new Set();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// every real t("key") usage (word-boundary t call) must be defined in both
|
|
44
|
+
const used = new Set();
|
|
45
|
+
const tRe = /(^|[^A-Za-z0-9_$.])t\(\s*"([^"]+)"\s*(?:\)|,)/g;
|
|
46
|
+
let mm;
|
|
47
|
+
while ((mm = tRe.exec(client))) used.add(mm[2]);
|
|
48
|
+
const missingDef = [...used].filter(k => !zhKeys.has(k) || !enKeys.has(k));
|
|
49
|
+
check('all t("key") usages have zh+en definitions', missingDef.length === 0, missingDef.length ? 'missing: ' + missingDef.join(',') : `${used.size} keys used`);
|
|
50
|
+
|
|
51
|
+
// no CJK literal remains outside the dictionary (UI fully localized)
|
|
52
|
+
const i18nEnd = client.lastIndexOf('// ------------------------------------------------------------ /i18n');
|
|
53
|
+
const render = client.slice(i18nEnd + 1);
|
|
54
|
+
const cjkRe = /"[^"\n]*[\u4e00-\u9fff][^"\n]*"/g;
|
|
55
|
+
const cjk = [];
|
|
56
|
+
let c;
|
|
57
|
+
while ((c = cjkRe.exec(render))) {
|
|
58
|
+
const lineStart = render.lastIndexOf('\n', c.index) + 1;
|
|
59
|
+
const line = render.slice(lineStart, c.index);
|
|
60
|
+
if (!line.trim().startsWith('//')) cjk.push(c[0]);
|
|
61
|
+
}
|
|
62
|
+
check('no CJK UI literal left outside dictionary', cjk.length === 0, cjk.length ? cjk.slice(0, 10).join(' | ') : undefined);
|
|
63
|
+
|
|
64
|
+
// dictionary has no dead keys (excluding voice.*/baseVoice.* data labels and lang labels used only in the switcher)
|
|
65
|
+
// host.* keys are resolved DYNAMICALLY (hostErrText(t(i18n.code)) and
|
|
66
|
+
// t("host.phase." + phaseKey)) so they won't appear as literal t("host.x")
|
|
67
|
+
// calls; they are part of the host<->client i18n contract. Exclude them here.
|
|
68
|
+
// cloud.usage.* tier keys are likewise dynamic (t("cloud.usage." + tier)).
|
|
69
|
+
const dead = [...zhKeys].filter(k =>
|
|
70
|
+
used.has(k) ? false
|
|
71
|
+
: /^voice\.|^baseVoice\.|^host\.|^cloud\.usage\.(standard|wavenet|neural2|chirp3)$/.test(k) ? false
|
|
72
|
+
: true);
|
|
73
|
+
check('dictionary has no dead keys (excluding voice/baseVoice/host data)', dead.length === 0, dead.length ? 'dead: ' + dead.join(',') : undefined);
|
|
74
|
+
// host.* keys form a complete bi-dictionary set (already covered by the
|
|
75
|
+
// identical-key-sets check); also ensure the handful the client references
|
|
76
|
+
// statically are present.
|
|
77
|
+
const hostStatic = ["host.phase.model", "host.phase.index", "host.phase.prepare"];
|
|
78
|
+
check('host.* phase keys present in both langs', hostStatic.every(k => zhKeys.has(k) && enKeys.has(k)));
|
|
79
|
+
|
|
80
|
+
const failed = results.filter(r => !r.ok);
|
|
81
|
+
console.log(`\n${results.length - failed.length}/${results.length} i18n checks passed`);
|
|
82
|
+
process.exit(failed.length === 0 ? 0 : 1);
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
// Boundary test for tools/package-runtime.py's macOS-only PyAV patch helper
|
|
2
|
-
// (patch_infer_for_pyav): it must rewrite av.open("rb"/"wb") -> "r"/"w" in the
|
|
3
|
-
// packaged infer/lib/audio.py on darwin, but leave a normal Python `open(
|
|
4
|
-
// file, "rb")` (a real file open) untouched. Verified end-to-end too by
|
|
5
|
-
// node tests/rvc-server-live.mjs (live /convert)
|
|
6
|
-
// Run: node tests/package-runtime-patch.mjs
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
|
-
import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
|
|
13
|
-
const results = [];
|
|
14
|
-
function check(name, ok, detail) {
|
|
15
|
-
results.push({ name, ok, detail });
|
|
16
|
-
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
-
const pkg = path.join(repo, 'tools', 'package-runtime.py');
|
|
21
|
-
// Run the driver with python3 when available, else fall back to `python` and
|
|
22
|
-
// then to the Windows `py -3` launcher. Windows installs often only ship
|
|
23
|
-
// `python`, and a bare `python`/`python3` may be a dead Microsoft Store stub
|
|
24
|
-
// that exits 9009, so the `py` launcher is the reliable last resort.
|
|
25
|
-
function runPython(driverPath) {
|
|
26
|
-
const tries = [
|
|
27
|
-
['python3', []],
|
|
28
|
-
['python', []],
|
|
29
|
-
['py', ['-3']]
|
|
30
|
-
];
|
|
31
|
-
for (const [cmd, pre] of tries) {
|
|
32
|
-
try {
|
|
33
|
-
return execFileSync(cmd, [...pre, driverPath], { encoding: 'utf8' });
|
|
34
|
-
} catch (e) {
|
|
35
|
-
if (cmd === 'py') throw e; // give up after the last fallback
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// sample = the unpatched (as-released) audio.py
|
|
40
|
-
const sample = [
|
|
41
|
-
'import av',
|
|
42
|
-
'import numpy as np',
|
|
43
|
-
'def wav2(i, o, format):',
|
|
44
|
-
' inp = av.open(i, "rb")',
|
|
45
|
-
' out = av.open(o, "wb", format=format)',
|
|
46
|
-
' return inp, out',
|
|
47
|
-
'def audio2(i, o, format, sr):',
|
|
48
|
-
' inp = av.open(i, "rb")',
|
|
49
|
-
' out = av.open(o, "wb", format=format)',
|
|
50
|
-
' return inp, out',
|
|
51
|
-
'def load_audio(file, sr):',
|
|
52
|
-
' with open(file, "rb") as f:',
|
|
53
|
-
' pass',
|
|
54
|
-
''
|
|
55
|
-
].join('\n');
|
|
56
|
-
|
|
57
|
-
const dir = mkdtempSync(path.join(os.tmpdir(), 'dsh-tts-pyav-'));
|
|
58
|
-
try {
|
|
59
|
-
const libDir = path.join(dir, 'infer', 'lib');
|
|
60
|
-
mkdirSync(libDir, { recursive: true });
|
|
61
|
-
const audioPath = path.join(libDir, 'audio.py');
|
|
62
|
-
writeFileSync(audioPath, sample);
|
|
63
|
-
|
|
64
|
-
const driver = path.join(dir, 'driver.py');
|
|
65
|
-
writeFileSync(driver, `
|
|
66
|
-
import importlib.util
|
|
67
|
-
spec = importlib.util.spec_from_file_location("prt", ${JSON.stringify(pkg)})
|
|
68
|
-
m = importlib.util.module_from_spec(spec)
|
|
69
|
-
spec.loader.exec_module(m)
|
|
70
|
-
m.patch_infer_for_pyav(${JSON.stringify(path.join(dir, 'infer'))})
|
|
71
|
-
`);
|
|
72
|
-
runPython(driver);
|
|
73
|
-
|
|
74
|
-
const out = readFileSync(audioPath, 'utf8');
|
|
75
|
-
check('darwin patch replaces av.open rb/wb', !/av\.open\([^)]*"(rb|wb)"/.test(out), out.split('\n').filter(l => l.includes('av.open')).join(' | '));
|
|
76
|
-
check('darwin patch keeps Python open(file,"rb")', out.includes('with open(file, "rb") as f:'));
|
|
77
|
-
check('darwin patch converts to r/w', out.includes('av.open(i, "r")') && out.includes('av.open(o, "w", format=format)'));
|
|
78
|
-
|
|
79
|
-
// idempotency: running twice changes nothing
|
|
80
|
-
runPython(driver);
|
|
81
|
-
check('patch idempotent (second run unchanged)', readFileSync(audioPath, 'utf8') === out);
|
|
82
|
-
} finally {
|
|
83
|
-
rmSync(dir, { recursive: true, force: true });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const failed = results.filter(r => !r.ok);
|
|
87
|
-
console.log(`\n${results.length - failed.length}/${results.length} patch checks passed`);
|
|
88
|
-
process.exit(failed.length === 0 ? 0 : 1);
|
|
1
|
+
// Boundary test for tools/package-runtime.py's macOS-only PyAV patch helper
|
|
2
|
+
// (patch_infer_for_pyav): it must rewrite av.open("rb"/"wb") -> "r"/"w" in the
|
|
3
|
+
// packaged infer/lib/audio.py on darwin, but leave a normal Python `open(
|
|
4
|
+
// file, "rb")` (a real file open) untouched. Verified end-to-end too by
|
|
5
|
+
// node tests/rvc-server-live.mjs (live /convert)
|
|
6
|
+
// Run: node tests/package-runtime-patch.mjs
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const results = [];
|
|
14
|
+
function check(name, ok, detail) {
|
|
15
|
+
results.push({ name, ok, detail });
|
|
16
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const pkg = path.join(repo, 'tools', 'package-runtime.py');
|
|
21
|
+
// Run the driver with python3 when available, else fall back to `python` and
|
|
22
|
+
// then to the Windows `py -3` launcher. Windows installs often only ship
|
|
23
|
+
// `python`, and a bare `python`/`python3` may be a dead Microsoft Store stub
|
|
24
|
+
// that exits 9009, so the `py` launcher is the reliable last resort.
|
|
25
|
+
function runPython(driverPath) {
|
|
26
|
+
const tries = [
|
|
27
|
+
['python3', []],
|
|
28
|
+
['python', []],
|
|
29
|
+
['py', ['-3']]
|
|
30
|
+
];
|
|
31
|
+
for (const [cmd, pre] of tries) {
|
|
32
|
+
try {
|
|
33
|
+
return execFileSync(cmd, [...pre, driverPath], { encoding: 'utf8' });
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (cmd === 'py') throw e; // give up after the last fallback
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// sample = the unpatched (as-released) audio.py
|
|
40
|
+
const sample = [
|
|
41
|
+
'import av',
|
|
42
|
+
'import numpy as np',
|
|
43
|
+
'def wav2(i, o, format):',
|
|
44
|
+
' inp = av.open(i, "rb")',
|
|
45
|
+
' out = av.open(o, "wb", format=format)',
|
|
46
|
+
' return inp, out',
|
|
47
|
+
'def audio2(i, o, format, sr):',
|
|
48
|
+
' inp = av.open(i, "rb")',
|
|
49
|
+
' out = av.open(o, "wb", format=format)',
|
|
50
|
+
' return inp, out',
|
|
51
|
+
'def load_audio(file, sr):',
|
|
52
|
+
' with open(file, "rb") as f:',
|
|
53
|
+
' pass',
|
|
54
|
+
''
|
|
55
|
+
].join('\n');
|
|
56
|
+
|
|
57
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), 'dsh-tts-pyav-'));
|
|
58
|
+
try {
|
|
59
|
+
const libDir = path.join(dir, 'infer', 'lib');
|
|
60
|
+
mkdirSync(libDir, { recursive: true });
|
|
61
|
+
const audioPath = path.join(libDir, 'audio.py');
|
|
62
|
+
writeFileSync(audioPath, sample);
|
|
63
|
+
|
|
64
|
+
const driver = path.join(dir, 'driver.py');
|
|
65
|
+
writeFileSync(driver, `
|
|
66
|
+
import importlib.util
|
|
67
|
+
spec = importlib.util.spec_from_file_location("prt", ${JSON.stringify(pkg)})
|
|
68
|
+
m = importlib.util.module_from_spec(spec)
|
|
69
|
+
spec.loader.exec_module(m)
|
|
70
|
+
m.patch_infer_for_pyav(${JSON.stringify(path.join(dir, 'infer'))})
|
|
71
|
+
`);
|
|
72
|
+
runPython(driver);
|
|
73
|
+
|
|
74
|
+
const out = readFileSync(audioPath, 'utf8');
|
|
75
|
+
check('darwin patch replaces av.open rb/wb', !/av\.open\([^)]*"(rb|wb)"/.test(out), out.split('\n').filter(l => l.includes('av.open')).join(' | '));
|
|
76
|
+
check('darwin patch keeps Python open(file,"rb")', out.includes('with open(file, "rb") as f:'));
|
|
77
|
+
check('darwin patch converts to r/w', out.includes('av.open(i, "r")') && out.includes('av.open(o, "w", format=format)'));
|
|
78
|
+
|
|
79
|
+
// idempotency: running twice changes nothing
|
|
80
|
+
runPython(driver);
|
|
81
|
+
check('patch idempotent (second run unchanged)', readFileSync(audioPath, 'utf8') === out);
|
|
82
|
+
} finally {
|
|
83
|
+
rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const failed = results.filter(r => !r.ok);
|
|
87
|
+
console.log(`\n${results.length - failed.length}/${results.length} patch checks passed`);
|
|
88
|
+
process.exit(failed.length === 0 ? 0 : 1);
|