@oxiaom/adoremix 1.0.19 → 1.0.21

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxiaom/adoremix",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "AdoreMix broadcast server - cross-platform installer, runner and service manager",
5
5
  "bin": {
6
6
  "adoremix": "./bin/adoremix.js"
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/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
- 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) {
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.execubles || deps.systemPkgs) {
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
- 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`, {
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.err
19
+ StandardError=append:__WORKDIR__/logs/svc.log
20
20
  LimitNOFILE=65536
21
21
 
22
22
  [Install]
@@ -17,7 +17,9 @@ const svc = new Service({
17
17
  description: SVC_DESC,
18
18
  script: CLI,
19
19
  execPath: NODE,
20
- args: ['start', '--daemon', '--workdir', WORKDIR],
20
+ // 用前台模式(不加 --daemon):node 持续运行作为服务主进程,winsw 才能正确跟踪/重启/停止。
21
+ // daemon 模式 node 会 unref 后退出,winsw 认为服务停了(跟 systemd 同 bug)。
22
+ args: ['start', '--workdir', WORKDIR],
21
23
  cwd: WORKDIR,
22
24
  env: [{
23
25
  name: 'NODE_ENV',
@@ -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
+ // 讯飞原生发音人(WebUI 音色选项:小燕/许久/小萍/小婧)→ 映射到 edge 有效 voice
47
+ aisjiuxu: 'zh-CN-YunxiNeural', // 许久(男)→ 云希
48
+ aisxping: 'zh-CN-XiaoxiaoNeural', // 小萍(女)→ 晓晓
49
+ aisjinger: 'zh-CN-XiaoyiNeural' // 小婧(女)→ 晓伊
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));