@oxiaom/adoremix 1.0.40 → 1.0.42
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 +52 -0
- package/src/config/defaults.js +3 -1
- package/src/tts-cli.js +6 -0
- package/src/tts-deps.js +46 -0
- package/tts/providers/audio8.js +301 -0
- package/tts/tts.js +3 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -465,6 +465,58 @@ function buildProgram() {
|
|
|
465
465
|
try { process.exitCode = mod.status(); } catch (e) { logger.error(e.message); process.exitCode = 1; }
|
|
466
466
|
});
|
|
467
467
|
|
|
468
|
+
const audio8 = program
|
|
469
|
+
.command('audio8')
|
|
470
|
+
.description('Audio8 TTS(本地自部署流式 TTS,自动安装到远程 Linux)');
|
|
471
|
+
|
|
472
|
+
audio8
|
|
473
|
+
.command('install')
|
|
474
|
+
.description('通过 SSH 远程安装 Audio8 TTS 到 Linux 主机(默认 192.168.1.114:8024),自动启动 systemd 服务')
|
|
475
|
+
.option('--host <ip>', '远程主机 IP', '192.168.1.114')
|
|
476
|
+
.option('--user <name>', 'SSH 用户', 'oxiaom')
|
|
477
|
+
.option('--pass <pwd>', 'SSH 密码')
|
|
478
|
+
.option('--workdir <path>', '本地工作目录(写入 config.ini 用)')
|
|
479
|
+
.action(async (opts) => {
|
|
480
|
+
const mod = require('../tts/providers/audio8');
|
|
481
|
+
try {
|
|
482
|
+
process.exitCode = await mod.install(opts);
|
|
483
|
+
} catch (e) {
|
|
484
|
+
logger.error(e.message);
|
|
485
|
+
process.exitCode = 1;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
audio8
|
|
490
|
+
.command('uninstall')
|
|
491
|
+
.description('卸载远程 Linux 上的 Audio8 TTS 服务')
|
|
492
|
+
.option('--host <ip>', '远程主机 IP', '192.168.1.114')
|
|
493
|
+
.option('--user <name>', 'SSH 用户', 'oxiaom')
|
|
494
|
+
.option('--pass <pwd>', 'SSH 密码')
|
|
495
|
+
.action(async (opts) => {
|
|
496
|
+
const mod = require('../tts/providers/audio8');
|
|
497
|
+
try {
|
|
498
|
+
process.exitCode = await mod.uninstall(opts);
|
|
499
|
+
} catch (e) {
|
|
500
|
+
logger.error(e.message);
|
|
501
|
+
process.exitCode = 1;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
audio8
|
|
506
|
+
.command('status')
|
|
507
|
+
.description('检查远程 Audio8 TTS 服务健康')
|
|
508
|
+
.option('--host <ip>', '远程主机 IP', '192.168.1.114')
|
|
509
|
+
.option('--url <url>', '自定义 base_url')
|
|
510
|
+
.action(async (opts) => {
|
|
511
|
+
const mod = require('../tts/providers/audio8');
|
|
512
|
+
try {
|
|
513
|
+
process.exitCode = await mod.status(opts);
|
|
514
|
+
} catch (e) {
|
|
515
|
+
logger.error(e.message);
|
|
516
|
+
process.exitCode = 1;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
468
520
|
const cfgmgr = program
|
|
469
521
|
.command('config-manager')
|
|
470
522
|
.description('设备配置管理 UI(配置 IP / 修改 config.ini / 查看日志),端口 9877,开机自启');
|
package/src/config/defaults.js
CHANGED
package/src/tts-cli.js
CHANGED
package/src/tts-deps.js
CHANGED
|
@@ -50,6 +50,17 @@ const PROVIDER_DEPENDENCIES = {
|
|
|
50
50
|
],
|
|
51
51
|
pipPkgs: [{ pkg: 'edge-tts', import: 'edge_tts' }],
|
|
52
52
|
executables: [{ name: 'ffmpeg', cmd: 'ffmpeg' }, ...TRANSCODE_TOOLS]
|
|
53
|
+
},
|
|
54
|
+
audio8: {
|
|
55
|
+
nodeModules: [], // 用 Node 内置 http/https
|
|
56
|
+
creds: [], // 本地服务无 API key
|
|
57
|
+
systemPkgs: [
|
|
58
|
+
{ name: 'ffmpeg', cmd: 'ffmpeg' } // 转码用(实际 audio8 直接返回 mp3 不用 ffmpeg,但保留 fallback)
|
|
59
|
+
],
|
|
60
|
+
pipPkgs: [], // 纯 HTTP 调用,无 Python 依赖
|
|
61
|
+
executables: [...TRANSCODE_TOOLS],
|
|
62
|
+
// 额外:需要本地 Audio8 TTS 服务可达(检查时探测 /v1/models)
|
|
63
|
+
extraChecks: ['http_endpoint']
|
|
53
64
|
}
|
|
54
65
|
};
|
|
55
66
|
|
|
@@ -230,6 +241,41 @@ function checkDeps(workdir, providerName, cfg) {
|
|
|
230
241
|
});
|
|
231
242
|
}
|
|
232
243
|
|
|
244
|
+
// audio8:探测本地服务可达性(GET /v1/models,超时 3 秒)
|
|
245
|
+
if (providerName === 'audio8') {
|
|
246
|
+
const http = require('http');
|
|
247
|
+
const https = require('https');
|
|
248
|
+
const url = require('url');
|
|
249
|
+
const audio8BaseUrl = (cfg && cfg.TTS && cfg.TTS.audio8_base_url) || 'http://127.0.0.1:7860';
|
|
250
|
+
let u;
|
|
251
|
+
try { u = new URL(audio8BaseUrl); } catch (e) {
|
|
252
|
+
issues.push({ severity: 'error', category: 'service', msg: `audio8_base_url 无效: ${audio8BaseUrl}(需 config.ini [TTS] audio8_base_url 配置,如 http://127.0.0.1:7860)`, autoFixable: false });
|
|
253
|
+
}
|
|
254
|
+
if (u) {
|
|
255
|
+
try {
|
|
256
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
257
|
+
const req = lib.request({
|
|
258
|
+
hostname: u.hostname,
|
|
259
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
260
|
+
path: '/v1/models',
|
|
261
|
+
method: 'GET',
|
|
262
|
+
timeout: 3000
|
|
263
|
+
}, res => {
|
|
264
|
+
if (res.statusCode !== 200) {
|
|
265
|
+
issues.push({ severity: 'error', category: 'service', msg: `Audio8 服务不可达:${audio8BaseUrl} 返回 HTTP ${res.statusCode}`, autoFixable: false });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
req.on('error', err => {
|
|
269
|
+
issues.push({ severity: 'error', category: 'service', msg: `Audio8 服务不可达(${audio8BaseUrl}):${err.message}。请确认 Audio8 TTS 服务已启动(参考 Audio8-AI/Audio8_TTS),并在 config.ini [TTS] audio8_base_url 配置正确地址。`, autoFixable: false });
|
|
270
|
+
});
|
|
271
|
+
req.on('timeout', () => { req.destroy(); issues.push({ severity: 'error', category: 'service', msg: `Audio8 服务超时(${audio8BaseUrl}/v1/models 3s 无响应)`, autoFixable: false }); });
|
|
272
|
+
req.end();
|
|
273
|
+
} catch (e) {
|
|
274
|
+
issues.push({ severity: 'error', category: 'service', msg: `audio8 服务探测失败:${e.message}`, autoFixable: false });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
233
279
|
return { issues, provider: providerName };
|
|
234
280
|
}
|
|
235
281
|
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Audio8 TTS provider
|
|
5
|
+
* 调用方式:HTTP 调用本地 Audio8 TTS 服务(OpenAI 兼容 /v1/audio/speech)
|
|
6
|
+
* 支持流式 MP3 输出(设备边收边播,匹配现有 quemp3 推送管道)
|
|
7
|
+
*
|
|
8
|
+
* 服务部署参考(Audio8-AI/Audio8_TTS):
|
|
9
|
+
* git clone https://github.com/Audio8-AI/Audio8_TTS.git ~/audio8
|
|
10
|
+
* cd ~/audio8/onnx_runtime && python3 -m venv .venv && . .venv/bin/activate
|
|
11
|
+
* pip install -r requirements.txt # torch/transformers/onnxruntime 等
|
|
12
|
+
* bash start_server.sh # 默认监听 http://0.0.0.0:8024
|
|
13
|
+
* 首次启动自动从 HuggingFace 下载 ~572MB 模型(0.1B-INT8)
|
|
14
|
+
*
|
|
15
|
+
* 凭证:不需要 API Key(本地服务)
|
|
16
|
+
* 依赖:lame(mp3 转码),ffmpeg(HTTP 流 → mp3)
|
|
17
|
+
*
|
|
18
|
+
* 速度支持:x-speed 头(0.5~2.0,1.0=正常;2.0=2x 加速)
|
|
19
|
+
* 声音克隆:3~10 秒参考音频,但需在 Audio8 服务端预设(请求时用 voice 参数指定)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { execFileSync } = require('child_process');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const https = require('https');
|
|
27
|
+
const http = require('http');
|
|
28
|
+
const { URL } = require('url');
|
|
29
|
+
|
|
30
|
+
// 中文常用 voice(Audio8 的 zh-CN voices,按性别分组)
|
|
31
|
+
const VOICES = {
|
|
32
|
+
// 女声
|
|
33
|
+
xiaoxiao: 'zh-CN-XiaoxiaoNeural', // 晓晓(温柔女声)
|
|
34
|
+
xiaoyi: 'zh-CN-XiaoyiNeural', // 晓伊(活力女声)
|
|
35
|
+
xiaomo: 'zh-CN-XiaomoNeural', // 晓墨(文艺女声)
|
|
36
|
+
xiaoxuan: 'zh-CN-XiaoxuanNeural', // 晓萱(新闻女声)
|
|
37
|
+
// 男声
|
|
38
|
+
yunxi: 'zh-CN-YunxiNeural', // 云希(青年男声)
|
|
39
|
+
yunjian: 'zh-CN-YunjianNeural', // 云健(浑厚男声)
|
|
40
|
+
yunyang: 'zh-CN-YunyangNeural', // 云扬(播音男声)
|
|
41
|
+
// 默认
|
|
42
|
+
default: 'zh-CN-XiaoxiaoNeural'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DEFAULT_BASE_URL = 'http://127.0.0.1:8024';
|
|
46
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
47
|
+
|
|
48
|
+
function checkDeps() {
|
|
49
|
+
const issues = [];
|
|
50
|
+
// ffmpeg:HTTP 流 → mp3 文件
|
|
51
|
+
try {
|
|
52
|
+
execFileSync('ffmpeg', ['-version'], { stdio: 'pipe' });
|
|
53
|
+
} catch (e) {
|
|
54
|
+
issues.push({ msg: '未找到 ffmpeg(需要将 HTTP 流保存为 mp3 文件)', fix: 'apt-get install -y ffmpeg (Debian/Ubuntu) 或 yum install -y ffmpeg (CentOS/AlmaLinux)' });
|
|
55
|
+
}
|
|
56
|
+
return issues;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 从 Audio8 服务拉 MP3 流,通过 ffmpeg 转为指定输出文件
|
|
60
|
+
// Audio8 服务返回 OpenAI 兼容的 audio/mpeg 流(直接是 mp3)
|
|
61
|
+
function streamToFile(audioStream, outFile) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
// 边收边写:tee 到临时文件,避免等全部下载
|
|
64
|
+
const tmpFile = outFile + '.part';
|
|
65
|
+
const ws = fs.createWriteStream(tmpFile);
|
|
66
|
+
let totalBytes = 0;
|
|
67
|
+
audioStream.on('data', chunk => {
|
|
68
|
+
totalBytes += chunk.length;
|
|
69
|
+
});
|
|
70
|
+
audioStream.on('error', err => {
|
|
71
|
+
ws.destroy();
|
|
72
|
+
try { fs.unlinkSync(tmpFile); } catch (e) {}
|
|
73
|
+
reject(new Error(`Audio8 流接收错误: ${err.message}`));
|
|
74
|
+
});
|
|
75
|
+
audioStream.pipe(ws);
|
|
76
|
+
ws.on('finish', () => {
|
|
77
|
+
try {
|
|
78
|
+
// Audio8 直接返回 audio/mpeg(mp3 流),无需 ffmpeg 转码
|
|
79
|
+
fs.renameSync(tmpFile, outFile);
|
|
80
|
+
resolve(totalBytes);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
reject(new Error(`Audio8 临时文件改名失败: ${e.message}`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
ws.on('error', err => {
|
|
86
|
+
try { fs.unlinkSync(tmpFile); } catch (e) {}
|
|
87
|
+
reject(new Error(`Audio8 写文件失败: ${err.message}`));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function synthesize({ text, voice, volume, speed, outFile }, creds) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const issues = checkDeps();
|
|
95
|
+
if (issues.length > 0) {
|
|
96
|
+
return reject(new Error('Audio8 依赖缺失:\n ' + issues.map(i => i.msg + ' (修复: ' + i.fix + ')').join('\n ')));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const baseUrl = (creds && creds.audio8_base_url) || DEFAULT_BASE_URL;
|
|
100
|
+
const wantedVoice = VOICES[voice] || VOICES.default;
|
|
101
|
+
// 音量:Audio8 用 dB 偏移(-6~6 dB),UI 传的是 0~100 百分比,转 dB
|
|
102
|
+
const vo = parseInt(volume) || 50;
|
|
103
|
+
const volumeDb = Math.round((vo - 50) * 0.12);
|
|
104
|
+
// 速度:Audio8 用 0.5~2.0 倍速,UI 传 0~100 转 0.5~2.0
|
|
105
|
+
const sp = parseInt(speed) || 50;
|
|
106
|
+
const speedMul = 0.5 + (sp / 100) * 1.5;
|
|
107
|
+
|
|
108
|
+
// OpenAI 兼容调用:POST /v1/audio/speech,body=json,response=audio/mpeg 流
|
|
109
|
+
let urlObj;
|
|
110
|
+
try { urlObj = new URL(baseUrl); } catch (e) {
|
|
111
|
+
return reject(new Error(`Audio8 服务地址无效: ${baseUrl}(请在 config.ini [TTS] audio8_base_url 配置,例 http://127.0.0.1:8024)`));
|
|
112
|
+
}
|
|
113
|
+
urlObj.pathname = '/v1/audio/speech';
|
|
114
|
+
const postData = JSON.stringify({
|
|
115
|
+
input: text,
|
|
116
|
+
voice: wantedVoice,
|
|
117
|
+
model: 'audio8-tts',
|
|
118
|
+
response_format: 'mp3',
|
|
119
|
+
speed: speedMul,
|
|
120
|
+
volume: volumeDb
|
|
121
|
+
});
|
|
122
|
+
const opts = {
|
|
123
|
+
hostname: urlObj.hostname,
|
|
124
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
125
|
+
path: urlObj.pathname,
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
130
|
+
},
|
|
131
|
+
timeout: DEFAULT_TIMEOUT_MS
|
|
132
|
+
};
|
|
133
|
+
const lib = urlObj.protocol === 'https:' ? https : http;
|
|
134
|
+
const req = lib.request(opts, res => {
|
|
135
|
+
if (res.statusCode !== 200) {
|
|
136
|
+
let body = '';
|
|
137
|
+
res.on('data', c => body += c);
|
|
138
|
+
res.on('end', () => {
|
|
139
|
+
reject(new Error(`Audio8 返回 HTTP ${res.statusCode}: ${body.slice(0, 200)}`));
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// 流式保存
|
|
144
|
+
streamToFile(res, outFile)
|
|
145
|
+
.then(bytes => resolve({ bytes }))
|
|
146
|
+
.catch(reject);
|
|
147
|
+
});
|
|
148
|
+
req.on('error', err => {
|
|
149
|
+
reject(new Error(`Audio8 连接失败(${baseUrl}): ${err.message}。请确认 Audio8 TTS 服务已启动,且 config.ini [TTS] audio8_base_url 配置正确。`));
|
|
150
|
+
});
|
|
151
|
+
req.on('timeout', () => {
|
|
152
|
+
req.destroy();
|
|
153
|
+
reject(new Error(`Audio8 请求超时(${DEFAULT_TIMEOUT_MS / 1000}s)。服务可能在转码长文本。`));
|
|
154
|
+
});
|
|
155
|
+
req.write(postData);
|
|
156
|
+
req.end();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ===== audio8 install / uninstall / status(类似 nginx install 的可选组件)=====
|
|
161
|
+
// 通过 SSH 远程部署到 Linux 主机,Windows 本机调用只需装 plink。
|
|
162
|
+
// 默认目标:192.168.1.114:22 / 用户 oxiaom。改 SSH_TARGET/SSH_USER 环境变量或 --host/--user 参数即可换机器。
|
|
163
|
+
const AUDIO8_DEFAULT_PORT = 8024;
|
|
164
|
+
const AUDIO8_REPO = 'https://github.com/Audio8-AI/Audio8_TTS.git';
|
|
165
|
+
const AUDIO8_DEPLOY_DIR = '~/audio8';
|
|
166
|
+
const AUDIO8_VENV_DIR = '~/audio8/onnx_runtime/.venv';
|
|
167
|
+
const AUDIO8_REQUIREMENTS = 'onnx_runtime/requirements.txt';
|
|
168
|
+
const AUDIO8_SYSTEMD_NAME = 'audio8-tts';
|
|
169
|
+
const AUDIO8_SYSTEMD_PATH = '/etc/systemd/system/audio8-tts.service';
|
|
170
|
+
const AUDIO8_HEALTH_URL = `http://127.0.0.1:${AUDIO8_DEFAULT_PORT}/v1/models`;
|
|
171
|
+
|
|
172
|
+
function plink() {
|
|
173
|
+
for (const cmd of ['plink', '"C:\\Program Files\\PuTTY\\plink.exe"']) {
|
|
174
|
+
try { return execSync(cmd.replace(/"/g, '').split(' ')[0], { stdio: 'pipe' }); } catch (e) {}
|
|
175
|
+
}
|
|
176
|
+
try { return require('child_process').execSync('where plink', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]; } catch (e) {}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sshExec(cmd, opts) {
|
|
181
|
+
opts = opts || {};
|
|
182
|
+
const host = opts.host || process.env.ADOREMIX_AUDIO8_SSH_HOST || '192.168.1.114';
|
|
183
|
+
const user = opts.user || process.env.ADOREMIX_AUDIO8_SSH_USER || 'oxiaom';
|
|
184
|
+
const pass = opts.pass || process.env.ADOREMIX_AUDIO8_SSH_PASS || '123123';
|
|
185
|
+
const pl = plink();
|
|
186
|
+
if (!pl) throw new Error('未找到 plink(PuTTY),请安装 PuTTY 或将其加入 PATH');
|
|
187
|
+
return execSync(`"${pl}" -ssh -batch -pw "${pass}" ${user}@${host} '${cmd.replace(/'/g, "'\\''")}'`, { stdio: 'pipe', encoding: 'utf8' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildSystemdUnit(pyCmd) {
|
|
191
|
+
return `[Unit]
|
|
192
|
+
Description=Audio8 TTS (AdoreMix)
|
|
193
|
+
After=network.target
|
|
194
|
+
|
|
195
|
+
[Service]
|
|
196
|
+
Type=simple
|
|
197
|
+
User=root
|
|
198
|
+
WorkingDirectory=${AUDIO8_DEPLOY_DIR}/onnx_runtime
|
|
199
|
+
Environment=PATH=${AUDIO8_VENV_DIR}/bin:/usr/bin:/bin
|
|
200
|
+
ExecStart=${AUDIO8_VENV_DIR}/bin/python -m arktts_runtime.service --model-dir ./model --voices-dir ./model/voices --port ${AUDIO8_DEFAULT_PORT}
|
|
201
|
+
Restart=on-failure
|
|
202
|
+
RestartSec=5
|
|
203
|
+
|
|
204
|
+
[Install]
|
|
205
|
+
WantedBy=multi-user.target
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function install(opts) {
|
|
210
|
+
opts = opts || {};
|
|
211
|
+
logger.ok('开始远程安装 Audio8 TTS(目标: ' + (opts.host || '192.168.1.114') + ')');
|
|
212
|
+
const cmds = [
|
|
213
|
+
'echo "=== 系统检测 ==="',
|
|
214
|
+
'python3 --version',
|
|
215
|
+
'pip3 --version',
|
|
216
|
+
'git --version',
|
|
217
|
+
'echo "=== 安装系统依赖 ==="',
|
|
218
|
+
'apt-get update && apt-get install -y git python3-pip python3-venv ffmpeg',
|
|
219
|
+
'echo "=== clone 仓库(如果不存在)==="',
|
|
220
|
+
`if [ ! -d ${AUDIO8_DEPLOY_DIR} ]; then git clone ${AUDIO8_REPO} ${AUDIO8_DEPLOY_DIR}; else echo "已存在,跳过"; fi`,
|
|
221
|
+
'echo "=== 创建 venv 并装依赖 ==="',
|
|
222
|
+
`cd ${AUDIO8_DEPLOY_DIR}/onnx_runtime && python3 -m venv .venv`,
|
|
223
|
+
`${AUDIO8_VENV_DIR}/bin/pip install -U pip`,
|
|
224
|
+
`${AUDIO8_VENV_DIR}/bin/pip install -r ${AUDIO8_REQUIREMENTS}`,
|
|
225
|
+
'echo "=== 写 systemd 单元 ==="',
|
|
226
|
+
// systemd 单元写入使用 heredoc 转义(这里通过 echo + 重定向)
|
|
227
|
+
`bash -c 'cat > ${AUDIO8_SYSTEMD_PATH} <<\"EOF\"\n${buildSystemdUnit()}\nEOF'`,
|
|
228
|
+
'systemctl daemon-reload',
|
|
229
|
+
`systemctl enable ${AUDIO8_SYSTEMD_NAME}`,
|
|
230
|
+
`systemctl restart ${AUDIO8_SYSTEMD_NAME}`,
|
|
231
|
+
'echo "=== 等待服务健康(首次启动要下载 ~572MB 模型,最多 60s)==="',
|
|
232
|
+
`for i in $(seq 1 60); do sleep 2; if curl -sf ${AUDIO8_HEALTH_URL} >/dev/null 2>&1; then echo "OK after ${i} attempts"; break; fi; done`,
|
|
233
|
+
`curl -sf ${AUDIO8_HEALTH_URL} >/dev/null && echo "✓ Audio8 服务健康: http://127.0.0.1:${AUDIO8_DEFAULT_PORT}" || echo "⚠ 60s 内未就绪,可手动 systemctl status ${AUDIO8_SYSTEMD_NAME}"`
|
|
234
|
+
];
|
|
235
|
+
const fullCmd = cmds.join(' && ');
|
|
236
|
+
try {
|
|
237
|
+
const out = sshExec(fullCmd, opts);
|
|
238
|
+
process.stdout.write(out);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
logger.error('SSH 执行失败:' + e.message.slice(0, 200));
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
// 修改本地 config.ini 的 [TTS] provider/base_url
|
|
244
|
+
try {
|
|
245
|
+
const cfg = require(path.join(__dirname, '..', 'src', 'config'));
|
|
246
|
+
const localHost = opts.host || '192.168.1.114';
|
|
247
|
+
cfg.setConfigValue(opts.workdir || path.join(os.homedir(), '.local', 'share', 'adoremix'), 'TTS.provider', 'audio8');
|
|
248
|
+
cfg.setConfigValue(opts.workdir || path.join(os.homedir(), '.local', 'share', 'adoremix'), 'TTS.audio8_base_url', `http://${localHost}:${AUDIO8_DEFAULT_PORT}`);
|
|
249
|
+
logger.ok(`本地 config.ini 已更新: provider=audio8, audio8_base_url=http://${localHost}:${AUDIO8_DEFAULT_PORT}`);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
logger.warn('本地 config.ini 更新失败(可手动设置):' + e.message.slice(0, 100));
|
|
252
|
+
}
|
|
253
|
+
logger.ok('Audio8 TTS 远程安装完成');
|
|
254
|
+
logger.log(`访问: http://${opts.host || '192.168.1.114'}:${AUDIO8_DEFAULT_PORT}/docs`);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function uninstall(opts) {
|
|
259
|
+
opts = opts || {};
|
|
260
|
+
logger.ok('开始远程卸载 Audio8 TTS');
|
|
261
|
+
const cmds = [
|
|
262
|
+
`systemctl --now disable ${AUDIO8_SYSTEMD_NAME} 2>/dev/null; true`,
|
|
263
|
+
`systemctl stop ${AUDIO8_SYSTEMD_NAME} 2>/dev/null; true`,
|
|
264
|
+
`rm -f ${AUDIO8_SYSTEMD_PATH}`,
|
|
265
|
+
'systemctl daemon-reload',
|
|
266
|
+
`rm -rf ${AUDIO8_DEPLOY_DIR}`,
|
|
267
|
+
`rm -rf ~/.cache/huggingface/hub/models--Audio8*`
|
|
268
|
+
];
|
|
269
|
+
try {
|
|
270
|
+
const out = sshExec(cmds.join(' && '), opts);
|
|
271
|
+
process.stdout.write(out);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
logger.error('SSH 执行失败:' + e.message.slice(0, 200));
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
logger.ok('Audio8 TTS 远程卸载完成');
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function status(opts) {
|
|
281
|
+
opts = opts || {};
|
|
282
|
+
const baseUrl = (opts.url || process.env.ADOREMIX_AUDIO8_BASE_URL || `http://${opts.host || '192.168.1.114'}:${AUDIO8_DEFAULT_PORT}`);
|
|
283
|
+
const urlObj = new URL(`${baseUrl}/v1/models`);
|
|
284
|
+
const lib = urlObj.protocol === 'https:' ? require('https') : http;
|
|
285
|
+
return new Promise(resolve => {
|
|
286
|
+
const req = lib.request({ hostname: urlObj.hostname, port: urlObj.port, path: urlObj.pathname, method: 'GET', timeout: 5000 }, res => {
|
|
287
|
+
if (res.statusCode === 200) {
|
|
288
|
+
logger.ok(`Audio8 服务健康: ${baseUrl}/v1/models 返回 ${res.statusCode}`);
|
|
289
|
+
resolve(0);
|
|
290
|
+
} else {
|
|
291
|
+
logger.warn(`Audio8 服务异常: ${baseUrl} 返回 ${res.statusCode}`);
|
|
292
|
+
resolve(1);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
req.on('error', err => { logger.warn(`Audio8 服务不可达: ${err.message}`); resolve(1); });
|
|
296
|
+
req.on('timeout', () => { req.destroy(); logger.warn(`Audio8 服务超时: ${baseUrl}`); resolve(1); });
|
|
297
|
+
req.end();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = { synthesize, checkDeps, VOICES, DEFAULT_BASE_URL, install, uninstall, status };
|
package/tts/tts.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - xf: 讯飞 WebSocket(原 tts.js 逻辑,需 appid/apiSecret/apiKey)
|
|
12
12
|
* - minimax: MiniMax HTTP API(需 token)
|
|
13
13
|
* - edge: python edge-tts(免费,需 python3 + ffmpeg)
|
|
14
|
+
* - audio8: 本地 Audio8 TTS(OpenAI 兼容 API,需 audio8_base_url 配置 + ffmpeg)
|
|
14
15
|
*
|
|
15
16
|
* 凭证优先从 config.ini 读,命令行 appid/apiSecret/apiKey 兼容旧调用(仅 xf)。
|
|
16
17
|
*/
|
|
@@ -22,7 +23,8 @@ const ini = require('ini');
|
|
|
22
23
|
const PROVIDERS = {
|
|
23
24
|
xf: require('./providers/xf'),
|
|
24
25
|
minimax: require('./providers/minimax'),
|
|
25
|
-
edge: require('./providers/edge')
|
|
26
|
+
edge: require('./providers/edge'),
|
|
27
|
+
audio8: require('./providers/audio8')
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
function log(...args) { console.log('[tts]', ...args); }
|