@oxiaom/adoremix 1.0.11 → 1.0.13
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 +53 -0
- package/src/tts-cli.js +235 -0
- package/src/tts-deps.js +7 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -45,6 +45,59 @@ function buildProgram() {
|
|
|
45
45
|
process.exitCode = doctor.runDoctor(resolveWorkdir(opts.workdir), { fix: !!opts.fix });
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
const ttsCmd = program
|
|
49
|
+
.command('tts')
|
|
50
|
+
.description('TTS 文字转语音(支持 xf 讯飞 / minimax / edge 三种 provider)');
|
|
51
|
+
|
|
52
|
+
ttsCmd
|
|
53
|
+
.command('list')
|
|
54
|
+
.description('列出 3 个 provider + 当前激活 + 凭证/依赖状态')
|
|
55
|
+
.option('--workdir <path>')
|
|
56
|
+
.action((opts) => {
|
|
57
|
+
const tts = require('./tts-cli');
|
|
58
|
+
process.exitCode = tts.cmdList(resolveWorkdir(opts.workdir));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
ttsCmd
|
|
62
|
+
.command('config')
|
|
63
|
+
.description('交互式配置:选 provider + 填凭证')
|
|
64
|
+
.option('--workdir <path>')
|
|
65
|
+
.action(async (opts) => {
|
|
66
|
+
const tts = require('./tts-cli');
|
|
67
|
+
process.exitCode = await tts.cmdConfig(resolveWorkdir(opts.workdir), {});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ttsCmd
|
|
71
|
+
.command('test [text]')
|
|
72
|
+
.description('用当前 provider 测试 TTS(生成 _cli_test.mp3)')
|
|
73
|
+
.option('--workdir <path>')
|
|
74
|
+
.option('-v, --voice <name>', `voice 短名(默认 ${DEFAULT_VOICE || 'xiaoxiao'})`)
|
|
75
|
+
.option('--volume <n>', '音量 0-100', '50')
|
|
76
|
+
.option('--speed <n>', '语速 0-100', '50')
|
|
77
|
+
.action(async (text, opts) => {
|
|
78
|
+
const tts = require('./tts-cli');
|
|
79
|
+
process.exitCode = await tts.cmdTest(resolveWorkdir(opts.workdir), text, opts);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
ttsCmd
|
|
83
|
+
.command('voices')
|
|
84
|
+
.description('列出所有 voice 短名 + 当前 provider 的映射')
|
|
85
|
+
.option('--workdir <path>')
|
|
86
|
+
.action((opts) => {
|
|
87
|
+
const tts = require('./tts-cli');
|
|
88
|
+
process.exitCode = tts.cmdVoices(resolveWorkdir(opts.workdir));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
ttsCmd
|
|
92
|
+
.command('deps')
|
|
93
|
+
.description('检查当前 provider 的依赖(python/edge-tts/ffmpeg/npm 包/凭证)')
|
|
94
|
+
.option('--workdir <path>')
|
|
95
|
+
.option('--fix', '自动修复缺的依赖(apt install / pip install / npm install)')
|
|
96
|
+
.action((opts) => {
|
|
97
|
+
const tts = require('./tts-cli');
|
|
98
|
+
process.exitCode = tts.cmdDeps(resolveWorkdir(opts.workdir), { fix: !!opts.fix });
|
|
99
|
+
});
|
|
100
|
+
|
|
48
101
|
program
|
|
49
102
|
.command('install')
|
|
50
103
|
.description('初始化工作目录、复制资源、安装协作依赖、生成 config.ini')
|
package/src/tts-cli.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const prompts = require('prompts');
|
|
7
|
+
const logger = require('./logger');
|
|
8
|
+
const paths = require('./paths');
|
|
9
|
+
const cfg = require('./config');
|
|
10
|
+
const ttsDeps = require('./tts-deps');
|
|
11
|
+
const { STANDARD_VOICES, DEFAULT_VOICE } = require('../tts/voice-mapping');
|
|
12
|
+
|
|
13
|
+
// provider 元信息(描述 + 费用 + python 依赖)
|
|
14
|
+
const PROVIDER_INFO = {
|
|
15
|
+
xf: {
|
|
16
|
+
name: '讯飞',
|
|
17
|
+
desc: '科大讯飞 WebSocket TTS',
|
|
18
|
+
cost: '约 4000 元/年',
|
|
19
|
+
needsPython: false
|
|
20
|
+
},
|
|
21
|
+
minimax: {
|
|
22
|
+
name: 'MiniMax',
|
|
23
|
+
desc: 'MiniMax HTTP API TTS',
|
|
24
|
+
cost: '包月几十元',
|
|
25
|
+
needsPython: false
|
|
26
|
+
},
|
|
27
|
+
edge: {
|
|
28
|
+
name: 'Edge TTS',
|
|
29
|
+
desc: 'Microsoft Edge TTS(python)',
|
|
30
|
+
cost: '免费',
|
|
31
|
+
needsPython: true
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function readTtsConfig(workdir) {
|
|
36
|
+
const obj = cfg.readConfig(workdir) || {};
|
|
37
|
+
return {
|
|
38
|
+
provider: (obj.TTS && obj.TTS.provider) || 'xf',
|
|
39
|
+
TTS: obj.TTS || {},
|
|
40
|
+
Settings: obj.Settings || {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// === tts list ===
|
|
45
|
+
function cmdList(workdir) {
|
|
46
|
+
const c = readTtsConfig(workdir);
|
|
47
|
+
logger.log('=== TTS Provider 列表 ===');
|
|
48
|
+
logger.log('');
|
|
49
|
+
for (const key of Object.keys(PROVIDER_INFO)) {
|
|
50
|
+
const info = PROVIDER_INFO[key];
|
|
51
|
+
const active = c.provider === key ? '★ 当前' : ' ';
|
|
52
|
+
logger.log(`${active} ${key.padEnd(8)} ${info.name.padEnd(10)} ${info.desc}`);
|
|
53
|
+
logger.log(` 费用: ${info.cost}`);
|
|
54
|
+
// 凭证状态
|
|
55
|
+
const r = ttsDeps.checkDeps(workdir, key, { TTS: c.TTS, Settings: c.Settings });
|
|
56
|
+
const credOk = r.issues.filter(i => i.category === 'cred').length === 0;
|
|
57
|
+
const depOk = r.issues.filter(i => i.category !== 'cred').length === 0;
|
|
58
|
+
logger.log(` 凭证: ${credOk ? '✓' : '✗ 缺'} 依赖: ${depOk ? '✓' : '✗ 缺'}`);
|
|
59
|
+
logger.log('');
|
|
60
|
+
}
|
|
61
|
+
logger.log(`当前激活: ${c.provider}`);
|
|
62
|
+
logger.log('切换: adoremix tts config');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// === tts voices ===
|
|
67
|
+
function cmdVoices(workdir) {
|
|
68
|
+
const c = readTtsConfig(workdir);
|
|
69
|
+
logger.log(`=== voice 短名列表(当前 provider: ${c.provider})===`);
|
|
70
|
+
logger.log('');
|
|
71
|
+
// 读对应 provider 的映射
|
|
72
|
+
let providerVoices = {};
|
|
73
|
+
try {
|
|
74
|
+
const providerMod = require(`../tts/providers/${c.provider}`);
|
|
75
|
+
providerVoices = providerMod.voices || {};
|
|
76
|
+
} catch (e) {}
|
|
77
|
+
for (const v of STANDARD_VOICES) {
|
|
78
|
+
const mapped = providerVoices[v.short] || '(无映射)';
|
|
79
|
+
logger.log(` ${v.short.padEnd(12)} → ${mapped.padEnd(28)} ${v.desc}`);
|
|
80
|
+
}
|
|
81
|
+
logger.log('');
|
|
82
|
+
logger.log(`默认: ${DEFAULT_VOICE}`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// === tts deps ===
|
|
87
|
+
function cmdDeps(workdir, opts) {
|
|
88
|
+
const c = readTtsConfig(workdir);
|
|
89
|
+
logger.log(`=== TTS 依赖检查(provider: ${c.provider})===`);
|
|
90
|
+
logger.log('');
|
|
91
|
+
const r = ttsDeps.checkDeps(workdir, c.provider, { TTS: c.TTS, Settings: c.Settings });
|
|
92
|
+
if (r.issues.length === 0) {
|
|
93
|
+
logger.ok(`✓ ${c.provider} 所有依赖就绪`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
for (const iss of r.issues) {
|
|
97
|
+
const mark = iss.severity === 'error' ? '❌' : '⚠';
|
|
98
|
+
logger.log(`${mark} ${iss.msg}`);
|
|
99
|
+
if (iss.fixCmd) logger.log(` 修复: ${iss.fixCmd}`);
|
|
100
|
+
}
|
|
101
|
+
if (opts.fix) {
|
|
102
|
+
logger.log('');
|
|
103
|
+
logger.info('==> 自动修复...');
|
|
104
|
+
const sudo = process.getuid && process.getuid() === 0 ? false : true;
|
|
105
|
+
const result = ttsDeps.fixDeps(workdir, r.issues, { sudo });
|
|
106
|
+
logger.ok(`✓ 修复 ${result.fixed} 项,失败 ${result.failed} 项`);
|
|
107
|
+
if (result.failed > 0) {
|
|
108
|
+
logger.warn('部分失败,可能需要 sudo 重跑:sudo adoremix tts deps --fix');
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
logger.log('');
|
|
112
|
+
logger.log('自动修复: adoremix tts deps --fix');
|
|
113
|
+
}
|
|
114
|
+
return r.issues.some(i => i.severity === 'error') ? 1 : 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// === tts config ===
|
|
118
|
+
async function cmdConfig(workdir, opts) {
|
|
119
|
+
const c = readTtsConfig(workdir);
|
|
120
|
+
logger.log('=== TTS 配置向导 ===');
|
|
121
|
+
logger.log('');
|
|
122
|
+
|
|
123
|
+
// 1. 选 provider
|
|
124
|
+
const choices = Object.keys(PROVIDER_INFO).map(k => ({
|
|
125
|
+
title: `${k.padEnd(8)} ${PROVIDER_INFO[k].name} (${PROVIDER_INFO[k].cost})`,
|
|
126
|
+
value: k,
|
|
127
|
+
description: PROVIDER_INFO[k].desc
|
|
128
|
+
}));
|
|
129
|
+
const r1 = await prompts({
|
|
130
|
+
type: 'select',
|
|
131
|
+
name: 'provider',
|
|
132
|
+
message: '选择 TTS provider',
|
|
133
|
+
choices,
|
|
134
|
+
initial: Object.keys(PROVIDER_INFO).indexOf(c.provider)
|
|
135
|
+
});
|
|
136
|
+
if (!r1.provider) return 1;
|
|
137
|
+
cfg.setConfigValue(workdir, 'TTS.provider', r1.provider);
|
|
138
|
+
logger.ok(`TTS.provider = ${r1.provider}`);
|
|
139
|
+
|
|
140
|
+
// 2. 根据 provider 填凭证
|
|
141
|
+
if (r1.provider === 'xf') {
|
|
142
|
+
const fields = [
|
|
143
|
+
['Settings.ttsxfAPPID', 'appid', c.Settings.ttsxfAPPID],
|
|
144
|
+
['Settings.ttsxfAPISecret', 'apiSecret', c.Settings.ttsxfAPISecret],
|
|
145
|
+
['Settings.ttsxfAPIKey', 'apiKey', c.Settings.ttsxfAPIKey]
|
|
146
|
+
];
|
|
147
|
+
for (const [key, name, cur] of fields) {
|
|
148
|
+
const r = await prompts({
|
|
149
|
+
type: 'text',
|
|
150
|
+
name: 'v',
|
|
151
|
+
message: `讯飞 ${name}${cur ? '(回车保持当前)' : ''}`,
|
|
152
|
+
initial: cur || ''
|
|
153
|
+
});
|
|
154
|
+
if (r.v && r.v !== cur) cfg.setConfigValue(workdir, key, r.v);
|
|
155
|
+
}
|
|
156
|
+
} else if (r1.provider === 'minimax') {
|
|
157
|
+
const cur = c.TTS.minimax_token;
|
|
158
|
+
const r = await prompts({
|
|
159
|
+
type: 'password',
|
|
160
|
+
name: 'v',
|
|
161
|
+
message: `MiniMax API token${cur ? '(回车保持当前)' : ''}`
|
|
162
|
+
});
|
|
163
|
+
if (r.v && r.v !== cur) cfg.setConfigValue(workdir, 'TTS.minimax_token', r.v);
|
|
164
|
+
} else if (r1.provider === 'edge') {
|
|
165
|
+
logger.log('Edge TTS 是免费的,无需凭证。');
|
|
166
|
+
logger.log('需要: python3 + edge-tts (pip) + ffmpeg');
|
|
167
|
+
logger.log('检查: adoremix tts deps');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
logger.log('');
|
|
171
|
+
logger.ok('配置完成。建议跑:adoremix tts test "你好"');
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// === tts test <text> ===
|
|
176
|
+
async function cmdTest(workdir, text, opts) {
|
|
177
|
+
if (!text) {
|
|
178
|
+
logger.error('用法: adoremix tts test "你好世界"');
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
const c = readTtsConfig(workdir);
|
|
182
|
+
const voice = opts.voice || DEFAULT_VOICE;
|
|
183
|
+
const volume = String(opts.volume || 50);
|
|
184
|
+
const speed = String(opts.speed || 50);
|
|
185
|
+
|
|
186
|
+
// 检查依赖
|
|
187
|
+
const r = ttsDeps.checkDeps(workdir, c.provider, { TTS: c.TTS, Settings: c.Settings });
|
|
188
|
+
const errs = r.issues.filter(i => i.severity === 'error');
|
|
189
|
+
if (errs.length > 0) {
|
|
190
|
+
logger.error(`${c.provider} 缺依赖,先跑:adoremix tts deps --fix`);
|
|
191
|
+
for (const e of errs) logger.log(` ❌ ${e.msg}`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 准备文本 + 输出
|
|
196
|
+
const ttyDir = path.join(workdir, 'tty');
|
|
197
|
+
if (!fs.existsSync(ttyDir)) fs.mkdirSync(ttyDir, { recursive: true });
|
|
198
|
+
const txtFile = path.join(ttyDir, '_cli_test.txt');
|
|
199
|
+
const outFile = path.join(ttyDir, '_cli_test.mp3');
|
|
200
|
+
fs.writeFileSync(txtFile, text);
|
|
201
|
+
|
|
202
|
+
// 调用 dispatcher
|
|
203
|
+
const ttsJs = path.join(workdir, 'tts.js');
|
|
204
|
+
if (!fs.existsSync(ttsJs)) {
|
|
205
|
+
logger.error(`未找到 ${ttsJs}(先跑 adoremix install)`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
logger.info(`provider=${c.provider} voice=${voice} text="${text.slice(0, 40)}${text.length > 40 ? '...' : ''}"`);
|
|
209
|
+
try {
|
|
210
|
+
execSync(`node "${ttsJs}" ${volume} ${speed} ${voice} _cli_test.mp3 _cli_test.txt`, {
|
|
211
|
+
cwd: workdir,
|
|
212
|
+
stdio: 'inherit'
|
|
213
|
+
});
|
|
214
|
+
if (fs.existsSync(outFile)) {
|
|
215
|
+
const size = fs.statSync(outFile).size;
|
|
216
|
+
logger.ok(`✓ 生成成功: ${outFile} (${(size / 1024).toFixed(1)} KB)`);
|
|
217
|
+
return 0;
|
|
218
|
+
} else {
|
|
219
|
+
logger.error('未生成 mp3 文件');
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
logger.error(`测试失败: ${e.message.split('\n')[0]}`);
|
|
224
|
+
return 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
cmdList,
|
|
230
|
+
cmdVoices,
|
|
231
|
+
cmdDeps,
|
|
232
|
+
cmdConfig,
|
|
233
|
+
cmdTest,
|
|
234
|
+
PROVIDER_INFO
|
|
235
|
+
};
|
package/src/tts-deps.js
CHANGED
|
@@ -84,13 +84,19 @@ function nodeModuleInstalled(workdir, modName) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function getCredValue(cfg, dottedKey) {
|
|
87
|
+
// 直接读
|
|
87
88
|
const parts = dottedKey.split('.');
|
|
88
89
|
let cur = cfg;
|
|
89
90
|
for (const p of parts) {
|
|
90
91
|
if (cur == null || typeof cur !== 'object') return undefined;
|
|
91
92
|
cur = cur[p];
|
|
92
93
|
}
|
|
93
|
-
return cur;
|
|
94
|
+
if (cur) return cur;
|
|
95
|
+
// 兼容 Qt 已有字段:TTS.xf_appid → Settings.ttsxfAPPID
|
|
96
|
+
if (dottedKey === 'TTS.xf_appid' && cfg.Settings && cfg.Settings.ttsxfAPPID) return cfg.Settings.ttsxfAPPID;
|
|
97
|
+
if (dottedKey === 'TTS.xf_apiSecret' && cfg.Settings && cfg.Settings.ttsxfAPISecret) return cfg.Settings.ttsxfAPISecret;
|
|
98
|
+
if (dottedKey === 'TTS.xf_apiKey' && cfg.Settings && cfg.Settings.ttsxfAPIKey) return cfg.Settings.ttsxfAPIKey;
|
|
99
|
+
return undefined;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
function detectAptLike() {
|