@oxiaom/adoremix 1.0.8 → 1.0.9
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 +3 -2
- package/tts/providers/edge.js +136 -0
- package/tts/providers/minimax.js +124 -0
- package/tts/providers/xf.js +96 -0
- package/tts/tts-wrapper.js +19 -0
- package/tts/tts.js +90 -0
- package/tts/voice-mapping.js +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxiaom/adoremix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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",
|
|
@@ -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,90 @@
|
|
|
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
|
+
const creds = {
|
|
68
|
+
xf_appid: ttsSection.xf_appid || appidArg || '',
|
|
69
|
+
xf_apiSecret: ttsSection.xf_apiSecret || apiSecretArg || '',
|
|
70
|
+
xf_apiKey: ttsSection.xf_apiKey || apiKeyArg || '',
|
|
71
|
+
xf_voice_override: ttsSection.xf_voice_override || '',
|
|
72
|
+
minimax_token: ttsSection.minimax_token || '',
|
|
73
|
+
edge_voice_override: ttsSection.edge_voice_override || ''
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
log(`provider=${providerName} voice=${voice} text=${text.length}字 → ${mp3Name}`);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await provider.synthesize(
|
|
80
|
+
{ text, voice, volume, speed, outFile },
|
|
81
|
+
creds
|
|
82
|
+
);
|
|
83
|
+
log(`✓ ${result.provider} 生成成功: ${result.outFile} (voice=${result.voice})`);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
err(`${providerName} 失败: ${e.message}`);
|
|
86
|
+
process.exit(4);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
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 };
|