@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 CHANGED
@@ -2,9 +2,12 @@
2
2
  'use strict';
3
3
 
4
4
  const cli = require('../src/cli');
5
- cli.run(process.argv.slice(2)).then((code) => {
6
- process.exit(code || 0);
7
- }).catch((err) => {
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.18",
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 wp = paths.workdirPaths(resolveWorkdir(opts.workdir));
312
- const file = opts.file || wp.logfile;
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
- if (!fs.existsSync(file)) {
316
- logger.warn(`日志文件不存在 ${file}`);
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
- const pkg = LIB_TO_PKG[lib];
100
- if (pkg) pkgs.push(pkg);
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
- for (const mod of deps.nodeModules) {
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.execubles || deps.systemPkgs) {
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
- execSync(`npm install ${issue.fixArgs.mod} --no-audit --no-fund`, {
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
- execSync(`${sudo}${py} -m pip install ${issue.fixArgs.pkg}`, { stdio: 'inherit' });
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/your-org/adoremix
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
- ExecStart=__NODE__ __CLI__ start --daemon --workdir __WORKDIR__
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.err
19
+ StandardError=append:__WORKDIR__/logs/svc.log
16
20
  LimitNOFILE=65536
17
21
 
18
22
  [Install]
@@ -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
- xiaochen: 'zh-CN-XiaochenNeural',
25
- xiaohan: 'zh-CN-XiaohanNeural',
26
- xiaomeng: 'zh-CN-XiaomengNeural',
27
- xiaomo: 'zh-CN-XiaomoNeural',
28
- xiaoqiu: 'zh-CN-XiaoqiuNeural',
29
- xiaorui: 'zh-CN-XiaoruiNeural',
30
- xiaoshuang: 'zh-CN-XiaoshuangNeural',
31
- xiaoxuan: 'zh-CN-XiaoxuanNeural',
32
- xiaoyan: 'zh-CN-XiaoyanNeural',
33
- xiaoyou: 'zh-CN-XiaoyouNeural',
34
- yunfeng: 'zh-CN-YunfengNeural',
35
- yunhao: 'zh-CN-YunhaoNeural',
36
- yunxiang: 'zh-CN-YunxiangNeural',
37
- yunyang: 'zh-CN-YunyangNeural'
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 voiceId = VOICES[voice] || creds.edge_voice_override || 'zh-CN-XiaoxiaoNeural';
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
- try {
95
- // python -m edge_tts 调用(避免命令找不到)
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
- if (!fs.existsSync(webmFile)) {
106
- throw new Error('edge-tts 未生成音频');
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: voiceId });
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));