@oxiaom/adoremix 1.0.8 → 1.0.10

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.8",
3
+ "version": "1.0.10",
4
4
  "description": "AdoreMix broadcast server - cross-platform installer, runner and service manager",
5
5
  "bin": {
6
6
  "adoremix": "./bin/adoremix.js"
@@ -37,7 +37,8 @@
37
37
  "files": [
38
38
  "bin",
39
39
  "src",
40
- "templates"
40
+ "templates",
41
+ "tts"
41
42
  ],
42
43
  "keywords": [
43
44
  "adoremix",
package/src/doctor.js CHANGED
@@ -162,6 +162,31 @@ function runDoctor(workdir, opts) {
162
162
  logger.log(`○ 服务未运行(adoremix start 启动)`);
163
163
  }
164
164
 
165
+ // 8. TTS provider 依赖检查
166
+ logger.log('');
167
+ logger.log('=== TTS Provider 检查 ===');
168
+ const cfg = require('./config');
169
+ const cfgObj = cfg.readConfig(workdir) || {};
170
+ const ttsDeps = require('./tts-deps');
171
+ const providerName = (cfgObj.TTS && cfgObj.TTS.provider) || 'xf';
172
+ logger.log(`当前 provider: ${providerName}`);
173
+ const ttsResult = ttsDeps.checkDeps(workdir, providerName, cfgObj);
174
+ if (ttsResult.issues.length === 0) {
175
+ logger.ok(`✓ ${providerName} provider 所有依赖就绪`);
176
+ } else {
177
+ for (const iss of ttsResult.issues) {
178
+ const mark = iss.severity === 'error' ? '❌' : '⚠';
179
+ logger.log(` ${mark} ${iss.msg}`);
180
+ if (iss.fixCmd) logger.log(` 修复:${iss.fixCmd}`);
181
+ if (iss.autoFixable) issues.push({ type: 'tts', severity: iss.severity, msg: iss.msg, fixCmd: iss.fixCmd });
182
+ }
183
+ if (opts.fix) {
184
+ logger.info('==> 自动修复 TTS 依赖...');
185
+ const r = ttsDeps.fixDeps(workdir, ttsResult.issues, { sudo: process.getuid && process.getuid() === 0 });
186
+ logger.ok(`✓ 修复 ${r.fixed} 项,失败 ${r.failed} 项`);
187
+ }
188
+ }
189
+
165
190
  return reportAndExit(issues, opts);
166
191
  }
167
192
 
@@ -0,0 +1,253 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * TTS provider 依赖检查(最硬核版)
5
+ *
6
+ * 检查每个 provider 需要的所有东西:
7
+ * xf: crypto-js, ws, log4node (Node 包) + appid/apiSecret/apiKey (凭证)
8
+ * minimax: token (凭证) - Node 内置 https,无外部依赖
9
+ * edge: python3, edge-tts (pip 包), ffmpeg (系统) - 完全免费但依赖多
10
+ *
11
+ * 用法:
12
+ * const issues = checkDeps(workdir);
13
+ * // issues: [{severity, msg, fixCmd, autoFixable}]
14
+ * if (opts.fix) fixDeps(workdir, issues);
15
+ */
16
+
17
+ const { execSync, execFileSync } = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const PROVIDER_DEPENDENCIES = {
22
+ xf: {
23
+ nodeModules: ['crypto-js', 'ws', 'log4node'],
24
+ creds: [
25
+ { key: 'TTS.xf_appid', name: 'appid' },
26
+ { key: 'TTS.xf_apiSecret', name: 'apiSecret' },
27
+ { key: 'TTS.xf_apiKey', name: 'apiKey' }
28
+ ],
29
+ systemPkgs: [],
30
+ pipPkgs: [],
31
+ executables: []
32
+ },
33
+ minimax: {
34
+ nodeModules: [], // 用 Node 内置 https
35
+ creds: [{ key: 'TTS.minimax_token', name: 'token' }],
36
+ systemPkgs: [],
37
+ pipPkgs: [],
38
+ executables: []
39
+ },
40
+ edge: {
41
+ nodeModules: [],
42
+ creds: [],
43
+ systemPkgs: [
44
+ { name: 'python3', cmd: 'python3', altCmd: 'python' },
45
+ { name: 'ffmpeg', cmd: 'ffmpeg' }
46
+ ],
47
+ pipPkgs: [{ pkg: 'edge-tts', import: 'edge_tts' }],
48
+ executables: [{ name: 'ffmpeg', cmd: 'ffmpeg' }]
49
+ }
50
+ };
51
+
52
+ function which(cmd) {
53
+ try {
54
+ execSync(`command -v ${cmd} 2>/dev/null || which ${cmd} 2>/dev/null`, { stdio: 'pipe' });
55
+ return true;
56
+ } catch (e) { return false; }
57
+ }
58
+
59
+ function findPythonCmd() {
60
+ for (const c of ['python3', 'python']) {
61
+ if (which(c)) {
62
+ try {
63
+ const v = execSync(`${c} --version`, { stdio: 'pipe', encoding: 'utf8' });
64
+ if (/Python 3\./.test(v)) return c;
65
+ } catch (e) {}
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function pythonHasModule(pyCmd, modName) {
72
+ if (!pyCmd) return false;
73
+ try {
74
+ execFileSync(pyCmd, ['-c', `import ${modName}`], { stdio: 'pipe' });
75
+ return true;
76
+ } catch (e) { return false; }
77
+ }
78
+
79
+ function nodeModuleInstalled(workdir, modName) {
80
+ try {
81
+ require.resolve(modName, { paths: [workdir] });
82
+ return true;
83
+ } catch (e) { return false; }
84
+ }
85
+
86
+ function getCredValue(cfg, dottedKey) {
87
+ const parts = dottedKey.split('.');
88
+ let cur = cfg;
89
+ for (const p of parts) {
90
+ if (cur == null || typeof cur !== 'object') return undefined;
91
+ cur = cur[p];
92
+ }
93
+ return cur;
94
+ }
95
+
96
+ function detectAptLike() {
97
+ if (which('apt-get')) return 'apt-get';
98
+ if (which('apt')) return 'apt';
99
+ if (which('yum')) return 'yum';
100
+ if (which('dnf')) return 'dnf';
101
+ if (which('apk')) return 'apk';
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * 检查指定 provider 的所有依赖
107
+ * @returns {issues: Array, provider: string}
108
+ */
109
+ function checkDeps(workdir, providerName, cfg) {
110
+ const issues = [];
111
+ providerName = providerName || (cfg && cfg.TTS && cfg.TTS.provider) || 'xf';
112
+ const deps = PROVIDER_DEPENDENCIES[providerName];
113
+ if (!deps) {
114
+ return { issues: [{ severity: 'error', msg: `未知 provider: ${providerName}`, autoFixable: false }], provider: providerName };
115
+ }
116
+
117
+ // 1. 凭证
118
+ for (const c of deps.creds) {
119
+ const val = getCredValue(cfg, c.key);
120
+ if (!val) {
121
+ issues.push({
122
+ severity: 'error',
123
+ category: 'cred',
124
+ msg: `${providerName} 缺凭证 ${c.name}(${c.key})`,
125
+ fixCmd: `adoremix config set ${c.key} <your_${c.name}>`,
126
+ autoFixable: false
127
+ });
128
+ }
129
+ }
130
+
131
+ // 2. Node 模块
132
+ for (const mod of deps.nodeModules) {
133
+ if (!nodeModuleInstalled(workdir, mod)) {
134
+ issues.push({
135
+ severity: 'error',
136
+ category: 'node_module',
137
+ msg: `Node 包未安装: ${mod}`,
138
+ fixCmd: `cd ${workdir} && npm install ${mod}`,
139
+ autoFixable: true,
140
+ fixType: 'npm',
141
+ fixArgs: { cwd: workdir, mod }
142
+ });
143
+ }
144
+ }
145
+
146
+ // 3. pip 包
147
+ const pyCmd = findPythonCmd();
148
+ for (const pip of deps.pipPkgs) {
149
+ if (!pythonHasModule(pyCmd, pip.import)) {
150
+ issues.push({
151
+ severity: 'error',
152
+ category: 'pip',
153
+ msg: `Python 包未安装: ${pip.pkg}`,
154
+ fixCmd: 'pip3 install ' + pip.pkg,
155
+ autoFixable: true,
156
+ fixType: 'pip',
157
+ fixArgs: { pkg: pip.pkg, pyCmd }
158
+ });
159
+ }
160
+ }
161
+
162
+ // 4. 系统可执行
163
+ for (const exe of deps.execubles || deps.systemPkgs) {
164
+ if (exe.cmd && !which(exe.cmd)) {
165
+ // 推断 apt 包名
166
+ let aptPkg = '';
167
+ if (exe.name === 'python3') aptPkg = 'python3 python3-pip';
168
+ else if (exe.name === 'ffmpeg') aptPkg = 'ffmpeg';
169
+ const pkgMgr = detectAptLike();
170
+ issues.push({
171
+ severity: 'error',
172
+ category: 'system',
173
+ msg: `系统命令未找到: ${exe.name}`,
174
+ fixCmd: aptPkg && pkgMgr ? `sudo ${pkgMgr} install -y ${aptPkg}` : '',
175
+ autoFixable: !!(aptPkg && pkgMgr),
176
+ fixType: 'system',
177
+ fixArgs: { pkgMgr, aptPkg }
178
+ });
179
+ }
180
+ }
181
+
182
+ // python3 特殊处理(如果 edge 需要 python3 但只装了 python)
183
+ if (providerName === 'edge' && !pyCmd) {
184
+ const pkgMgr = detectAptLike();
185
+ issues.push({
186
+ severity: 'error',
187
+ category: 'system',
188
+ msg: '需要 python3(未找到 Python 3.x)',
189
+ fixCmd: pkgMgr ? `sudo ${pkgMgr} install -y python3 python3-pip` : '',
190
+ autoFixable: !!pkgMgr,
191
+ fixType: 'system',
192
+ fixArgs: { pkgMgr, aptPkg: 'python3 python3-pip' }
193
+ });
194
+ }
195
+
196
+ return { issues, provider: providerName };
197
+ }
198
+
199
+ /**
200
+ * 自动修复(仅 autoFixable=true 的)
201
+ */
202
+ function fixDeps(workdir, issues, opts) {
203
+ opts = opts || {};
204
+ const sudo = opts.sudo ? 'sudo ' : '';
205
+ let fixed = 0;
206
+ let failed = 0;
207
+ for (const issue of issues) {
208
+ if (!issue.autoFixable) continue;
209
+ try {
210
+ if (issue.fixType === 'npm') {
211
+ execSync(`npm install ${issue.fixArgs.mod} --no-audit --no-fund`, {
212
+ cwd: issue.fixArgs.cwd,
213
+ stdio: 'inherit'
214
+ });
215
+ } else if (issue.fixType === 'pip') {
216
+ const py = issue.fixArgs.pyCmd || findPythonCmd() || 'python3';
217
+ execSync(`${sudo}${py} -m pip install ${issue.fixArgs.pkg}`, { stdio: 'inherit' });
218
+ } else if (issue.fixType === 'system') {
219
+ if (!issue.fixArgs.pkgMgr) throw new Error('包管理器未识别');
220
+ execSync(`${sudo}${issue.fixArgs.pkgMgr} install -y ${issue.fixArgs.aptPkg}`, { stdio: 'inherit' });
221
+ }
222
+ fixed++;
223
+ } catch (e) {
224
+ console.error(` ✗ 修复失败 [${issue.msg}]: ${e.message.split('\n')[0]}`);
225
+ failed++;
226
+ }
227
+ }
228
+ return { fixed, failed };
229
+ }
230
+
231
+ /**
232
+ * 列出所有 provider 的依赖(用于 adoremix tts deps 不带 provider 时)
233
+ */
234
+ function listAllProviders(workdir, cfg) {
235
+ const result = {};
236
+ for (const name of Object.keys(PROVIDER_DEPENDENCIES)) {
237
+ const r = checkDeps(workdir, name, cfg);
238
+ result[name] = {
239
+ issues: r.issues,
240
+ ok: r.issues.length === 0
241
+ };
242
+ }
243
+ return result;
244
+ }
245
+
246
+ module.exports = {
247
+ PROVIDER_DEPENDENCIES,
248
+ checkDeps,
249
+ fixDeps,
250
+ listAllProviders,
251
+ which,
252
+ findPythonCmd
253
+ };
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Edge TTS provider(免费)
5
+ * 调用方式:python edge-tts 包 + ffmpeg 转 mp3
6
+ * 凭证:不需要(免费)
7
+ *
8
+ * 依赖:python3 + pip install edge-tts + ffmpeg
9
+ * 自动检测:adoremix tts deps
10
+ */
11
+
12
+ const { execFileSync } = require('child_process');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+
17
+ // 短名 → edge-tts voice 全名
18
+ const VOICES = {
19
+ xiaoxiao: 'zh-CN-XiaoxiaoNeural',
20
+ yunxi: 'zh-CN-YunxiNeural',
21
+ yunjian: 'zh-CN-YunjianNeural',
22
+ xiaoyi: 'zh-CN-XiaoyiNeural',
23
+ 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'
38
+ };
39
+
40
+ function findPython() {
41
+ for (const cmd of ['python3', 'python']) {
42
+ try {
43
+ execFileSync(cmd, ['--version'], { stdio: 'pipe' });
44
+ return cmd;
45
+ } catch (e) {}
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function checkDeps() {
51
+ const issues = [];
52
+ const py = findPython();
53
+ if (!py) {
54
+ issues.push({ msg: '未找到 python3/python', fix: 'apt-get install -y python3 python3-pip' });
55
+ return issues;
56
+ }
57
+ // 检查 edge-tts 包
58
+ try {
59
+ execFileSync(py, ['-c', 'import edge_tts'], { stdio: 'pipe' });
60
+ } catch (e) {
61
+ issues.push({ msg: 'python 缺 edge-tts 包', fix: 'pip3 install edge-tts' });
62
+ }
63
+ // 检查 ffmpeg
64
+ try {
65
+ execFileSync('ffmpeg', ['-version'], { stdio: 'pipe' });
66
+ } catch (e) {
67
+ issues.push({ msg: '未找到 ffmpeg', fix: 'apt-get install -y ffmpeg' });
68
+ }
69
+ return issues;
70
+ }
71
+
72
+ function synthesize({ text, voice, volume, speed, outFile }, creds) {
73
+ return new Promise((resolve, reject) => {
74
+ const issues = checkDeps();
75
+ if (issues.length > 0) {
76
+ return reject(new Error('edge-tts 依赖缺失:\n ' + issues.map(i => i.msg + ' (修复: ' + i.fix + ')').join('\n ')));
77
+ }
78
+
79
+ const py = findPython();
80
+ const voiceId = VOICES[voice] || creds.edge_voice_override || 'zh-CN-XiaoxiaoNeural';
81
+
82
+ // edge-tts rate/volume 是百分比字符串,如 "+50%" / "-20%"
83
+ const sp = parseInt(speed) || 50;
84
+ const vo = parseInt(volume) || 50;
85
+ const rateStr = `${sp - 50 >= 0 ? '+' : ''}${sp - 50}%`;
86
+ const volStr = `${vo - 50 >= 0 ? '+' : ''}${vo - 50}%`;
87
+
88
+ // 1. edge-tts 生成 webm(临时文件)
89
+ const tmpDir = os.tmpdir();
90
+ const webmFile = path.join(tmpDir, `adoremix-edge-${Date.now()}.webm`);
91
+ const txtFile = path.join(tmpDir, `adoremix-edge-${Date.now()}.txt`);
92
+ fs.writeFileSync(txtFile, text);
93
+
94
+ try {
95
+ // 用 python -m edge_tts 调用(避免命令找不到)
96
+ execFileSync(py, [
97
+ '-m', 'edge_tts',
98
+ '--voice', voiceId,
99
+ '--rate', rateStr,
100
+ '--volume', volStr,
101
+ '-f', txtFile,
102
+ '--write-media', webmFile
103
+ ], { stdio: 'pipe' });
104
+
105
+ if (!fs.existsSync(webmFile)) {
106
+ throw new Error('edge-tts 未生成音频');
107
+ }
108
+
109
+ // 2. ffmpeg 转 mp3
110
+ execFileSync('ffmpeg', [
111
+ '-y', '-i', webmFile,
112
+ '-acodec', 'libmp3lame',
113
+ '-ab', '128k',
114
+ '-ar', '22050',
115
+ outFile
116
+ ], { stdio: 'pipe' });
117
+
118
+ // 3. 清理临时
119
+ try { fs.unlinkSync(webmFile); fs.unlinkSync(txtFile); } catch (e) {}
120
+
121
+ resolve({ outFile, provider: 'edge', voice: voiceId });
122
+ } catch (e) {
123
+ try { fs.unlinkSync(webmFile); fs.unlinkSync(txtFile); } catch (_) {}
124
+ reject(new Error('edge-tts 失败: ' + e.message));
125
+ }
126
+ });
127
+ }
128
+
129
+ module.exports = {
130
+ name: 'edge',
131
+ description: 'Microsoft Edge TTS(免费,需 python3 + edge-tts + ffmpeg)',
132
+ voices: VOICES,
133
+ requiredCreds: [],
134
+ checkDeps,
135
+ synthesize
136
+ };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MiniMax TTS provider
5
+ * 调用方式:HTTP POST https://api.minimaxi.com/v1/t2a_v2
6
+ * 凭证:API token(包月几十元)
7
+ * 返回:JSON 里 data.audio 是 hex 编码的 mp3
8
+ *
9
+ * 不依赖 python,纯 Node 实现。
10
+ */
11
+
12
+ const https = require('https');
13
+ const fs = require('fs');
14
+
15
+ const API_URL = 'https://api.minimaxi.com/v1/t2a_v2';
16
+
17
+ // 短名 → minimax voice_id(基于 demo 提供的映射表)
18
+ const VOICES = {
19
+ xiaoxiao: 'female-tianmei',
20
+ yunxi: 'male-qn-jingying',
21
+ yunjian: 'male-qn-jingying',
22
+ xiaoyi: 'female-shaonv',
23
+ yunxia: 'female-shaonv',
24
+ xiaochen: 'female-chengshu',
25
+ xiaohan: 'female-yujie',
26
+ xiaomeng: 'female-shaonv',
27
+ xiaomo: 'female-chengshu',
28
+ xiaoqiu: 'female-tianmei-jingpin',
29
+ xiaorui: 'female-shaonv-jingpin',
30
+ xiaoshuang: 'female-yujie-jingpin',
31
+ xiaoxuan: 'female-chengshu-jingpin',
32
+ xiaoyan: 'female-sharon',
33
+ xiaoyou: 'female-shaonv',
34
+ yunfeng: 'male-qn-badao',
35
+ yunhao: 'male-qn-badao',
36
+ yunxiang: 'male-qn-jingying',
37
+ yunyang: 'male-qn-qingse',
38
+ // minimax 自有音色(直接透传)
39
+ 'male-qn-qingse': 'male-qn-qingse',
40
+ 'male-qn-daxuesheng': 'male-qn-daxuesheng',
41
+ 'male-qn-badao': 'male-qn-badao',
42
+ 'female-shaonv': 'female-shaonv',
43
+ 'female-yujie': 'female-yujie',
44
+ 'female-chengshu': 'female-chengshu',
45
+ 'female-tianmei': 'female-tianmei',
46
+ 'female-sharon': 'female-sharon'
47
+ };
48
+
49
+ function synthesize({ text, voice, volume, speed, outFile }, creds) {
50
+ return new Promise((resolve, reject) => {
51
+ const token = creds.minimax_token;
52
+ if (!token) return reject(new Error('MiniMax TTS 缺凭证:minimax_token'));
53
+
54
+ const voiceId = VOICES[voice] || 'female-tianmei';
55
+
56
+ // volume/speed 在 0-100 范围,minimax 用 0-2 范围的 speed,0-100 的 vol
57
+ const speedNorm = Math.max(0.5, Math.min(2, (parseInt(speed) || 50) / 50));
58
+ const volNorm = parseInt(volume) || 50;
59
+
60
+ const body = JSON.stringify({
61
+ model: 'speech-2.8-hd',
62
+ text,
63
+ stream: false,
64
+ voice_setting: {
65
+ voice_id: voiceId,
66
+ speed: speedNorm,
67
+ vol: volNorm,
68
+ pitch: 0,
69
+ emotion: 'happy'
70
+ },
71
+ audio_setting: {
72
+ sample_rate: 32000,
73
+ bitrate: 128000,
74
+ format: 'mp3',
75
+ channel: 1
76
+ },
77
+ subtitle_enable: false
78
+ });
79
+
80
+ const url = new URL(API_URL);
81
+ const req = https.request({
82
+ hostname: url.hostname,
83
+ path: url.pathname,
84
+ method: 'POST',
85
+ headers: {
86
+ 'Authorization': `Bearer ${token}`,
87
+ 'Content-Type': 'application/json',
88
+ 'Content-Length': Buffer.byteLength(body)
89
+ }
90
+ }, (res) => {
91
+ let data = '';
92
+ res.on('data', (c) => data += c);
93
+ res.on('end', () => {
94
+ let result;
95
+ try { result = JSON.parse(data); }
96
+ catch (e) { return reject(new Error('MiniMax 返回非 JSON: ' + data.slice(0, 200))); }
97
+ if (result.base_resp && result.base_resp.status_code !== 0) {
98
+ return reject(new Error('MiniMax API 错误: ' + result.base_resp.status_msg));
99
+ }
100
+ const audioHex = result.data && result.data.audio;
101
+ if (!audioHex) return reject(new Error('MiniMax 返回无 audio 字段'));
102
+ try {
103
+ const audioData = Buffer.from(audioHex, 'hex');
104
+ fs.writeFileSync(outFile, audioData);
105
+ resolve({ outFile, provider: 'minimax', voice: voiceId });
106
+ } catch (e) {
107
+ reject(new Error('写入 mp3 失败: ' + e.message));
108
+ }
109
+ });
110
+ });
111
+
112
+ req.on('error', (e) => reject(new Error('MiniMax 网络错误: ' + e.message)));
113
+ req.write(body);
114
+ req.end();
115
+ });
116
+ }
117
+
118
+ module.exports = {
119
+ name: 'minimax',
120
+ description: 'MiniMax TTS(HTTP API,包月几十元)',
121
+ voices: VOICES,
122
+ requiredCreds: ['minimax_token'],
123
+ synthesize
124
+ };
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 讯飞 TTS provider
5
+ * 调用方式:WebSocket wss://tts-api.xfyun.cn/v2/tts
6
+ * 凭证:appid + apiSecret + apiKey(一年 4000 元)
7
+ * 返回:直接 mp3 流(不需要 lame 转码)
8
+ */
9
+
10
+ const CryptoJS = require('crypto-js');
11
+ const WebSocket = require('ws');
12
+ const fs = require('fs');
13
+
14
+ // 讯飞 vcn(voice name)跟标准短名 1:1 映射(讯飞有自己的 vcn 名)
15
+ // 这里用标准短名直接传给讯飞(讯飞接受任意 vcn,无效会用默认)
16
+ // 如果用户的讯飞账号有自定义 vcn,可以在 config.ini 配 xf_voice_override
17
+ const VOICES = {
18
+ xiaoxiao: 'xiaoxiao', yunxi: 'yunxi', yunjian: 'yunjian',
19
+ xiaoyi: 'xiaoyi', yunxia: 'yunxia', xiaochen: 'xiaochen',
20
+ xiaohan: 'xiaohan', xiaomeng: 'xiaomeng', xiaomo: 'xiaomo',
21
+ xiaoqiu: 'xiaoqiu', xiaorui: 'xiaorui', xiaoshuang: 'xiaoshuang',
22
+ xiaoxuan: 'xiaoxuan', xiaoyan: 'xiaoyan', xiaoyou: 'xiaoyou',
23
+ yunfeng: 'yunfeng', yunhao: 'yunhao', yunxiang: 'yunxiang', yunyang: 'yunyang'
24
+ };
25
+
26
+ function synthesize({ text, voice, volume, speed, outFile }, creds) {
27
+ return new Promise((resolve, reject) => {
28
+ const appid = creds.xf_appid;
29
+ const apiSecret = creds.xf_apiSecret;
30
+ const apiKey = creds.xf_apiKey;
31
+ if (!appid || !apiSecret || !apiKey) {
32
+ return reject(new Error('讯飞 TTS 缺凭证:xf_appid / xf_apiSecret / xf_apiKey'));
33
+ }
34
+
35
+ const vcn = VOICES[voice] || creds.xf_voice_override || 'xiaoxiao';
36
+ const host = 'tts-api.xfyun.cn';
37
+ const hostUrl = `wss://${host}/v2/tts`;
38
+
39
+ const date = new Date().toUTCString();
40
+ const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/tts HTTP/1.1`;
41
+ const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
42
+ const signature = CryptoJS.enc.Base64.stringify(signatureSha);
43
+ const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
44
+ const authStr = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin));
45
+ const wssUrl = `${hostUrl}?authorization=${authStr}&date=${date}&host=${host}`;
46
+
47
+ const ws = new WebSocket(wssUrl);
48
+ let receivedAny = false;
49
+
50
+ ws.on('open', () => {
51
+ const frame = {
52
+ common: { app_id: appid },
53
+ business: {
54
+ aue: 'lame', sfl: 1,
55
+ volume: parseInt(volume) || 50,
56
+ speed: parseInt(speed) || 50,
57
+ vcn, tte: 'UTF8'
58
+ },
59
+ data: { text: Buffer.from(text).toString('base64'), status: 2 }
60
+ };
61
+ ws.send(JSON.stringify(frame));
62
+ if (fs.existsSync(outFile)) fs.unlinkSync(outFile);
63
+ });
64
+
65
+ ws.on('message', (data) => {
66
+ let res;
67
+ try { res = JSON.parse(data); } catch (e) { return; }
68
+ if (res.code !== 0) {
69
+ ws.close();
70
+ reject(new Error(`讯飞错误 ${res.code}: ${res.message}`));
71
+ return;
72
+ }
73
+ const audioBuf = Buffer.from(res.data.audio, 'base64');
74
+ fs.appendFileSync(outFile, audioBuf);
75
+ receivedAny = true;
76
+ if (res.data.status === 2) {
77
+ ws.close();
78
+ }
79
+ });
80
+
81
+ ws.on('close', () => {
82
+ if (!receivedAny) return reject(new Error('讯飞未返回音频'));
83
+ resolve({ outFile, provider: 'xf', voice: vcn });
84
+ });
85
+
86
+ ws.on('error', (err) => reject(new Error('讯飞 WebSocket 错误: ' + err.message)));
87
+ });
88
+ }
89
+
90
+ module.exports = {
91
+ name: 'xf',
92
+ description: '讯飞在线 TTS(WebSocket,年费约 4000 元)',
93
+ voices: VOICES,
94
+ requiredCreds: ['xf_appid', 'xf_apiSecret', 'xf_apiKey'],
95
+ synthesize
96
+ };
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // 顶层 tts.js wrapper(Qt 调用入口)
5
+ // Qt 调:node tts.js <vol> <speed> <voice> <mp3> <txt> [appid] [apiSecret] [apiKey]
6
+ // 这里转发给 adoremix-tts/ 目录的 dispatcher
7
+
8
+ const path = require('path');
9
+ const dispatcherPath = path.join(__dirname, 'adoremix-tts', 'tts.js');
10
+ const fs = require('fs');
11
+
12
+ if (!fs.existsSync(dispatcherPath)) {
13
+ console.error('[tts] 错误:adoremix-tts/ 目录不存在,请重新运行 adoremix install');
14
+ process.exit(1);
15
+ }
16
+
17
+ // 让 dispatcher 看到正确的 argv(保留 Qt 传入的所有参数)
18
+ process.argv[1] = dispatcherPath;
19
+ require(dispatcherPath);
package/tts/tts.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * AdoreMix TTS dispatcher
6
+ *
7
+ * Qt/C++ 调用方式(不变):
8
+ * node tts.js <volume> <speed> <voice_short> <mp3_out> <txt_path> [appid] [apiSecret] [apiKey]
9
+ *
10
+ * 根据 config.ini [TTS].provider 调用对应实现:
11
+ * - xf: 讯飞 WebSocket(原 tts.js 逻辑,需 appid/apiSecret/apiKey)
12
+ * - minimax: MiniMax HTTP API(需 token)
13
+ * - edge: python edge-tts(免费,需 python3 + ffmpeg)
14
+ *
15
+ * 凭证优先从 config.ini 读,命令行 appid/apiSecret/apiKey 兼容旧调用(仅 xf)。
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const ini = require('ini');
21
+
22
+ const PROVIDERS = {
23
+ xf: require('./providers/xf'),
24
+ minimax: require('./providers/minimax'),
25
+ edge: require('./providers/edge')
26
+ };
27
+
28
+ function log(...args) { console.log('[tts]', ...args); }
29
+ function err(...args) { console.error('[tts ERR]', ...args); }
30
+
31
+ async function main() {
32
+ const arg = process.argv.slice(2);
33
+ if (arg.length < 5) {
34
+ err('用法: node tts.js <volume> <speed> <voice> <mp3_out> <txt_path> [appid] [apiSecret] [apiKey]');
35
+ process.exit(1);
36
+ }
37
+
38
+ const [volume, speed, voice, mp3Name, txtPath, appidArg, apiSecretArg, apiKeyArg] = arg;
39
+
40
+ // 读文本(Qt 写的 utf-8 文本)
41
+ let text;
42
+ try { text = fs.readFileSync(txtPath, 'utf-8'); }
43
+ catch (e) { err(`读文本失败: ${txtPath} - ${e.message}`); process.exit(2); }
44
+
45
+ // 读 config.ini 找 provider
46
+ const cfgPath = path.join(process.cwd(), 'config.ini');
47
+ let ttsSection = {};
48
+ if (fs.existsSync(cfgPath)) {
49
+ try {
50
+ const cfg = ini.parse(fs.readFileSync(cfgPath, 'utf-8'));
51
+ ttsSection = cfg.TTS || {};
52
+ } catch (e) { err(`config.ini 解析失败: ${e.message}`); }
53
+ }
54
+ const providerName = (ttsSection.provider || 'xf').toLowerCase();
55
+ const provider = PROVIDERS[providerName];
56
+ if (!provider) {
57
+ err(`未知 provider: ${providerName}(可选: xf / minimax / edge)`);
58
+ process.exit(3);
59
+ }
60
+
61
+ // 输出路径(Qt 传的是文件名,实际写到 ./tty/ 下)
62
+ const ttyDir = path.join(process.cwd(), 'tty');
63
+ if (!fs.existsSync(ttyDir)) fs.mkdirSync(ttyDir, { recursive: true });
64
+ const outFile = path.join(ttyDir, mp3Name);
65
+
66
+ // 凭证:优先 config.ini,命令行参数兼容旧版 xf
67
+ // 兼容 Qt 已有字段:Settings.ttsxfAPPID / ttsxfAPISecret / ttsxfAPIKey
68
+ const settings = cfg.Settings || {};
69
+ const creds = {
70
+ xf_appid: ttsSection.xf_appid || settings.ttsxfAPPID || appidArg || '',
71
+ xf_apiSecret: ttsSection.xf_apiSecret || settings.ttsxfAPISecret || apiSecretArg || '',
72
+ xf_apiKey: ttsSection.xf_apiKey || settings.ttsxfAPIKey || apiKeyArg || '',
73
+ xf_voice_override: ttsSection.xf_voice_override || '',
74
+ minimax_token: ttsSection.minimax_token || '',
75
+ edge_voice_override: ttsSection.edge_voice_override || ''
76
+ };
77
+
78
+ log(`provider=${providerName} voice=${voice} text=${text.length}字 → ${mp3Name}`);
79
+
80
+ try {
81
+ const result = await provider.synthesize(
82
+ { text, voice, volume, speed, outFile },
83
+ creds
84
+ );
85
+ log(`✓ ${result.provider} 生成成功: ${result.outFile} (voice=${result.voice})`);
86
+ } catch (e) {
87
+ err(`${providerName} 失败: ${e.message}`);
88
+ process.exit(4);
89
+ }
90
+ }
91
+
92
+ main().catch(e => { err(e.message); process.exit(99); });
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 统一 voice 短名表(前端 UI 用这套)
5
+ *
6
+ * Qt 调 tts.js 时传的 voice 参数用这里的短名(如 xiaoxiao/yunxi)。
7
+ * 每个 provider 在自己的 voices 字典里把短名映射到平台真实 voice_id。
8
+ *
9
+ * 短名以 edge-tts 风格命名(小写拼音),是 UI 蓝本。
10
+ */
11
+
12
+ // 标准短名(前端 UI 应该只展示这些)
13
+ const STANDARD_VOICES = [
14
+ { short: 'xiaoxiao', desc: '晓晓 - 女声,自然亲切' },
15
+ { short: 'yunxi', desc: '云希 - 男声,年轻活力' },
16
+ { short: 'yunjian', desc: '云健 - 男声,成熟稳重' },
17
+ { short: 'xiaoyi', desc: '晓伊 - 女声,温柔甜美' },
18
+ { short: 'yunxia', desc: '云夏 - 男童声' },
19
+ { short: 'xiaochen', desc: '晓辰 - 女声,新闻播报' },
20
+ { short: 'xiaohan', desc: '晓涵 - 女声,温暖' },
21
+ { short: 'xiaomeng', desc: '晓梦 - 女声,活泼' },
22
+ { short: 'xiaomo', desc: '晓墨 - 女声,知性' },
23
+ { short: 'xiaoqiu', desc: '晓秋 - 女声,温和' },
24
+ { short: 'xiaorui', desc: '晓睿 - 女童声' },
25
+ { short: 'xiaoshuang', desc: '晓双 - 女童声' },
26
+ { short: 'xiaoxuan', desc: '晓萱 - 女声' },
27
+ { short: 'xiaoyan', desc: '晓妍 - 女声' },
28
+ { short: 'xiaoyou', desc: '悠悠 - 女童声' },
29
+ { short: 'yunfeng', desc: '云枫 - 男声' },
30
+ { short: 'yunhao', desc: '云皓 - 男声' },
31
+ { short: 'yunxiang', desc: '云翔 - 男声' },
32
+ { short: 'yunyang', desc: '云扬 - 男声' }
33
+ ];
34
+
35
+ const DEFAULT_VOICE = 'xiaoxiao';
36
+
37
+ module.exports = { STANDARD_VOICES, DEFAULT_VOICE };