@oxiaom/adoremix 1.0.18 → 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/bin/adoremix.js +6 -3
- package/package.json +4 -2
- package/src/cli.js +27 -4
- package/src/doctor.js +94 -12
- package/src/paths.js +4 -2
- package/src/tts-deps.js +64 -8
- package/templates/adoremix.service.tmpl +7 -3
- package/tts/providers/edge.js +48 -21
package/bin/adoremix.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const cli = require('../src/cli');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// 注意:不能在 run() resolve 后强制 process.exit —— 前台模式 `adoremix start`
|
|
6
|
+
// spawn 出的二进制是 active handle,node 会自然挂起等待它运行(Ctrl+C 退出)。
|
|
7
|
+
// 无脑 process.exit 会把 node 杀掉,二进制成孤儿后收 SIGHUP 被终止,表现为"启动即退"。
|
|
8
|
+
// 普通命令(version/install/config 等)action 完成后事件循环空了会自然退出,
|
|
9
|
+
// 退出码由各命令自己设 process.exitCode(doctor/doctor --fix 等已设)。
|
|
10
|
+
cli.run(process.argv.slice(2)).catch((err) => {
|
|
8
11
|
const logger = require('../src/logger');
|
|
9
12
|
logger.error('未捕获的错误:', err);
|
|
10
13
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxiaom/adoremix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "AdoreMix broadcast server - cross-platform installer, runner and service manager",
|
|
5
5
|
"bin": {
|
|
6
6
|
"adoremix": "./bin/adoremix.js"
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"@oxiaom/adoremix-win32-x64": "^1.0.0",
|
|
15
15
|
"@oxiaom/adoremix-linux-x64": "^1.0.0",
|
|
16
16
|
"@oxiaom/adoremix-linux-arm64": "^1.0.0",
|
|
17
|
-
"@oxiaom/adoremix-linux-arm": "^1.0.0"
|
|
17
|
+
"@oxiaom/adoremix-linux-arm": "^1.0.0",
|
|
18
|
+
"@oxiaom/adoremix-darwin-x64": "^1.0.0",
|
|
19
|
+
"@oxiaom/adoremix-darwin-arm64": "^1.0.0"
|
|
18
20
|
},
|
|
19
21
|
"dependencies": {
|
|
20
22
|
"commander": "^12.1.0",
|
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/doctor.js
CHANGED
|
@@ -26,6 +26,57 @@ const LIB_TO_PKG = {
|
|
|
26
26
|
'libkrb5.so.3': 'libkrb5-3'
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
// ICU 70 兜底:
|
|
30
|
+
// AdoreMix 二进制在 Ubuntu 22.04(ICU 70)编译,链接 libicui18n.so.70 / libicuuc.so.70 / libicudata.so.70。
|
|
31
|
+
// Debian 12 / 树莓派 OS / Armbian 等 apt 源里只有 libicu72,没有 libicu70,跨发行版运行会缺库。
|
|
32
|
+
// 这三个 .so 都由同一个包 libicu70 提供,从 Ubuntu 官方源下载对应架构 .deb 装上即可(与系统 libicu72 共存,不冲突)。
|
|
33
|
+
const ICU70_LIBS = ['libicui18n.so.70', 'libicuuc.so.70', 'libicudata.so.70'];
|
|
34
|
+
const ICU70_DEB_SOURCES = {
|
|
35
|
+
// node arch → [deb arch, Ubuntu 源根]
|
|
36
|
+
arm64: ['arm64', 'http://ports.ubuntu.com/ubuntu-ports'], // 非 amd64 架构走 ports
|
|
37
|
+
arm: ['armhf', 'http://ports.ubuntu.com/ubuntu-ports'], // armv7 / armhf(树莓派 32 位、老款盒子)
|
|
38
|
+
x64: ['amd64', 'http://archive.ubuntu.com/ubuntu']
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function getIcu70DebInfo() {
|
|
42
|
+
const src = ICU70_DEB_SOURCES[process.arch];
|
|
43
|
+
if (!src) return null;
|
|
44
|
+
const [debArch, base] = src;
|
|
45
|
+
return {
|
|
46
|
+
debArch,
|
|
47
|
+
url: `${base}/pool/main/i/icu/libicu70_70.1-2_${debArch}.deb`,
|
|
48
|
+
tmpPath: '/tmp/adoremix-libicu70.deb'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tryFixIcu70() {
|
|
53
|
+
const info = getIcu70DebInfo();
|
|
54
|
+
if (!info) {
|
|
55
|
+
logger.warn(` ICU 70 兜底暂不支持架构 ${process.arch},请手动安装 libicu70`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
logger.info(`==> ICU 70 兜底:从 Ubuntu 官方源下载 libicu70 (${info.debArch})`);
|
|
59
|
+
logger.log(` ${info.url}`);
|
|
60
|
+
try {
|
|
61
|
+
execSync(`wget -q -O ${info.tmpPath} '${info.url}'`, { stdio: 'inherit' });
|
|
62
|
+
const size = fs.statSync(info.tmpPath).size;
|
|
63
|
+
if (size < 100000) {
|
|
64
|
+
logger.error(`下载失败(文件 ${size} 字节,可能 URL 失效或网络不通)`);
|
|
65
|
+
try { fs.unlinkSync(info.tmpPath); } catch (e) {}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
logger.info(`==> dpkg -i libicu70_70.1-2_${info.debArch}.deb (${(size / 1024 / 1024).toFixed(1)} MB)`);
|
|
69
|
+
execSync(`dpkg -i ${info.tmpPath}`, { stdio: 'inherit' });
|
|
70
|
+
try { fs.unlinkSync(info.tmpPath); } catch (e) {}
|
|
71
|
+
return true;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
logger.error(`ICU 70 安装失败:${(e.message || '').split('\n')[0]}`);
|
|
74
|
+
logger.warn(` 可能需要 root,手动执行:`);
|
|
75
|
+
logger.warn(` wget -O /tmp/libicu70.deb '${info.url}' && sudo dpkg -i /tmp/libicu70.deb`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
29
80
|
function runDoctor(workdir, opts) {
|
|
30
81
|
opts = opts || {};
|
|
31
82
|
logger.log('=== AdoreMix 健康检查 ===');
|
|
@@ -93,38 +144,69 @@ function runDoctor(workdir, opts) {
|
|
|
93
144
|
logger.ok(`✓ 动态库依赖完整`);
|
|
94
145
|
} else {
|
|
95
146
|
logger.error(`❌ 缺动态库:${missing.join(', ')}`);
|
|
147
|
+
// 分类:apt 可装 / ICU 70 跨发行版兜底 / 完全未知
|
|
96
148
|
const pkgs = [];
|
|
149
|
+
const icu70 = [];
|
|
97
150
|
const unknown = [];
|
|
98
151
|
for (const lib of missing) {
|
|
99
|
-
|
|
100
|
-
if (
|
|
152
|
+
if (ICU70_LIBS.includes(lib)) icu70.push(lib);
|
|
153
|
+
else if (LIB_TO_PKG[lib]) pkgs.push(LIB_TO_PKG[lib]);
|
|
101
154
|
else unknown.push(lib);
|
|
102
155
|
}
|
|
103
156
|
const allPkgs = [...new Set(pkgs)];
|
|
157
|
+
|
|
158
|
+
// 1) apt 可装的库
|
|
159
|
+
let aptSolved = allPkgs.length === 0;
|
|
104
160
|
if (allPkgs.length > 0) {
|
|
105
161
|
const cmd = `apt-get install -y ${allPkgs.join(' ')}`;
|
|
106
162
|
logger.log(` 建议命令(root 用户):${cmd}`);
|
|
107
|
-
if (unknown.length > 0) {
|
|
108
|
-
logger.warn(` 未知库(无 apt 映射):${unknown.join(', ')}`);
|
|
109
|
-
}
|
|
110
|
-
issues.push({
|
|
111
|
-
type: 'libs',
|
|
112
|
-
severity: 'error',
|
|
113
|
-
msg: missing.join(', '),
|
|
114
|
-
fixCmd: cmd,
|
|
115
|
-
pkgs: allPkgs
|
|
116
|
-
});
|
|
117
163
|
if (opts.fix) {
|
|
118
164
|
logger.info(`==> 自动修复:执行 ${cmd}`);
|
|
119
165
|
try {
|
|
120
166
|
execSync(cmd, { stdio: 'inherit', cwd: workdir });
|
|
121
167
|
logger.ok('✓ 缺库已安装');
|
|
168
|
+
aptSolved = true;
|
|
122
169
|
} catch (e) {
|
|
123
170
|
logger.error(`修复失败:${e.message}`);
|
|
124
171
|
logger.warn('可能需要 sudo:手动执行 sudo ' + cmd);
|
|
125
172
|
}
|
|
126
173
|
}
|
|
127
174
|
}
|
|
175
|
+
|
|
176
|
+
// 2) ICU 70 跨发行版兜底(Debian 12 / 树莓派 OS / Armbian 等无 libicu70)
|
|
177
|
+
let icuSolved = icu70.length === 0;
|
|
178
|
+
if (icu70.length > 0) {
|
|
179
|
+
const icuMsg = `ICU 70 缺失(${icu70.join(', ')})— 二进制在 Ubuntu 22.04 编译,Debian 12 等需补 libicu70`;
|
|
180
|
+
if (opts.fix) {
|
|
181
|
+
if (tryFixIcu70()) {
|
|
182
|
+
logger.ok('✓ libicu70 已安装(ICU 70 兜底)');
|
|
183
|
+
icuSolved = true;
|
|
184
|
+
} else {
|
|
185
|
+
issues.push({ type: 'icu70', severity: 'error', msg: icuMsg });
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
logger.warn(` ${icuMsg}`);
|
|
189
|
+
logger.log(` 自动修复:adoremix doctor --fix(从 Ubuntu 官方源下载 libicu70 .deb)`);
|
|
190
|
+
issues.push({ type: 'icu70', severity: 'error', msg: icuMsg });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3) 未解决的记 issue
|
|
195
|
+
if (!aptSolved) {
|
|
196
|
+
issues.push({
|
|
197
|
+
type: 'libs',
|
|
198
|
+
severity: 'error',
|
|
199
|
+
msg: missing.join(', '),
|
|
200
|
+
fixCmd: `apt-get install -y ${allPkgs.join(' ')}`,
|
|
201
|
+
pkgs: allPkgs
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 4) 完全未知的库
|
|
206
|
+
if (unknown.length > 0) {
|
|
207
|
+
logger.warn(` 未知库(无 apt 映射,需手动排查):${unknown.join(', ')}`);
|
|
208
|
+
issues.push({ type: 'libs-unknown', severity: 'error', msg: unknown.join(', ') });
|
|
209
|
+
}
|
|
128
210
|
}
|
|
129
211
|
} catch (e) {
|
|
130
212
|
logger.warn(`⚠ ldd 检查失败(非 Linux 或权限问题):${e.message.split('\n')[0]}`);
|
package/src/paths.js
CHANGED
|
@@ -9,7 +9,9 @@ const PLATFORM_PKG = {
|
|
|
9
9
|
'win32_x64': '@oxiaom/adoremix-win32-x64',
|
|
10
10
|
'linux_x64': '@oxiaom/adoremix-linux-x64',
|
|
11
11
|
'linux_arm64': '@oxiaom/adoremix-linux-arm64',
|
|
12
|
-
'linux_arm': '@oxiaom/adoremix-linux-arm'
|
|
12
|
+
'linux_arm': '@oxiaom/adoremix-linux-arm',
|
|
13
|
+
'darwin_x64': '@oxiaom/adoremix-darwin-x64',
|
|
14
|
+
'darwin_arm64': '@oxiaom/adoremix-darwin-arm64'
|
|
13
15
|
}[PLATFORM_KEY];
|
|
14
16
|
|
|
15
17
|
let _native = null;
|
|
@@ -19,7 +21,7 @@ function loadNative() {
|
|
|
19
21
|
if (_native) return _native;
|
|
20
22
|
if (_loadError) throw _loadError;
|
|
21
23
|
if (!PLATFORM_PKG) {
|
|
22
|
-
_loadError = new Error(`不支持的平台:${PLATFORM_KEY}。当前支持 win32-x64 / linux-x64 / linux-arm64 / linux-arm。`);
|
|
24
|
+
_loadError = new Error(`不支持的平台:${PLATFORM_KEY}。当前支持 win32-x64 / linux-x64 / linux-arm64 / linux-arm / darwin-x64 / darwin-arm64。`);
|
|
23
25
|
throw _loadError;
|
|
24
26
|
}
|
|
25
27
|
try {
|
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
|
|
|
@@ -76,6 +80,27 @@ function pythonHasModule(pyCmd, modName) {
|
|
|
76
80
|
} catch (e) { return false; }
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
// python3 自带 pip 吗(Debian/Armbian 精简系统默认不带,要 apt install python3-pip)
|
|
84
|
+
function pythonHasPip(pyCmd) {
|
|
85
|
+
if (!pyCmd) return false;
|
|
86
|
+
try {
|
|
87
|
+
execFileSync(pyCmd, ['-m', 'pip', '--version'], { stdio: 'pipe' });
|
|
88
|
+
return true;
|
|
89
|
+
} catch (e) { return false; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 是否 PEP 668 externally-managed(Debian 12 / Ubuntu 23.04+ 默认禁止往系统 Python 装 pip 包)
|
|
93
|
+
// 这种环境 pip install 会报 externally-managed-environment,需加 --break-system-packages
|
|
94
|
+
function isExternallyManaged(pyCmd) {
|
|
95
|
+
if (!pyCmd) return false;
|
|
96
|
+
try {
|
|
97
|
+
const out = execFileSync(pyCmd, ['-c',
|
|
98
|
+
'import sysconfig,os;print(os.path.exists(os.path.join(sysconfig.get_path("stdlib"),"EXTERNALLY-MANAGED")))'
|
|
99
|
+
], { stdio: 'pipe', encoding: 'utf8' });
|
|
100
|
+
return String(out).trim() === 'True';
|
|
101
|
+
} catch (e) { return false; }
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
function nodeModuleInstalled(workdir, modName) {
|
|
80
105
|
try {
|
|
81
106
|
require.resolve(modName, { paths: [workdir] });
|
|
@@ -134,8 +159,13 @@ function checkDeps(workdir, providerName, cfg) {
|
|
|
134
159
|
}
|
|
135
160
|
}
|
|
136
161
|
|
|
137
|
-
// 2. Node
|
|
138
|
-
|
|
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) {
|
|
139
169
|
if (!nodeModuleInstalled(workdir, mod)) {
|
|
140
170
|
issues.push({
|
|
141
171
|
severity: 'error',
|
|
@@ -166,12 +196,13 @@ function checkDeps(workdir, providerName, cfg) {
|
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
// 4. 系统可执行
|
|
169
|
-
for (const exe of deps.
|
|
199
|
+
for (const exe of deps.executables || deps.systemPkgs) {
|
|
170
200
|
if (exe.cmd && !which(exe.cmd)) {
|
|
171
201
|
// 推断 apt 包名
|
|
172
202
|
let aptPkg = '';
|
|
173
203
|
if (exe.name === 'python3') aptPkg = 'python3 python3-pip';
|
|
174
204
|
else if (exe.name === 'ffmpeg') aptPkg = 'ffmpeg';
|
|
205
|
+
else if (exe.name === 'lame') aptPkg = 'lame';
|
|
175
206
|
const pkgMgr = detectAptLike();
|
|
176
207
|
issues.push({
|
|
177
208
|
severity: 'error',
|
|
@@ -214,13 +245,38 @@ function fixDeps(workdir, issues, opts) {
|
|
|
214
245
|
if (!issue.autoFixable) continue;
|
|
215
246
|
try {
|
|
216
247
|
if (issue.fixType === 'npm') {
|
|
217
|
-
|
|
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`, {
|
|
218
254
|
cwd: issue.fixArgs.cwd,
|
|
219
255
|
stdio: 'inherit'
|
|
220
256
|
});
|
|
221
257
|
} else if (issue.fixType === 'pip') {
|
|
222
258
|
const py = issue.fixArgs.pyCmd || findPythonCmd() || 'python3';
|
|
223
|
-
|
|
259
|
+
// 1. 确保 pip 可用(Debian/Armbian 精简系统 python3 自带但 pip 缺失)
|
|
260
|
+
if (!pythonHasPip(py)) {
|
|
261
|
+
const pkgMgr = detectAptLike();
|
|
262
|
+
console.log(` pip 未安装,先补 python3-pip ...`);
|
|
263
|
+
if (pkgMgr) {
|
|
264
|
+
execSync(`${sudo}${pkgMgr} install -y python3-pip`, { stdio: 'inherit' });
|
|
265
|
+
} else {
|
|
266
|
+
// 退路:用 Python 自带的 ensurepip 引导
|
|
267
|
+
execSync(`${sudo}${py} -m ensurepip --upgrade`, { stdio: 'inherit' });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// 2. PEP 668:Debian 12 / Ubuntu 23.04+ 禁止往系统 Python 装包,需 --break-system-packages
|
|
271
|
+
const breakFlag = isExternallyManaged(py) ? ' --break-system-packages' : '';
|
|
272
|
+
if (breakFlag) console.log(' 检测到 externally-managed-environment,加 --break-system-packages');
|
|
273
|
+
// 3. PyPI 镜像:默认用清华(国内默认 PyPI 慢,常因下载损坏导致 hash 校验失败)。
|
|
274
|
+
// 国外用户可设环境变量 ADOREMIX_PIP_INDEX 覆盖,留空则用官方源。
|
|
275
|
+
const indexUrl = process.env.ADOREMIX_PIP_INDEX !== undefined
|
|
276
|
+
? process.env.ADOREMIX_PIP_INDEX
|
|
277
|
+
: 'https://pypi.tuna.tsinghua.edu.cn/simple';
|
|
278
|
+
const indexFlag = indexUrl ? ` -i ${indexUrl}` : '';
|
|
279
|
+
execSync(`${sudo}${py} -m pip install${breakFlag}${indexFlag} ${issue.fixArgs.pkg}`, { stdio: 'inherit' });
|
|
224
280
|
} else if (issue.fixType === 'system') {
|
|
225
281
|
if (!issue.fixArgs.pkgMgr) throw new Error('包管理器未识别');
|
|
226
282
|
execSync(`${sudo}${issue.fixArgs.pkgMgr} install -y ${issue.fixArgs.aptPkg}`, { stdio: 'inherit' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[Unit]
|
|
2
2
|
Description=AdoreMix Broadcast Server
|
|
3
|
-
Documentation=https://github.com/
|
|
3
|
+
Documentation=https://github.com/oxiaom/adoremix-npm
|
|
4
4
|
After=network.target mysqld.service redis.service
|
|
5
5
|
|
|
6
6
|
[Service]
|
|
@@ -8,11 +8,15 @@ Type=simple
|
|
|
8
8
|
User=__USER__
|
|
9
9
|
Group=__GROUP__
|
|
10
10
|
WorkingDirectory=__WORKDIR__
|
|
11
|
-
|
|
11
|
+
# 用前台模式(不加 --daemon):node 持续运行作为 systemd 主进程,
|
|
12
|
+
# systemd 才能正确跟踪/重启/停止服务。
|
|
13
|
+
# daemon 模式 node 会 unref 后退出,systemd 默认 KillMode=control-group
|
|
14
|
+
# 会把整个 cgroup(含刚 spawn 的二进制)一起杀掉,服务起不来。
|
|
15
|
+
ExecStart=__NODE__ __CLI__ start --workdir __WORKDIR__
|
|
12
16
|
Restart=on-failure
|
|
13
17
|
RestartSec=5
|
|
14
18
|
StandardOutput=append:__WORKDIR__/logs/svc.log
|
|
15
|
-
StandardError=append:__WORKDIR__/logs/svc.
|
|
19
|
+
StandardError=append:__WORKDIR__/logs/svc.log
|
|
16
20
|
LimitNOFILE=65536
|
|
17
21
|
|
|
18
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));
|