@oxiaom/adoremix 1.0.19 → 1.0.20
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/package.json +1 -1
- package/src/cli.js +27 -4
- package/src/tts-deps.js +22 -7
- package/templates/adoremix.service.tmpl +1 -1
- package/tts/providers/edge.js +48 -21
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -308,15 +308,38 @@ function buildProgram() {
|
|
|
308
308
|
.option('--file <path>', '指定日志文件')
|
|
309
309
|
.option('-n, --lines <n>', '初始打印行数', parseInt, 50)
|
|
310
310
|
.action((opts) => {
|
|
311
|
-
const
|
|
312
|
-
const
|
|
311
|
+
const workdir = resolveWorkdir(opts.workdir);
|
|
312
|
+
const wp = paths.workdirPaths(workdir);
|
|
313
313
|
const fs = require('fs');
|
|
314
314
|
const { spawn } = require('child_process');
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
// 默认日志文件:选最近更新的。
|
|
316
|
+
// systemd 模式 → logs/svc.log(unit 模板 StandardOutput=append 写这里)
|
|
317
|
+
// 前台/daemon 模式 → var/app.log(spawn 重定向)
|
|
318
|
+
// 用户可用 --file 覆盖。
|
|
319
|
+
let file = opts.file;
|
|
320
|
+
if (!file) {
|
|
321
|
+
const candidates = [
|
|
322
|
+
path.join(workdir, 'logs', 'svc.err'), // systemd 模式:二进制实时输出(stderr,更新最频繁)
|
|
323
|
+
path.join(workdir, 'logs', 'svc.log'), // systemd 模式:node logger(stdout)
|
|
324
|
+
wp.logfile // 前台/daemon 模式:var/app.log
|
|
325
|
+
];
|
|
326
|
+
let bestMtime = 0;
|
|
327
|
+
for (const f of candidates) {
|
|
328
|
+
try {
|
|
329
|
+
const m = fs.statSync(f).mtimeMs;
|
|
330
|
+
if (m > bestMtime) { bestMtime = m; file = f; }
|
|
331
|
+
} catch (e) {}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!file || !fs.existsSync(file)) {
|
|
335
|
+
logger.warn(`日志文件不存在(找了 logs/svc.err、logs/svc.log、${wp.logfile})`);
|
|
336
|
+
logger.log('提示:systemd 模式也可用 journalctl -u adoremix -f');
|
|
317
337
|
process.exitCode = 1;
|
|
318
338
|
return;
|
|
319
339
|
}
|
|
340
|
+
if (file !== wp.logfile && !opts.file) {
|
|
341
|
+
logger.log(`(自动选中 ${path.relative(workdir, file)},--file 可指定其他)`);
|
|
342
|
+
}
|
|
320
343
|
if (!opts.follow) {
|
|
321
344
|
const tail = spawn(process.platform === 'win32' ? 'more' : 'tail', process.platform === 'win32' ? [file] : ['-n', String(opts.lines), file], { stdio: 'inherit' });
|
|
322
345
|
tail.on('exit', (c) => process.exit(c || 0));
|
package/src/tts-deps.js
CHANGED
|
@@ -18,6 +18,10 @@ const { execSync, execFileSync } = require('child_process');
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
|
|
21
|
+
// 二进制调外部命令 lame 把 TTS 生成的音频转码成 mp3(源码 cmd = "lame -S"),
|
|
22
|
+
// 所有 provider 都需要,缺失时 node tts.js 生成 .txt 但 lame 转码 mp3 失败。
|
|
23
|
+
const TRANSCODE_TOOLS = [{ name: 'lame', cmd: 'lame' }];
|
|
24
|
+
|
|
21
25
|
const PROVIDER_DEPENDENCIES = {
|
|
22
26
|
xf: {
|
|
23
27
|
nodeModules: ['crypto-js', 'ws', 'log4node'],
|
|
@@ -28,14 +32,14 @@ const PROVIDER_DEPENDENCIES = {
|
|
|
28
32
|
],
|
|
29
33
|
systemPkgs: [],
|
|
30
34
|
pipPkgs: [],
|
|
31
|
-
executables:
|
|
35
|
+
executables: TRANSCODE_TOOLS
|
|
32
36
|
},
|
|
33
37
|
minimax: {
|
|
34
38
|
nodeModules: [], // 用 Node 内置 https
|
|
35
39
|
creds: [{ key: 'TTS.minimax_token', name: 'token' }],
|
|
36
40
|
systemPkgs: [],
|
|
37
41
|
pipPkgs: [],
|
|
38
|
-
executables:
|
|
42
|
+
executables: TRANSCODE_TOOLS
|
|
39
43
|
},
|
|
40
44
|
edge: {
|
|
41
45
|
nodeModules: [],
|
|
@@ -45,7 +49,7 @@ const PROVIDER_DEPENDENCIES = {
|
|
|
45
49
|
{ name: 'ffmpeg', cmd: 'ffmpeg' }
|
|
46
50
|
],
|
|
47
51
|
pipPkgs: [{ pkg: 'edge-tts', import: 'edge_tts' }],
|
|
48
|
-
executables: [{ name: 'ffmpeg', cmd: 'ffmpeg' }]
|
|
52
|
+
executables: [{ name: 'ffmpeg', cmd: 'ffmpeg' }, ...TRANSCODE_TOOLS]
|
|
49
53
|
}
|
|
50
54
|
};
|
|
51
55
|
|
|
@@ -155,8 +159,13 @@ function checkDeps(workdir, providerName, cfg) {
|
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
// 2. Node
|
|
159
|
-
|
|
162
|
+
// 2. Node 模块(provider 自己的 + dispatcher 公共依赖)
|
|
163
|
+
// dispatcher tts.js 所有 provider 都走,它 require ini 读 config,
|
|
164
|
+
// 缺失时二进制调 node tts.js 直接 MODULE_NOT_FOUND(TTS 全挂)。
|
|
165
|
+
// 常见于 --skip-npm-install 安装或 npm install 失败的场景。
|
|
166
|
+
const DISPATCHER_NODE_DEPS = ['ini'];
|
|
167
|
+
const allNodeMods = [...new Set([...deps.nodeModules, ...DISPATCHER_NODE_DEPS])];
|
|
168
|
+
for (const mod of allNodeMods) {
|
|
160
169
|
if (!nodeModuleInstalled(workdir, mod)) {
|
|
161
170
|
issues.push({
|
|
162
171
|
severity: 'error',
|
|
@@ -187,12 +196,13 @@ function checkDeps(workdir, providerName, cfg) {
|
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
// 4. 系统可执行
|
|
190
|
-
for (const exe of deps.
|
|
199
|
+
for (const exe of deps.executables || deps.systemPkgs) {
|
|
191
200
|
if (exe.cmd && !which(exe.cmd)) {
|
|
192
201
|
// 推断 apt 包名
|
|
193
202
|
let aptPkg = '';
|
|
194
203
|
if (exe.name === 'python3') aptPkg = 'python3 python3-pip';
|
|
195
204
|
else if (exe.name === 'ffmpeg') aptPkg = 'ffmpeg';
|
|
205
|
+
else if (exe.name === 'lame') aptPkg = 'lame';
|
|
196
206
|
const pkgMgr = detectAptLike();
|
|
197
207
|
issues.push({
|
|
198
208
|
severity: 'error',
|
|
@@ -235,7 +245,12 @@ function fixDeps(workdir, issues, opts) {
|
|
|
235
245
|
if (!issue.autoFixable) continue;
|
|
236
246
|
try {
|
|
237
247
|
if (issue.fixType === 'npm') {
|
|
238
|
-
|
|
248
|
+
// 国内默认 npm 源慢/易失败,走 npmmirror(ADOREMIX_NPM_REGISTRY 可覆盖,留空用官方源)
|
|
249
|
+
const registry = process.env.ADOREMIX_NPM_REGISTRY !== undefined
|
|
250
|
+
? process.env.ADOREMIX_NPM_REGISTRY
|
|
251
|
+
: 'https://registry.npmmirror.com';
|
|
252
|
+
const regFlag = registry ? ` --registry ${registry}` : '';
|
|
253
|
+
execSync(`npm install ${issue.fixArgs.mod}${regFlag} --no-audit --no-fund`, {
|
|
239
254
|
cwd: issue.fixArgs.cwd,
|
|
240
255
|
stdio: 'inherit'
|
|
241
256
|
});
|
|
@@ -16,7 +16,7 @@ ExecStart=__NODE__ __CLI__ start --workdir __WORKDIR__
|
|
|
16
16
|
Restart=on-failure
|
|
17
17
|
RestartSec=5
|
|
18
18
|
StandardOutput=append:__WORKDIR__/logs/svc.log
|
|
19
|
-
StandardError=append:__WORKDIR__/logs/svc.
|
|
19
|
+
StandardError=append:__WORKDIR__/logs/svc.log
|
|
20
20
|
LimitNOFILE=65536
|
|
21
21
|
|
|
22
22
|
[Install]
|
package/tts/providers/edge.js
CHANGED
|
@@ -15,26 +15,38 @@ const path = require('path');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
|
|
17
17
|
// 短名 → edge-tts voice 全名
|
|
18
|
+
// 注意:edge-tts 的 zh-CN voice 实际只有 8 个(--list-voices 确认):
|
|
19
|
+
// 女声 XiaoxiaoNeural(晓晓) XiaoyiNeural(晓伊)
|
|
20
|
+
// 男声 YunxiNeural(云希) YunjianNeural(云健) YunxiaNeural(云夏) YunyangNeural(云扬)
|
|
21
|
+
// 方言 liaoning-XiaobeiNeural shaanxi-XiaoniNeural
|
|
22
|
+
// 讯飞/Qt 传的很多短名在 edge 无对应,就近映射到同性别有效 voice;
|
|
23
|
+
// synthesize 另有 fallback 兜底(voice 调用失败回落 XiaoxiaoNeural)。
|
|
18
24
|
const VOICES = {
|
|
25
|
+
// 直接对应
|
|
19
26
|
xiaoxiao: 'zh-CN-XiaoxiaoNeural',
|
|
27
|
+
xiaoyi: 'zh-CN-XiaoyiNeural',
|
|
20
28
|
yunxi: 'zh-CN-YunxiNeural',
|
|
21
29
|
yunjian: 'zh-CN-YunjianNeural',
|
|
22
|
-
xiaoyi: 'zh-CN-XiaoyiNeural',
|
|
23
30
|
yunxia: 'zh-CN-YunxiaNeural',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
yunyang: 'zh-CN-YunyangNeural',
|
|
32
|
+
// edge 无对应,就近映射(女声→晓晓/晓伊,男声→云希/云健)
|
|
33
|
+
xiaoyan: 'zh-CN-XiaoxiaoNeural',
|
|
34
|
+
xiaochen: 'zh-CN-XiaoxiaoNeural',
|
|
35
|
+
xiaohan: 'zh-CN-XiaoxiaoNeural',
|
|
36
|
+
xiaomeng: 'zh-CN-XiaoyiNeural',
|
|
37
|
+
xiaomo: 'zh-CN-XiaoyiNeural',
|
|
38
|
+
xiaoqiu: 'zh-CN-XiaoxiaoNeural',
|
|
39
|
+
xiaorui: 'zh-CN-XiaoxiaoNeural',
|
|
40
|
+
xiaoshuang: 'zh-CN-XiaoyiNeural',
|
|
41
|
+
xiaoxuan: 'zh-CN-XiaoxiaoNeural',
|
|
42
|
+
xiaoyou: 'zh-CN-XiaoyiNeural',
|
|
43
|
+
yunfeng: 'zh-CN-YunxiNeural',
|
|
44
|
+
yunhao: 'zh-CN-YunxiNeural',
|
|
45
|
+
yunxiang: 'zh-CN-YunjianNeural',
|
|
46
|
+
// 讯飞原生发音人(Qt/WebUI 可能直接传讯飞名 aisxxx)→ 就近映射到 edge 有效 voice
|
|
47
|
+
aisjiuxu: 'zh-CN-YunxiNeural', // 许久(男)→ 云希
|
|
48
|
+
aisbabyxu: 'zh-CN-YunxiaNeural', // 男童 → 云夏
|
|
49
|
+
aisxuxin: 'zh-CN-YunxiNeural' // 晓旭(男)→ 云希
|
|
38
50
|
};
|
|
39
51
|
|
|
40
52
|
function findPython() {
|
|
@@ -77,7 +89,8 @@ function synthesize({ text, voice, volume, speed, outFile }, creds) {
|
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
const py = findPython();
|
|
80
|
-
const
|
|
92
|
+
const DEFAULT_VOICE = 'zh-CN-XiaoxiaoNeural';
|
|
93
|
+
const wantedVoice = VOICES[voice] || creds.edge_voice_override || DEFAULT_VOICE;
|
|
81
94
|
|
|
82
95
|
// edge-tts rate/volume 是百分比字符串,如 "+50%" / "-20%"
|
|
83
96
|
const sp = parseInt(speed) || 50;
|
|
@@ -91,8 +104,8 @@ function synthesize({ text, voice, volume, speed, outFile }, creds) {
|
|
|
91
104
|
const txtFile = path.join(tmpDir, `adoremix-edge-${Date.now()}.txt`);
|
|
92
105
|
fs.writeFileSync(txtFile, text);
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
// 跑一次 edge-tts(指定 voice),voice 无效/网络抖动时回落默认 voice 重试
|
|
108
|
+
function runEdge(voiceId) {
|
|
96
109
|
execFileSync(py, [
|
|
97
110
|
'-m', 'edge_tts',
|
|
98
111
|
'--voice', voiceId,
|
|
@@ -101,9 +114,23 @@ function synthesize({ text, voice, volume, speed, outFile }, creds) {
|
|
|
101
114
|
'-f', txtFile,
|
|
102
115
|
'--write-media', webmFile
|
|
103
116
|
], { stdio: 'pipe' });
|
|
117
|
+
if (!fs.existsSync(webmFile) || fs.statSync(webmFile).size === 0) {
|
|
118
|
+
throw new Error('edge-tts 未生成音频(voice 可能无效或网络问题)');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
104
121
|
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
let usedVoice = wantedVoice;
|
|
123
|
+
try {
|
|
124
|
+
try {
|
|
125
|
+
runEdge(wantedVoice);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// voice 在 edge-tts 不存在(微软列表动态变化)或网络抖动 → 回落默认 voice 重试一次
|
|
128
|
+
if (wantedVoice !== DEFAULT_VOICE) {
|
|
129
|
+
usedVoice = DEFAULT_VOICE;
|
|
130
|
+
runEdge(DEFAULT_VOICE);
|
|
131
|
+
} else {
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
// 2. ffmpeg 转 mp3
|
|
@@ -118,7 +145,7 @@ function synthesize({ text, voice, volume, speed, outFile }, creds) {
|
|
|
118
145
|
// 3. 清理临时
|
|
119
146
|
try { fs.unlinkSync(webmFile); fs.unlinkSync(txtFile); } catch (e) {}
|
|
120
147
|
|
|
121
|
-
resolve({ outFile, provider: 'edge', voice:
|
|
148
|
+
resolve({ outFile, provider: 'edge', voice: usedVoice });
|
|
122
149
|
} catch (e) {
|
|
123
150
|
try { fs.unlinkSync(webmFile); fs.unlinkSync(txtFile); } catch (_) {}
|
|
124
151
|
reject(new Error('edge-tts 失败: ' + e.message));
|