@mingxy/ocosay 1.0.2 → 1.0.3
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/README.md +21 -0
- package/dist/config.d.ts +21 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -1
- package/dist/config.js.map +1 -1
- package/dist/plugin.js +1907 -138
- package/dist/plugin.js.map +7 -1
- package/package.json +4 -2
- package/src/config.ts +36 -2
package/dist/plugin.js
CHANGED
|
@@ -1,162 +1,1931 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
|
|
4
|
+
// src/core/speaker.ts
|
|
5
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
6
|
+
|
|
7
|
+
// src/core/types.ts
|
|
8
|
+
var TTSError = class extends Error {
|
|
9
|
+
constructor(message, code, provider, details) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "TTSError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.provider = provider;
|
|
14
|
+
this.details = details;
|
|
15
|
+
}
|
|
16
|
+
code;
|
|
17
|
+
provider;
|
|
18
|
+
details;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/providers/base.ts
|
|
22
|
+
var providers = /* @__PURE__ */ new Map();
|
|
23
|
+
function registerProvider(name, provider) {
|
|
24
|
+
if (providers.has(name)) {
|
|
25
|
+
throw new TTSError(
|
|
26
|
+
`Provider "${name}" is already registered`,
|
|
27
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
28
|
+
"system"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
providers.set(name, provider);
|
|
32
|
+
}
|
|
33
|
+
function getProvider(name) {
|
|
34
|
+
const provider = providers.get(name);
|
|
35
|
+
if (!provider) {
|
|
36
|
+
throw new TTSError(
|
|
37
|
+
`TTS Provider "${name}" not found`,
|
|
38
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
39
|
+
"system"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return provider;
|
|
43
|
+
}
|
|
44
|
+
function listProviders() {
|
|
45
|
+
return Array.from(providers.keys());
|
|
46
|
+
}
|
|
47
|
+
function hasProvider(name) {
|
|
48
|
+
return providers.has(name);
|
|
49
|
+
}
|
|
50
|
+
var BaseTTSProvider = class {
|
|
51
|
+
apiKey;
|
|
52
|
+
defaultVoice;
|
|
53
|
+
defaultModel = "stream";
|
|
54
|
+
async initialize() {
|
|
55
|
+
}
|
|
56
|
+
async destroy() {
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 通用 speak 实现,处理通用逻辑
|
|
60
|
+
*/
|
|
61
|
+
async speak(text, options) {
|
|
62
|
+
if (!text || text.trim().length === 0) {
|
|
63
|
+
throw new TTSError(
|
|
64
|
+
"Text cannot be empty",
|
|
65
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
66
|
+
this.name
|
|
67
|
+
);
|
|
23
68
|
}
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
69
|
+
const voice = options?.voice || this.defaultVoice;
|
|
70
|
+
const model = options?.model || this.defaultModel;
|
|
71
|
+
return this.doSpeak(text, voice, model, options);
|
|
72
|
+
}
|
|
73
|
+
pause() {
|
|
74
|
+
throw new TTSError(
|
|
75
|
+
"Pause is not supported by this provider",
|
|
76
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
77
|
+
this.name
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
resume() {
|
|
81
|
+
throw new TTSError(
|
|
82
|
+
"Resume is not supported by this provider",
|
|
83
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
84
|
+
this.name
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
stop() {
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
}
|
|
90
|
+
async listVoices() {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
getCapabilities() {
|
|
94
|
+
return this.capabilities;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 验证 API Key
|
|
98
|
+
*/
|
|
99
|
+
validateApiKey() {
|
|
100
|
+
if (!this.apiKey) {
|
|
101
|
+
throw new TTSError(
|
|
102
|
+
`API key is required for provider "${this.name}"`,
|
|
103
|
+
"AUTH" /* AUTH */,
|
|
104
|
+
this.name
|
|
105
|
+
);
|
|
34
106
|
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/core/player.ts
|
|
111
|
+
import { EventEmitter } from "events";
|
|
112
|
+
import { spawn } from "child_process";
|
|
113
|
+
import fs from "fs";
|
|
114
|
+
import { createWriteStream } from "fs";
|
|
115
|
+
import { tmpdir } from "os";
|
|
116
|
+
import { join } from "path";
|
|
117
|
+
var AudioPlayer = class extends EventEmitter {
|
|
118
|
+
constructor(events) {
|
|
119
|
+
super();
|
|
120
|
+
this.events = events;
|
|
121
|
+
}
|
|
122
|
+
events;
|
|
123
|
+
_playing = false;
|
|
124
|
+
_paused = false;
|
|
125
|
+
currentProcess;
|
|
126
|
+
currentFile;
|
|
127
|
+
/**
|
|
128
|
+
* 播放音频
|
|
129
|
+
* @param audioData 音频数据 (Buffer 或 ReadableStream)
|
|
130
|
+
* @param format 音频格式 (mp3, wav, etc.)
|
|
131
|
+
*/
|
|
132
|
+
async play(audioData, format) {
|
|
133
|
+
if (this._playing) {
|
|
134
|
+
await this.stop();
|
|
135
|
+
}
|
|
136
|
+
this._playing = true;
|
|
137
|
+
this._paused = false;
|
|
138
|
+
try {
|
|
139
|
+
const tempFile = join(tmpdir(), `ocosay-${Date.now()}.${format}`);
|
|
140
|
+
this.currentFile = tempFile;
|
|
141
|
+
if (Buffer.isBuffer(audioData)) {
|
|
142
|
+
fs.writeFileSync(tempFile, audioData);
|
|
143
|
+
} else {
|
|
144
|
+
const writeStream = createWriteStream(tempFile);
|
|
145
|
+
const reader = audioData.getReader();
|
|
146
|
+
try {
|
|
147
|
+
while (true) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (done) break;
|
|
150
|
+
if (value instanceof Uint8Array) {
|
|
151
|
+
writeStream.write(Buffer.from(value));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
writeStream.end();
|
|
155
|
+
} finally {
|
|
156
|
+
reader.releaseLock();
|
|
43
157
|
}
|
|
44
|
-
|
|
158
|
+
await new Promise((resolve, reject) => {
|
|
159
|
+
writeStream.on("finish", resolve);
|
|
160
|
+
writeStream.on("error", reject);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
this.events?.onStart?.();
|
|
164
|
+
this.emit("start");
|
|
165
|
+
await this.playFile(tempFile, format);
|
|
166
|
+
this._playing = false;
|
|
167
|
+
this.events?.onEnd?.();
|
|
168
|
+
this.emit("end");
|
|
169
|
+
this.cleanup();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this._playing = false;
|
|
172
|
+
const ttsError = new TTSError(
|
|
173
|
+
error.message || "Playback failed",
|
|
174
|
+
"PLAYER_ERROR" /* PLAYER_ERROR */,
|
|
175
|
+
"player"
|
|
176
|
+
);
|
|
177
|
+
this.events?.onError?.(ttsError);
|
|
178
|
+
this.emit("error", ttsError);
|
|
179
|
+
throw ttsError;
|
|
45
180
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 播放音频文件
|
|
184
|
+
* 优先使用 afplay (macOS), aplay (Linux), 否则用 PowerShell (Windows)
|
|
185
|
+
*/
|
|
186
|
+
playFile(filePath, format) {
|
|
187
|
+
const platform = process.platform;
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
let command;
|
|
190
|
+
let args;
|
|
191
|
+
if (platform === "darwin") {
|
|
192
|
+
command = "afplay";
|
|
193
|
+
args = [filePath];
|
|
194
|
+
} else if (platform === "linux") {
|
|
195
|
+
command = "aplay";
|
|
196
|
+
args = [filePath];
|
|
197
|
+
} else {
|
|
198
|
+
command = "powershell";
|
|
199
|
+
args = ["-c", `(New-Object System.Media.SoundPlayer('${filePath.replace(/\\/g, "\\\\")}')).PlaySync()`];
|
|
200
|
+
}
|
|
201
|
+
this.currentProcess = spawn(command, args, {
|
|
202
|
+
stdio: "ignore",
|
|
203
|
+
detached: false
|
|
204
|
+
});
|
|
205
|
+
this.currentProcess.on("exit", (code, signal) => {
|
|
206
|
+
if (signal === "SIGTERM" || signal === "SIGINT") {
|
|
207
|
+
resolve();
|
|
208
|
+
} else if (code === 0) {
|
|
209
|
+
resolve();
|
|
210
|
+
} else {
|
|
211
|
+
reject(new Error(`Player exited with code ${code}`));
|
|
54
212
|
}
|
|
55
|
-
|
|
213
|
+
});
|
|
214
|
+
this.currentProcess.on("error", (error) => {
|
|
215
|
+
reject(error);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 暂停播放
|
|
221
|
+
* 注意: 目前通过 SIGSTOP 实现,真正的 pause 需要支持暂停的音频库
|
|
222
|
+
*/
|
|
223
|
+
pause() {
|
|
224
|
+
if (!this._playing || this._paused) return;
|
|
225
|
+
if (this.currentProcess) {
|
|
226
|
+
try {
|
|
227
|
+
this.currentProcess.kill("SIGSTOP");
|
|
228
|
+
this._paused = true;
|
|
229
|
+
this.events?.onPause?.();
|
|
230
|
+
this.emit("pause");
|
|
231
|
+
} catch (e) {
|
|
232
|
+
}
|
|
56
233
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 恢复播放
|
|
237
|
+
*/
|
|
238
|
+
resume() {
|
|
239
|
+
if (!this._playing || !this._paused) return;
|
|
240
|
+
if (this.currentProcess) {
|
|
241
|
+
try {
|
|
242
|
+
this.currentProcess.kill("SIGCONT");
|
|
243
|
+
this._paused = false;
|
|
244
|
+
this.events?.onResume?.();
|
|
245
|
+
this.emit("resume");
|
|
246
|
+
} catch (e) {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 停止播放
|
|
252
|
+
*/
|
|
253
|
+
async stop() {
|
|
254
|
+
this._playing = false;
|
|
255
|
+
this._paused = false;
|
|
256
|
+
if (this.currentProcess) {
|
|
257
|
+
try {
|
|
258
|
+
this.currentProcess.kill("SIGTERM");
|
|
259
|
+
} catch (e) {
|
|
260
|
+
}
|
|
261
|
+
this.currentProcess = void 0;
|
|
262
|
+
}
|
|
263
|
+
this.cleanup();
|
|
264
|
+
this.events?.onStop?.();
|
|
265
|
+
this.emit("stop");
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 是否正在播放
|
|
269
|
+
*/
|
|
270
|
+
isPlaying() {
|
|
271
|
+
return this._playing;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 是否暂停
|
|
275
|
+
*/
|
|
276
|
+
isPaused() {
|
|
277
|
+
return this._paused;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 清理临时文件
|
|
281
|
+
*/
|
|
282
|
+
cleanup() {
|
|
283
|
+
if (this.currentFile) {
|
|
284
|
+
try {
|
|
285
|
+
if (fs.existsSync(this.currentFile)) {
|
|
286
|
+
fs.unlinkSync(this.currentFile);
|
|
67
287
|
}
|
|
68
|
-
|
|
288
|
+
} catch (e) {
|
|
289
|
+
}
|
|
290
|
+
this.currentFile = void 0;
|
|
69
291
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/core/speaker.ts
|
|
296
|
+
var Speaker = class extends EventEmitter2 {
|
|
297
|
+
constructor(options = {}) {
|
|
298
|
+
super();
|
|
299
|
+
this.options = options;
|
|
300
|
+
const playerEvents = {
|
|
301
|
+
onStart: () => this.emit("start", this.currentText),
|
|
302
|
+
onEnd: () => {
|
|
303
|
+
this.isSpeaking = false;
|
|
304
|
+
this.emit("end", this.currentText);
|
|
305
|
+
},
|
|
306
|
+
onError: (error) => this.emit("error", error),
|
|
307
|
+
onPause: () => {
|
|
308
|
+
this.isPaused = true;
|
|
309
|
+
this.emit("pause");
|
|
310
|
+
},
|
|
311
|
+
onResume: () => {
|
|
312
|
+
this.isPaused = false;
|
|
313
|
+
this.emit("resume");
|
|
314
|
+
},
|
|
315
|
+
onStop: () => {
|
|
316
|
+
this.isSpeaking = false;
|
|
317
|
+
this.isPaused = false;
|
|
318
|
+
this.emit("stop");
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
this.player = new AudioPlayer(playerEvents);
|
|
322
|
+
}
|
|
323
|
+
options;
|
|
324
|
+
currentProvider;
|
|
325
|
+
player;
|
|
326
|
+
currentText;
|
|
327
|
+
isSpeaking = false;
|
|
328
|
+
isPaused = false;
|
|
329
|
+
/**
|
|
330
|
+
* 说话 - 核心方法
|
|
331
|
+
* @param text 要说的文本
|
|
332
|
+
* @param options 可选参数
|
|
333
|
+
*/
|
|
334
|
+
async speak(text, options = {}) {
|
|
335
|
+
if (!text || text.trim().length === 0) {
|
|
336
|
+
throw new TTSError(
|
|
337
|
+
"Text cannot be empty",
|
|
338
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
339
|
+
"speaker"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (this.isSpeaking) {
|
|
343
|
+
await this.stop();
|
|
344
|
+
}
|
|
345
|
+
this.isSpeaking = true;
|
|
346
|
+
this.currentText = text;
|
|
347
|
+
try {
|
|
348
|
+
const providerName = options.provider || this.options.defaultProvider || "minimax";
|
|
349
|
+
if (!hasProvider(providerName)) {
|
|
350
|
+
throw new TTSError(
|
|
351
|
+
`Provider "${providerName}" not found`,
|
|
352
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
353
|
+
"speaker"
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
this.currentProvider = getProvider(providerName);
|
|
357
|
+
const result = await this.currentProvider.speak(text, {
|
|
358
|
+
model: options.model || this.options.defaultModel || "stream",
|
|
359
|
+
voice: options.voice || this.options.defaultVoice,
|
|
360
|
+
speed: options.speed,
|
|
361
|
+
volume: options.volume,
|
|
362
|
+
pitch: options.pitch,
|
|
363
|
+
sourceVoice: options.sourceVoice
|
|
364
|
+
});
|
|
365
|
+
if (this.player) {
|
|
366
|
+
await this.player.play(result.audioData, result.format);
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
this.isSpeaking = false;
|
|
370
|
+
if (error instanceof TTSError) {
|
|
371
|
+
this.emit("error", error);
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
const ttsError = new TTSError(
|
|
375
|
+
"Speak failed",
|
|
376
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
377
|
+
"speaker",
|
|
378
|
+
error
|
|
379
|
+
);
|
|
380
|
+
this.emit("error", ttsError);
|
|
381
|
+
throw ttsError;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 暂停播放
|
|
386
|
+
*/
|
|
387
|
+
pause() {
|
|
388
|
+
if (this.player && this.isSpeaking && !this.isPaused) {
|
|
389
|
+
this.player.pause();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 恢复播放
|
|
394
|
+
*/
|
|
395
|
+
resume() {
|
|
396
|
+
if (this.player && this.isPaused) {
|
|
397
|
+
this.player.resume();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 停止播放
|
|
402
|
+
*/
|
|
403
|
+
async stop() {
|
|
404
|
+
this.isSpeaking = false;
|
|
405
|
+
this.isPaused = false;
|
|
406
|
+
if (this.player) {
|
|
407
|
+
await this.player.stop();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 销毁 Speaker,释放资源
|
|
412
|
+
*/
|
|
413
|
+
async destroy() {
|
|
414
|
+
this.isSpeaking = false;
|
|
415
|
+
this.isPaused = false;
|
|
416
|
+
if (this.player) {
|
|
417
|
+
await this.player.stop();
|
|
418
|
+
this.player = void 0;
|
|
419
|
+
}
|
|
420
|
+
this.currentProvider = void 0;
|
|
421
|
+
this.currentText = void 0;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* 列出可用音色
|
|
425
|
+
*/
|
|
426
|
+
async listVoices(providerName) {
|
|
427
|
+
const name = providerName || this.options.defaultProvider || "minimax";
|
|
428
|
+
const provider = getProvider(name);
|
|
429
|
+
return provider.listVoices();
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* 获取 Provider 能力
|
|
433
|
+
*/
|
|
434
|
+
getCapabilities(providerName) {
|
|
435
|
+
const name = providerName || this.options.defaultProvider || "minimax";
|
|
436
|
+
const provider = getProvider(name);
|
|
437
|
+
return provider.getCapabilities();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* 获取所有已注册的 Provider
|
|
441
|
+
*/
|
|
442
|
+
getProviders() {
|
|
443
|
+
return listProviders();
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 是否正在播放
|
|
447
|
+
*/
|
|
448
|
+
isPlaying() {
|
|
449
|
+
return this.isSpeaking && !this.isPaused;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 是否暂停
|
|
453
|
+
*/
|
|
454
|
+
isPausedState() {
|
|
455
|
+
return this.isPaused;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
var defaultSpeaker;
|
|
459
|
+
function getDefaultSpeaker() {
|
|
460
|
+
if (!defaultSpeaker) {
|
|
461
|
+
defaultSpeaker = new Speaker();
|
|
462
|
+
}
|
|
463
|
+
return defaultSpeaker;
|
|
464
|
+
}
|
|
465
|
+
async function speak(text, options) {
|
|
466
|
+
const speaker2 = getDefaultSpeaker();
|
|
467
|
+
return speaker2.speak(text, options);
|
|
468
|
+
}
|
|
469
|
+
async function stop() {
|
|
470
|
+
const speaker2 = getDefaultSpeaker();
|
|
471
|
+
return speaker2.stop();
|
|
472
|
+
}
|
|
473
|
+
function pause() {
|
|
474
|
+
const speaker2 = getDefaultSpeaker();
|
|
475
|
+
speaker2.pause();
|
|
476
|
+
}
|
|
477
|
+
function resume() {
|
|
478
|
+
const speaker2 = getDefaultSpeaker();
|
|
479
|
+
speaker2.resume();
|
|
480
|
+
}
|
|
481
|
+
async function listVoices(providerName) {
|
|
482
|
+
const speaker2 = getDefaultSpeaker();
|
|
483
|
+
return speaker2.listVoices(providerName);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/tools/tts.ts
|
|
487
|
+
async function handleToolCall(toolName, args) {
|
|
488
|
+
try {
|
|
489
|
+
switch (toolName) {
|
|
490
|
+
case "tts_speak":
|
|
491
|
+
await speak(args?.text, {
|
|
492
|
+
provider: args?.provider,
|
|
493
|
+
voice: args?.voice,
|
|
494
|
+
model: args?.model,
|
|
495
|
+
speed: args?.speed,
|
|
496
|
+
volume: args?.volume,
|
|
497
|
+
pitch: args?.pitch
|
|
498
|
+
});
|
|
499
|
+
return { success: true, message: "Speech completed" };
|
|
500
|
+
case "tts_stop":
|
|
501
|
+
await stop();
|
|
502
|
+
return { success: true, message: "Stopped" };
|
|
503
|
+
case "tts_pause":
|
|
504
|
+
pause();
|
|
505
|
+
return { success: true, message: "Paused" };
|
|
506
|
+
case "tts_resume":
|
|
507
|
+
resume();
|
|
508
|
+
return { success: true, message: "Resumed" };
|
|
509
|
+
case "tts_list_voices":
|
|
510
|
+
const voices = await listVoices(args?.provider);
|
|
511
|
+
return { success: true, voices };
|
|
512
|
+
case "tts_list_providers":
|
|
513
|
+
const speaker2 = getDefaultSpeaker();
|
|
514
|
+
const providers2 = speaker2.getProviders();
|
|
515
|
+
return { success: true, providers: providers2 };
|
|
516
|
+
case "tts_status":
|
|
517
|
+
const s = getDefaultSpeaker();
|
|
518
|
+
return {
|
|
519
|
+
success: true,
|
|
520
|
+
isPlaying: s.isPlaying(),
|
|
521
|
+
isPaused: s.isPausedState()
|
|
522
|
+
};
|
|
523
|
+
case "tts_stream_speak":
|
|
524
|
+
if (!isAutoReadEnabled()) {
|
|
525
|
+
throw new TTSError(
|
|
526
|
+
"Stream mode is not enabled. autoRead must be enabled in configuration to use tts_stream_speak.",
|
|
527
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
528
|
+
"tts_stream"
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (!isStreamEnabled()) {
|
|
532
|
+
throw new TTSError(
|
|
533
|
+
"Stream components not initialized. Please initialize with autoRead enabled.",
|
|
534
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
535
|
+
"tts_stream"
|
|
536
|
+
);
|
|
78
537
|
}
|
|
79
|
-
|
|
538
|
+
const streamReader2 = getStreamReader();
|
|
539
|
+
const synthesizer = getStreamingSynthesizer();
|
|
540
|
+
if (streamReader2 && synthesizer) {
|
|
541
|
+
streamReader2.start();
|
|
542
|
+
if (args?.text) {
|
|
543
|
+
synthesizer.synthesize(args.text);
|
|
544
|
+
}
|
|
545
|
+
return { success: true, message: "Stream speak started" };
|
|
546
|
+
}
|
|
547
|
+
throw new TTSError(
|
|
548
|
+
"Stream components not available",
|
|
549
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
550
|
+
"tts_stream"
|
|
551
|
+
);
|
|
552
|
+
case "tts_stream_stop":
|
|
553
|
+
if (!isStreamEnabled()) {
|
|
554
|
+
throw new TTSError(
|
|
555
|
+
"Stream mode is not enabled. Please enable autoRead in configuration.",
|
|
556
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
557
|
+
"tts_stream"
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const player = getStreamPlayer();
|
|
561
|
+
if (player) {
|
|
562
|
+
player.stop();
|
|
563
|
+
return { success: true, message: "Stream stopped" };
|
|
564
|
+
}
|
|
565
|
+
throw new TTSError(
|
|
566
|
+
"Stream player not available",
|
|
567
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
568
|
+
"tts_stream"
|
|
569
|
+
);
|
|
570
|
+
case "tts_stream_status":
|
|
571
|
+
if (!isStreamEnabled()) {
|
|
572
|
+
return {
|
|
573
|
+
success: true,
|
|
574
|
+
isActive: false,
|
|
575
|
+
bytesWritten: 0,
|
|
576
|
+
state: "not_initialized"
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
success: true,
|
|
581
|
+
...getStreamStatus()
|
|
582
|
+
};
|
|
583
|
+
default:
|
|
584
|
+
throw new TTSError(
|
|
585
|
+
`Unknown tool: ${toolName}`,
|
|
586
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
587
|
+
"tools"
|
|
588
|
+
);
|
|
80
589
|
}
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (error instanceof TTSError) {
|
|
592
|
+
return {
|
|
593
|
+
success: false,
|
|
594
|
+
error: error.message,
|
|
595
|
+
code: error.code,
|
|
596
|
+
provider: error.provider
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
success: false,
|
|
601
|
+
error: String(error)
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/providers/minimax.ts
|
|
607
|
+
import axios from "axios";
|
|
608
|
+
var MiniMaxProvider = class extends BaseTTSProvider {
|
|
609
|
+
name = "minimax";
|
|
610
|
+
capabilities = {
|
|
611
|
+
speak: true,
|
|
612
|
+
stream: true,
|
|
613
|
+
sync: true,
|
|
614
|
+
async: true,
|
|
615
|
+
voiceList: true,
|
|
616
|
+
voiceClone: true
|
|
617
|
+
};
|
|
618
|
+
config;
|
|
619
|
+
httpClient;
|
|
620
|
+
wsConnection;
|
|
621
|
+
currentAudioData = [];
|
|
622
|
+
audioFormat = "mp3";
|
|
623
|
+
constructor(config) {
|
|
624
|
+
super();
|
|
625
|
+
this.config = config;
|
|
626
|
+
this.apiKey = config.apiKey;
|
|
627
|
+
this.defaultVoice = config.voiceId;
|
|
628
|
+
this.defaultModel = config.model || "stream";
|
|
629
|
+
this.audioFormat = config.audioFormat || "mp3";
|
|
630
|
+
this.httpClient = axios.create({
|
|
631
|
+
baseURL: this.config.baseURL || "https://api.minimaxi.com",
|
|
632
|
+
headers: {
|
|
633
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
634
|
+
"Content-Type": "application/json"
|
|
635
|
+
},
|
|
636
|
+
timeout: 3e4
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
async initialize() {
|
|
640
|
+
this.validateApiKey();
|
|
641
|
+
}
|
|
642
|
+
async destroy() {
|
|
643
|
+
if (this.wsConnection) {
|
|
644
|
+
this.wsConnection.close();
|
|
645
|
+
this.wsConnection = void 0;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async doSpeak(text, voice, model, options) {
|
|
649
|
+
this.validateApiKey();
|
|
650
|
+
switch (model) {
|
|
651
|
+
case "stream":
|
|
652
|
+
return this.streamingSpeak(text, voice, options);
|
|
653
|
+
case "sync":
|
|
654
|
+
return this.syncSpeak(text, voice, options);
|
|
655
|
+
case "async":
|
|
656
|
+
return this.asyncSpeak(text, voice, options);
|
|
657
|
+
default:
|
|
658
|
+
return this.streamingSpeak(text, voice, options);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* 流式合成 (HTTP) - T2A v2 with stream: true
|
|
663
|
+
*/
|
|
664
|
+
async streamingSpeak(text, voice, options) {
|
|
665
|
+
try {
|
|
666
|
+
const voiceId = voice || this.defaultVoice || "male-qn-qingse";
|
|
667
|
+
const speed = options?.speed || this.config.speed || 1;
|
|
668
|
+
const vol = options?.volume !== void 0 ? options.volume / 10 : this.config.volume !== void 0 ? this.config.volume / 10 : 1;
|
|
669
|
+
const pitch = options?.pitch !== void 0 ? Math.round((options.pitch - 1) * 12) : this.config.pitch !== void 0 ? Math.round((this.config.pitch - 1) * 12) : 0;
|
|
670
|
+
const response = await this.httpClient.post("/v1/t2a_v2", {
|
|
671
|
+
model: this.config.ttsModel || "speech-2.8-hd",
|
|
672
|
+
text,
|
|
673
|
+
stream: true,
|
|
674
|
+
voice_setting: {
|
|
675
|
+
voice_id: voiceId,
|
|
676
|
+
speed,
|
|
677
|
+
vol,
|
|
678
|
+
pitch
|
|
679
|
+
},
|
|
680
|
+
audio_setting: {
|
|
681
|
+
sample_rate: 32e3,
|
|
682
|
+
bitrate: 128e3,
|
|
683
|
+
format: this.audioFormat,
|
|
684
|
+
channel: 1
|
|
89
685
|
}
|
|
90
|
-
|
|
686
|
+
}, {
|
|
687
|
+
headers: {
|
|
688
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
689
|
+
"Content-Type": "application/json"
|
|
690
|
+
},
|
|
691
|
+
responseType: "stream"
|
|
692
|
+
});
|
|
693
|
+
const stream = response.data;
|
|
694
|
+
const audioChunks = [];
|
|
695
|
+
return new Promise((resolve, reject) => {
|
|
696
|
+
stream.on("data", (chunk) => {
|
|
697
|
+
try {
|
|
698
|
+
const lines = chunk.toString().split("\n");
|
|
699
|
+
for (const line of lines) {
|
|
700
|
+
if (line.startsWith("data:")) {
|
|
701
|
+
const data = JSON.parse(line.slice(5));
|
|
702
|
+
if (data.data?.audio) {
|
|
703
|
+
audioChunks.push(Buffer.from(data.data.audio, "hex"));
|
|
704
|
+
}
|
|
705
|
+
if (data.data?.status === 2) {
|
|
706
|
+
const fullAudio = Buffer.concat(audioChunks);
|
|
707
|
+
resolve({
|
|
708
|
+
audioData: fullAudio,
|
|
709
|
+
format: this.audioFormat,
|
|
710
|
+
isStream: true,
|
|
711
|
+
duration: this.estimateDuration(fullAudio.length)
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} catch (e) {
|
|
717
|
+
audioChunks.push(chunk);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
stream.on("error", (err) => {
|
|
721
|
+
reject(new TTSError(
|
|
722
|
+
"Stream error",
|
|
723
|
+
"NETWORK" /* NETWORK */,
|
|
724
|
+
this.name,
|
|
725
|
+
err
|
|
726
|
+
));
|
|
727
|
+
});
|
|
728
|
+
stream.on("end", () => {
|
|
729
|
+
if (audioChunks.length > 0) {
|
|
730
|
+
const fullAudio = Buffer.concat(audioChunks);
|
|
731
|
+
resolve({
|
|
732
|
+
audioData: fullAudio,
|
|
733
|
+
format: this.audioFormat,
|
|
734
|
+
isStream: true,
|
|
735
|
+
duration: this.estimateDuration(fullAudio.length)
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (error instanceof TTSError) throw error;
|
|
742
|
+
throw this.mapError(error);
|
|
91
743
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* 同步合成 (HTTP) - T2A v2
|
|
747
|
+
* API: POST https://api.minimax.io/v1/t2a_v2
|
|
748
|
+
*/
|
|
749
|
+
async syncSpeak(text, voice, options) {
|
|
750
|
+
try {
|
|
751
|
+
const voiceId = voice || this.defaultVoice || "male-qn-qingse";
|
|
752
|
+
const speed = options?.speed || this.config.speed || 1;
|
|
753
|
+
const vol = options?.volume !== void 0 ? options.volume / 10 : this.config.volume !== void 0 ? this.config.volume / 10 : 1;
|
|
754
|
+
const pitch = options?.pitch !== void 0 ? Math.round((options.pitch - 1) * 12) : this.config.pitch !== void 0 ? Math.round((this.config.pitch - 1) * 12) : 0;
|
|
755
|
+
const response = await this.httpClient.post("/v1/t2a_v2", {
|
|
756
|
+
model: this.config.ttsModel || "speech-2.8-hd",
|
|
757
|
+
text,
|
|
758
|
+
stream: false,
|
|
759
|
+
output_format: "hex",
|
|
760
|
+
voice_setting: {
|
|
761
|
+
voice_id: voiceId,
|
|
762
|
+
speed,
|
|
763
|
+
vol,
|
|
764
|
+
pitch
|
|
765
|
+
},
|
|
766
|
+
audio_setting: {
|
|
767
|
+
sample_rate: 32e3,
|
|
768
|
+
bitrate: 128e3,
|
|
769
|
+
format: this.audioFormat,
|
|
770
|
+
channel: 1
|
|
104
771
|
}
|
|
105
|
-
|
|
772
|
+
}, {
|
|
773
|
+
headers: {
|
|
774
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
775
|
+
"Content-Type": "application/json"
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
if (response.data.base_resp?.status_code !== 0) {
|
|
779
|
+
throw new TTSError(
|
|
780
|
+
response.data.base_resp?.status_msg || "API request failed",
|
|
781
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
782
|
+
this.name,
|
|
783
|
+
response.data.base_resp
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
const audioHex = response.data.data?.audio;
|
|
787
|
+
if (!audioHex) {
|
|
788
|
+
throw new TTSError(
|
|
789
|
+
"No audio data in response",
|
|
790
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
791
|
+
this.name
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const audioBuffer = Buffer.from(audioHex, "hex");
|
|
795
|
+
return {
|
|
796
|
+
audioData: audioBuffer,
|
|
797
|
+
format: this.audioFormat,
|
|
798
|
+
isStream: false,
|
|
799
|
+
duration: response.data.extra_info?.audio_length ? response.data.extra_info.audio_length / 1e3 : this.estimateDuration(audioBuffer.length)
|
|
800
|
+
};
|
|
801
|
+
} catch (error) {
|
|
802
|
+
if (error instanceof TTSError) throw error;
|
|
803
|
+
throw this.mapError(error);
|
|
106
804
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* 异步合成 (轮询) - T2A Async v2
|
|
808
|
+
*/
|
|
809
|
+
async asyncSpeak(text, voice, options) {
|
|
810
|
+
try {
|
|
811
|
+
const voiceId = voice || this.defaultVoice || "male-qn-qingse";
|
|
812
|
+
const speed = options?.speed || this.config.speed || 1;
|
|
813
|
+
const vol = options?.volume !== void 0 ? options.volume / 10 : this.config.volume !== void 0 ? this.config.volume / 10 : 1;
|
|
814
|
+
const pitch = options?.pitch !== void 0 ? Math.round((options.pitch - 1) * 12) : this.config.pitch !== void 0 ? Math.round((this.config.pitch - 1) * 12) : 0;
|
|
815
|
+
const createResponse = await this.httpClient.post("/v1/t2a_async_v2", {
|
|
816
|
+
model: this.config.ttsModel || "speech-2.8-hd",
|
|
817
|
+
text,
|
|
818
|
+
voice_setting: {
|
|
819
|
+
voice_id: voiceId,
|
|
820
|
+
speed,
|
|
821
|
+
vol,
|
|
822
|
+
pitch
|
|
823
|
+
},
|
|
824
|
+
audio_setting: {
|
|
825
|
+
sample_rate: 32e3,
|
|
826
|
+
bitrate: 128e3,
|
|
827
|
+
format: this.audioFormat,
|
|
828
|
+
channel: 1
|
|
115
829
|
}
|
|
116
|
-
|
|
830
|
+
}, {
|
|
831
|
+
headers: {
|
|
832
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
833
|
+
"Content-Type": "application/json"
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
const taskId = createResponse.data.task_id;
|
|
837
|
+
if (!taskId) {
|
|
838
|
+
throw new TTSError(
|
|
839
|
+
"No task_id in async response",
|
|
840
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
841
|
+
this.name,
|
|
842
|
+
createResponse.data
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
let attempts = 0;
|
|
846
|
+
const maxAttempts = 60;
|
|
847
|
+
while (attempts < maxAttempts) {
|
|
848
|
+
await this.delay(2e3);
|
|
849
|
+
const statusResponse = await this.httpClient.get(
|
|
850
|
+
`/v1/query/t2a_async_query_v2?task_id=${taskId}`,
|
|
851
|
+
{
|
|
852
|
+
headers: {
|
|
853
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
if (statusResponse.data.status === "success") {
|
|
858
|
+
const fileId = statusResponse.data.file_id;
|
|
859
|
+
if (!fileId) {
|
|
860
|
+
throw new TTSError(
|
|
861
|
+
"No file_id in async response",
|
|
862
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
863
|
+
this.name,
|
|
864
|
+
statusResponse.data
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
const downloadResponse = await this.httpClient.get(
|
|
868
|
+
`/v1/files/retrieve_content?file_id=${fileId}`,
|
|
869
|
+
{
|
|
870
|
+
headers: {
|
|
871
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
872
|
+
},
|
|
873
|
+
responseType: "arraybuffer"
|
|
874
|
+
}
|
|
875
|
+
);
|
|
876
|
+
return {
|
|
877
|
+
audioData: Buffer.from(downloadResponse.data),
|
|
878
|
+
format: this.audioFormat,
|
|
879
|
+
isStream: false,
|
|
880
|
+
duration: 0
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
if (statusResponse.data.status === "failed") {
|
|
884
|
+
throw new TTSError(
|
|
885
|
+
"Async TTS task failed",
|
|
886
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
887
|
+
this.name,
|
|
888
|
+
statusResponse.data
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
attempts++;
|
|
892
|
+
}
|
|
893
|
+
throw new TTSError(
|
|
894
|
+
"Async TTS task timeout",
|
|
895
|
+
"NETWORK" /* NETWORK */,
|
|
896
|
+
this.name
|
|
897
|
+
);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (error instanceof TTSError) throw error;
|
|
900
|
+
throw this.mapError(error);
|
|
117
901
|
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* 音色克隆 - 使用参考音频克隆声音
|
|
905
|
+
*/
|
|
906
|
+
async voiceClone(audioUrl, text, voice) {
|
|
907
|
+
this.validateApiKey();
|
|
908
|
+
try {
|
|
909
|
+
const response = await this.httpClient.post("/v1/t2a_v2/voice_clone", {
|
|
910
|
+
audio_url: audioUrl,
|
|
911
|
+
text,
|
|
912
|
+
voice_id: voice || "custom_clone"
|
|
913
|
+
}, {
|
|
914
|
+
headers: {
|
|
915
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
916
|
+
"Content-Type": "application/json"
|
|
917
|
+
},
|
|
918
|
+
responseType: "arraybuffer"
|
|
919
|
+
});
|
|
920
|
+
return {
|
|
921
|
+
audioData: Buffer.from(response.data),
|
|
922
|
+
format: this.audioFormat,
|
|
923
|
+
isStream: false,
|
|
924
|
+
duration: this.estimateDuration(response.data.length)
|
|
925
|
+
};
|
|
926
|
+
} catch (error) {
|
|
927
|
+
throw this.mapError(error);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* 获取音色列表
|
|
932
|
+
*/
|
|
933
|
+
async listVoices() {
|
|
934
|
+
this.validateApiKey();
|
|
935
|
+
try {
|
|
936
|
+
const response = await this.httpClient.get("/v1/t2a/voices", {
|
|
937
|
+
headers: {
|
|
938
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
126
939
|
}
|
|
127
|
-
|
|
940
|
+
});
|
|
941
|
+
return response.data.voices.map((v) => ({
|
|
942
|
+
id: v.voice_id,
|
|
943
|
+
name: v.name,
|
|
944
|
+
language: v.language,
|
|
945
|
+
gender: v.gender
|
|
946
|
+
}));
|
|
947
|
+
} catch (error) {
|
|
948
|
+
return MINIMAX_VOICES;
|
|
128
949
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* 错误映射
|
|
953
|
+
*/
|
|
954
|
+
mapError(error) {
|
|
955
|
+
if (error.response) {
|
|
956
|
+
const status = error.response.status;
|
|
957
|
+
const code = status === 401 ? "AUTH" /* AUTH */ : status === 429 ? "QUOTA" /* QUOTA */ : status >= 500 ? "NETWORK" /* NETWORK */ : "UNKNOWN" /* UNKNOWN */;
|
|
958
|
+
return new TTSError(
|
|
959
|
+
error.response.data?.message || "API request failed",
|
|
960
|
+
code,
|
|
961
|
+
this.name,
|
|
962
|
+
error.response.data
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
966
|
+
return new TTSError(
|
|
967
|
+
"Network error: Unable to connect to MiniMax API",
|
|
968
|
+
"NETWORK" /* NETWORK */,
|
|
969
|
+
this.name,
|
|
970
|
+
error.message
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return new TTSError(
|
|
974
|
+
error.message || "Unknown error",
|
|
975
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
976
|
+
this.name,
|
|
977
|
+
error
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* 延迟辅助函数
|
|
982
|
+
*/
|
|
983
|
+
delay(ms) {
|
|
984
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* 估算音频时长
|
|
988
|
+
* 基于 32kbps MP3 估算
|
|
989
|
+
*/
|
|
990
|
+
estimateDuration(bytes) {
|
|
991
|
+
return bytes * 8 / (32e3 * 60);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
var MINIMAX_VOICES = [
|
|
995
|
+
{ id: "male-qn-qingse", name: "\u9752\u5E74\u6E05\u6F88", language: "zh-CN", gender: "male" },
|
|
996
|
+
{ id: "male-qn-qingse_2", name: "\u9752\u5E74\u6E05\u6F88v2", language: "zh-CN", gender: "male" },
|
|
997
|
+
{ id: "female-shaonv", name: "\u5C11\u5973", language: "zh-CN", gender: "female" },
|
|
998
|
+
{ id: "male-baiming", name: "\u6210\u719F\u7537\u58F0", language: "zh-CN", gender: "male" },
|
|
999
|
+
{ id: "female-tianmei", name: "\u751C\u7F8E\u5973\u58F0", language: "zh-CN", gender: "female" },
|
|
1000
|
+
{ id: "male-zhongnan", name: "\u4E2D\u5E74\u7537\u58F0", language: "zh-CN", gender: "male" },
|
|
1001
|
+
{ id: "female-yujie", name: "\u5FA1\u59D0\u97F3", language: "zh-CN", gender: "female" },
|
|
1002
|
+
{ id: "male-qn-xiaoao", name: "\u9752\u5E74\u8C6A\u723D", language: "zh-CN", gender: "male" },
|
|
1003
|
+
{ id: "female-shandian", name: "\u751C\u5FC3\u5C0F\u5A18", language: "zh-CN", gender: "female" },
|
|
1004
|
+
{ id: "male-qn-buke", name: "\u9752\u5E74\u4F4E\u6C89", language: "zh-CN", gender: "male" },
|
|
1005
|
+
{ id: "male-qn-wenlv", name: "\u6587\u7EFF\u9752\u5E74", language: "zh-CN", gender: "male" },
|
|
1006
|
+
{ id: "female-tianmei-2", name: "\u751C\u7F8E\u5973\u58F0v2", language: "zh-CN", gender: "female" },
|
|
1007
|
+
{ id: "female-yujie-2", name: "\u5FA1\u59D0\u97F3v2", language: "zh-CN", gender: "female" },
|
|
1008
|
+
{ id: "male-shaonian", name: "\u5C11\u5E74\u97F3", language: "zh-CN", gender: "male" },
|
|
1009
|
+
{ id: "female-yunv", name: "\u6E29\u67D4\u5973\u58F0", language: "zh-CN", gender: "female" },
|
|
1010
|
+
{ id: "male-qn-jingdian", name: "\u7ECF\u5178\u7537\u58F0", language: "zh-CN", gender: "male" },
|
|
1011
|
+
{ id: "male-qn-kuang\u91CE", name: "\u72C2\u91CE\u9752\u5E74", language: "zh-CN", gender: "male" },
|
|
1012
|
+
{ id: "female-yujie-old", name: "\u4F18\u96C5\u4F4E\u6C89", language: "zh-CN", gender: "female" },
|
|
1013
|
+
{ id: "female-tianmei-old", name: "\u751C\u7F8E\u5973\u5B69", language: "zh-CN", gender: "female" },
|
|
1014
|
+
{ id: "male-qn-taohua", name: "\u6843\u82B1\u9752\u5E74", language: "zh-CN", gender: "male" }
|
|
1015
|
+
];
|
|
1016
|
+
|
|
1017
|
+
// src/core/stream-reader.ts
|
|
1018
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
1019
|
+
var StreamReader = class extends EventEmitter3 {
|
|
1020
|
+
constructor(bufferSize = 30, bufferTimeout = 2e3) {
|
|
1021
|
+
super();
|
|
1022
|
+
this.bufferSize = bufferSize;
|
|
1023
|
+
this.bufferTimeout = bufferTimeout;
|
|
1024
|
+
}
|
|
1025
|
+
bufferSize;
|
|
1026
|
+
bufferTimeout;
|
|
1027
|
+
state = "idle" /* IDLE */;
|
|
1028
|
+
buffer = "";
|
|
1029
|
+
sessionID;
|
|
1030
|
+
messageID;
|
|
1031
|
+
partID;
|
|
1032
|
+
timeoutHandle;
|
|
1033
|
+
/**
|
|
1034
|
+
* 启动流式监听
|
|
1035
|
+
* 将状态从 IDLE 切换到 BUFFERING,开始监听事件
|
|
1036
|
+
*/
|
|
1037
|
+
start() {
|
|
1038
|
+
if (this.state === "idle" /* IDLE */) {
|
|
1039
|
+
this.state = "buffering" /* BUFFERING */;
|
|
1040
|
+
this.emit("streamStart");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* 处理 message.part.delta 事件
|
|
1045
|
+
*/
|
|
1046
|
+
handleDelta(sessionID, messageID, partID, delta) {
|
|
1047
|
+
if (this.state === "idle" /* IDLE */) {
|
|
1048
|
+
this.state = "buffering" /* BUFFERING */;
|
|
1049
|
+
this.sessionID = sessionID;
|
|
1050
|
+
this.messageID = messageID;
|
|
1051
|
+
this.partID = partID;
|
|
1052
|
+
this.emit("streamStart");
|
|
1053
|
+
}
|
|
1054
|
+
this.buffer += delta;
|
|
1055
|
+
this.resetTimeout();
|
|
1056
|
+
if (this.shouldFlush()) {
|
|
1057
|
+
this.flushBuffer();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* 处理流结束
|
|
1062
|
+
*/
|
|
1063
|
+
handleEnd() {
|
|
1064
|
+
if (this.state === "ended" /* ENDED */) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (this.buffer.length > 0) {
|
|
1068
|
+
this.flushBuffer();
|
|
1069
|
+
}
|
|
1070
|
+
this.state = "ended" /* ENDED */;
|
|
1071
|
+
this.clearTimeout();
|
|
1072
|
+
this.emit("streamEnd");
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* 处理错误
|
|
1076
|
+
*/
|
|
1077
|
+
handleError(error) {
|
|
1078
|
+
this.clearTimeout();
|
|
1079
|
+
this.state = "idle" /* IDLE */;
|
|
1080
|
+
this.buffer = "";
|
|
1081
|
+
this.emit("streamError", error);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* 重置缓冲器
|
|
1085
|
+
*/
|
|
1086
|
+
reset() {
|
|
1087
|
+
this.state = "idle" /* IDLE */;
|
|
1088
|
+
this.buffer = "";
|
|
1089
|
+
this.sessionID = void 0;
|
|
1090
|
+
this.messageID = void 0;
|
|
1091
|
+
this.partID = void 0;
|
|
1092
|
+
this.clearTimeout();
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* 判断是否应该刷新缓冲区
|
|
1096
|
+
* 条件:
|
|
1097
|
+
* 1. 包含句子结束符(任何长度)
|
|
1098
|
+
* 2. 缓冲区长度 >= bufferSize
|
|
1099
|
+
*/
|
|
1100
|
+
shouldFlush() {
|
|
1101
|
+
const sentenceEnd = /[。!?.!?]|……/;
|
|
1102
|
+
if (sentenceEnd.test(this.buffer)) {
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
if (this.buffer.length >= this.bufferSize) {
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* 刷新缓冲区,发送textReady事件
|
|
1112
|
+
*/
|
|
1113
|
+
flushBuffer() {
|
|
1114
|
+
const text = this.buffer.trim();
|
|
1115
|
+
if (text.length > 0) {
|
|
1116
|
+
this.emit("textReady", text);
|
|
1117
|
+
}
|
|
1118
|
+
this.buffer = "";
|
|
1119
|
+
this.resetTimeout();
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* 重置超时计时器
|
|
1123
|
+
*/
|
|
1124
|
+
resetTimeout() {
|
|
1125
|
+
this.clearTimeout();
|
|
1126
|
+
this.timeoutHandle = setTimeout(() => {
|
|
1127
|
+
if (this.buffer.length > 0) {
|
|
1128
|
+
this.flushBuffer();
|
|
1129
|
+
}
|
|
1130
|
+
}, this.bufferTimeout);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* 清除超时计时器
|
|
1134
|
+
*/
|
|
1135
|
+
clearTimeout() {
|
|
1136
|
+
if (this.timeoutHandle) {
|
|
1137
|
+
clearTimeout(this.timeoutHandle);
|
|
1138
|
+
this.timeoutHandle = void 0;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* 获取当前状态
|
|
1143
|
+
*/
|
|
1144
|
+
getState() {
|
|
1145
|
+
return this.state;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* 检查流是否处于活跃状态
|
|
1149
|
+
*/
|
|
1150
|
+
isActive() {
|
|
1151
|
+
return this.state === "buffering" /* BUFFERING */;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* 获取当前缓冲区内容
|
|
1155
|
+
*/
|
|
1156
|
+
getBuffer() {
|
|
1157
|
+
return this.buffer;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* 获取当前会话ID
|
|
1161
|
+
*/
|
|
1162
|
+
getSessionID() {
|
|
1163
|
+
return this.sessionID;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* 获取当前消息ID
|
|
1167
|
+
*/
|
|
1168
|
+
getMessageID() {
|
|
1169
|
+
return this.messageID;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* 获取当前分块ID
|
|
1173
|
+
*/
|
|
1174
|
+
getPartID() {
|
|
1175
|
+
return this.partID;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// src/core/streaming-synthesizer.ts
|
|
1180
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
1181
|
+
var StreamingSynthesizer = class extends EventEmitter4 {
|
|
1182
|
+
constructor(options) {
|
|
1183
|
+
super();
|
|
1184
|
+
this.options = options;
|
|
1185
|
+
}
|
|
1186
|
+
options;
|
|
1187
|
+
audioChunks = [];
|
|
1188
|
+
/**
|
|
1189
|
+
* 发送文本片段进行合成
|
|
1190
|
+
* 调用 provider.speak() 并处理返回的音频流
|
|
1191
|
+
*/
|
|
1192
|
+
async synthesize(text) {
|
|
1193
|
+
if (!text || text.trim().length === 0) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
const result = await this.options.provider.speak(text, {
|
|
1198
|
+
model: "stream",
|
|
1199
|
+
voice: this.options.voice,
|
|
1200
|
+
speed: this.options.speed,
|
|
1201
|
+
volume: this.options.volume,
|
|
1202
|
+
pitch: this.options.pitch
|
|
1203
|
+
});
|
|
1204
|
+
await this.processAudioResult(result);
|
|
1205
|
+
this.emit("done");
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
const ttsError = error instanceof TTSError ? error : new TTSError(
|
|
1208
|
+
error instanceof Error ? error.message : "Synthesis failed",
|
|
1209
|
+
"UNKNOWN",
|
|
1210
|
+
this.options.provider.name,
|
|
1211
|
+
error
|
|
1212
|
+
);
|
|
1213
|
+
this.emit("error", ttsError);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* 处理 AudioResult,根据 audioData 类型进行相应处理
|
|
1218
|
+
*/
|
|
1219
|
+
async processAudioResult(result) {
|
|
1220
|
+
if (result.isStream && result.audioData instanceof ReadableStream) {
|
|
1221
|
+
await this.processReadableStream(result.audioData);
|
|
1222
|
+
} else if (Buffer.isBuffer(result.audioData)) {
|
|
1223
|
+
this.emitChunk(result.audioData);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* 处理 ReadableStream,逐chunk emit
|
|
1228
|
+
*/
|
|
1229
|
+
async processReadableStream(stream) {
|
|
1230
|
+
const reader = stream.getReader();
|
|
1231
|
+
try {
|
|
1232
|
+
while (true) {
|
|
1233
|
+
const { done, value } = await reader.read();
|
|
1234
|
+
if (done) {
|
|
1235
|
+
break;
|
|
141
1236
|
}
|
|
1237
|
+
if (value) {
|
|
1238
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
1239
|
+
this.emitChunk(chunk);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
} finally {
|
|
1243
|
+
reader.releaseLock();
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* emit chunk 并累积
|
|
1248
|
+
*/
|
|
1249
|
+
emitChunk(chunk) {
|
|
1250
|
+
this.audioChunks.push(chunk);
|
|
1251
|
+
this.emit("chunk", chunk);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* 重置状态
|
|
1255
|
+
* 清空累积的音频数据
|
|
1256
|
+
*/
|
|
1257
|
+
reset() {
|
|
1258
|
+
this.audioChunks = [];
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* 获取累积的音频数据
|
|
1262
|
+
* 返回所有已接收的 chunk
|
|
1263
|
+
*/
|
|
1264
|
+
getAudioChunks() {
|
|
1265
|
+
return [...this.audioChunks];
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// src/core/stream-player.ts
|
|
1270
|
+
import { EventEmitter as EventEmitter5 } from "events";
|
|
1271
|
+
import { spawn as spawn2 } from "child_process";
|
|
1272
|
+
import fs2 from "fs";
|
|
1273
|
+
import { createWriteStream as createWriteStream2 } from "fs";
|
|
1274
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1275
|
+
import { join as join2 } from "path";
|
|
1276
|
+
var StreamPlayer = class extends EventEmitter5 {
|
|
1277
|
+
tempFile = "";
|
|
1278
|
+
writeStream;
|
|
1279
|
+
playerProcess;
|
|
1280
|
+
_bytesWritten = 0;
|
|
1281
|
+
_started = false;
|
|
1282
|
+
_paused = false;
|
|
1283
|
+
_stopped = false;
|
|
1284
|
+
format = "mp3";
|
|
1285
|
+
events;
|
|
1286
|
+
constructor(options = {}) {
|
|
1287
|
+
super();
|
|
1288
|
+
this.format = options.format || "mp3";
|
|
1289
|
+
this.events = options.events;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 开始播放
|
|
1293
|
+
* 创建临时文件,创建写入流,启动播放器进程
|
|
1294
|
+
*/
|
|
1295
|
+
start() {
|
|
1296
|
+
if (this._started) {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
this.tempFile = join2(tmpdir2(), `ocosay-stream-${Date.now()}.${this.format}`);
|
|
1300
|
+
this.writeStream = createWriteStream2(this.tempFile, { highWaterMark: 64 * 1024 });
|
|
1301
|
+
this.writeStream.on("error", (error) => {
|
|
1302
|
+
this.handleError(error);
|
|
1303
|
+
});
|
|
1304
|
+
this.writeStream.on("finish", () => {
|
|
1305
|
+
});
|
|
1306
|
+
this.startPlayer();
|
|
1307
|
+
this._started = true;
|
|
1308
|
+
this._stopped = false;
|
|
1309
|
+
this.events?.onStart?.();
|
|
1310
|
+
this.emit("start");
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* 启动播放器进程
|
|
1314
|
+
*/
|
|
1315
|
+
startPlayer() {
|
|
1316
|
+
const platform = process.platform;
|
|
1317
|
+
let command;
|
|
1318
|
+
let args;
|
|
1319
|
+
if (platform === "darwin") {
|
|
1320
|
+
command = "afplay";
|
|
1321
|
+
args = [this.tempFile];
|
|
1322
|
+
} else if (platform === "linux") {
|
|
1323
|
+
command = "aplay";
|
|
1324
|
+
args = [this.tempFile];
|
|
1325
|
+
} else {
|
|
1326
|
+
this.handleError(new Error("Windows platform is not supported for stream playback. PlaySync() blocks the Node.js event loop."));
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
this.playerProcess = spawn2(command, args, {
|
|
1330
|
+
stdio: "ignore",
|
|
1331
|
+
detached: false
|
|
1332
|
+
});
|
|
1333
|
+
this.playerProcess.on("exit", (code, signal) => {
|
|
1334
|
+
if (this._stopped) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (signal === "SIGTERM" || signal === "SIGINT") {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (code === 0 || code === null) {
|
|
1341
|
+
this.events?.onEnd?.();
|
|
1342
|
+
this.emit("end");
|
|
1343
|
+
} else {
|
|
1344
|
+
this.handleError(new Error(`Player exited with code ${code}`));
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
this.playerProcess.on("error", (error) => {
|
|
1348
|
+
this.handleError(error);
|
|
142
1349
|
});
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* 写入音频数据块(边收边写)
|
|
1353
|
+
* 如果尚未 start(),会自动调用
|
|
1354
|
+
*/
|
|
1355
|
+
write(chunk) {
|
|
1356
|
+
if (this._stopped) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (!this._started) {
|
|
1360
|
+
this.start();
|
|
1361
|
+
}
|
|
1362
|
+
if (this.writeStream) {
|
|
1363
|
+
const canContinue = this.writeStream.write(chunk);
|
|
1364
|
+
if (!canContinue) {
|
|
1365
|
+
this.writeStream.once("drain", () => {
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
this._bytesWritten += chunk.length;
|
|
1369
|
+
this.events?.onProgress?.(this._bytesWritten);
|
|
1370
|
+
this.emit("progress", this._bytesWritten);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* 结束写入
|
|
1375
|
+
* 关闭写入流,但不杀播放器进程,让它播完
|
|
1376
|
+
*/
|
|
1377
|
+
end() {
|
|
1378
|
+
if (this.writeStream) {
|
|
1379
|
+
this.writeStream.end();
|
|
1380
|
+
this.writeStream = void 0;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* 停止播放
|
|
1385
|
+
* 杀死播放器进程,删除临时文件
|
|
1386
|
+
*/
|
|
1387
|
+
stop() {
|
|
1388
|
+
this._stopped = true;
|
|
1389
|
+
this._started = false;
|
|
1390
|
+
this._paused = false;
|
|
1391
|
+
if (this.playerProcess) {
|
|
1392
|
+
try {
|
|
1393
|
+
this.playerProcess.kill("SIGTERM");
|
|
1394
|
+
} catch (e) {
|
|
1395
|
+
}
|
|
1396
|
+
this.playerProcess = void 0;
|
|
1397
|
+
}
|
|
1398
|
+
if (this.writeStream) {
|
|
1399
|
+
try {
|
|
1400
|
+
this.writeStream.destroy();
|
|
1401
|
+
} catch (e) {
|
|
1402
|
+
}
|
|
1403
|
+
this.writeStream = void 0;
|
|
1404
|
+
}
|
|
1405
|
+
this.deleteTempFile();
|
|
1406
|
+
this.events?.onStop?.();
|
|
1407
|
+
this.emit("stop");
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* 暂停播放
|
|
1411
|
+
* 使用 SIGSTOP 暂停播放器进程
|
|
1412
|
+
*/
|
|
1413
|
+
pause() {
|
|
1414
|
+
if (!this._started || this._paused || this._stopped) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (this.playerProcess) {
|
|
1418
|
+
try {
|
|
1419
|
+
this.playerProcess.kill("SIGSTOP");
|
|
1420
|
+
this._paused = true;
|
|
1421
|
+
this.events?.onPause?.();
|
|
1422
|
+
this.emit("pause");
|
|
1423
|
+
} catch (e) {
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* 恢复播放
|
|
1429
|
+
*/
|
|
1430
|
+
resume() {
|
|
1431
|
+
if (!this._paused || this._stopped) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (this.playerProcess) {
|
|
1435
|
+
try {
|
|
1436
|
+
this.playerProcess.kill("SIGCONT");
|
|
1437
|
+
this._paused = false;
|
|
1438
|
+
this.events?.onResume?.();
|
|
1439
|
+
this.emit("resume");
|
|
1440
|
+
} catch (e) {
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* 是否已启动
|
|
1446
|
+
*/
|
|
1447
|
+
isStarted() {
|
|
1448
|
+
return this._started;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* 是否暂停
|
|
1452
|
+
*/
|
|
1453
|
+
isPaused() {
|
|
1454
|
+
return this._paused;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* 是否已停止
|
|
1458
|
+
*/
|
|
1459
|
+
isStopped() {
|
|
1460
|
+
return this._stopped;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* 获取已写入的字节数
|
|
1464
|
+
*/
|
|
1465
|
+
getBytesWritten() {
|
|
1466
|
+
return this._bytesWritten;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* 获取临时文件路径
|
|
1470
|
+
*/
|
|
1471
|
+
getTempFile() {
|
|
1472
|
+
return this.tempFile;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* 处理错误
|
|
1476
|
+
*/
|
|
1477
|
+
handleError(error) {
|
|
1478
|
+
this.events?.onError?.(error);
|
|
1479
|
+
this.emit("error", error);
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* 删除临时文件
|
|
1483
|
+
*/
|
|
1484
|
+
deleteTempFile() {
|
|
1485
|
+
if (this.tempFile) {
|
|
1486
|
+
try {
|
|
1487
|
+
if (fs2.existsSync(this.tempFile)) {
|
|
1488
|
+
fs2.unlinkSync(this.tempFile);
|
|
1489
|
+
}
|
|
1490
|
+
} catch (e) {
|
|
1491
|
+
}
|
|
1492
|
+
this.tempFile = "";
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// src/index.ts
|
|
1498
|
+
var speaker;
|
|
1499
|
+
var streamReader;
|
|
1500
|
+
var streamingSynthesizer;
|
|
1501
|
+
var streamPlayer;
|
|
1502
|
+
var initialized = false;
|
|
1503
|
+
var autoReadEnabled = false;
|
|
1504
|
+
async function initialize(config) {
|
|
1505
|
+
if (initialized) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
if (config?.providers?.minimax) {
|
|
1509
|
+
if (!config.providers.minimax.apiKey) {
|
|
1510
|
+
throw new Error("[ocosay] API Key is required. Please set minimax.apiKey in ~/.config/opencode/ocosay.jsonc");
|
|
1511
|
+
}
|
|
1512
|
+
const minimaxProvider = new MiniMaxProvider(config.providers.minimax);
|
|
1513
|
+
registerProvider("minimax", minimaxProvider);
|
|
1514
|
+
await minimaxProvider.initialize();
|
|
1515
|
+
}
|
|
1516
|
+
const speakerOptions = {
|
|
1517
|
+
defaultProvider: config?.defaultProvider || "minimax",
|
|
1518
|
+
defaultModel: config?.defaultModel || "stream",
|
|
1519
|
+
defaultVoice: config?.defaultVoice
|
|
1520
|
+
};
|
|
1521
|
+
speaker = new Speaker(speakerOptions);
|
|
1522
|
+
if (config?.autoRead) {
|
|
1523
|
+
autoReadEnabled = true;
|
|
1524
|
+
initializeStreamComponents(config);
|
|
1525
|
+
}
|
|
1526
|
+
initialized = true;
|
|
1527
|
+
}
|
|
1528
|
+
function initializeStreamComponents(config) {
|
|
1529
|
+
const provider = getProvider(config?.defaultProvider || "minimax");
|
|
1530
|
+
streamReader = new StreamReader(
|
|
1531
|
+
config?.streamBufferSize || 30,
|
|
1532
|
+
config?.streamBufferTimeout || 2e3
|
|
1533
|
+
);
|
|
1534
|
+
streamingSynthesizer = new StreamingSynthesizer({
|
|
1535
|
+
provider,
|
|
1536
|
+
voice: config?.defaultVoice,
|
|
1537
|
+
speed: 1,
|
|
1538
|
+
volume: 1,
|
|
1539
|
+
pitch: 1
|
|
1540
|
+
});
|
|
1541
|
+
const playerEvents = {
|
|
1542
|
+
onStart: () => {
|
|
1543
|
+
},
|
|
1544
|
+
onEnd: () => {
|
|
1545
|
+
},
|
|
1546
|
+
onProgress: (bytesWritten) => {
|
|
1547
|
+
},
|
|
1548
|
+
onError: (error) => console.error("Stream player error:", error),
|
|
1549
|
+
onStop: () => {
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
streamPlayer = new StreamPlayer({ events: playerEvents });
|
|
1553
|
+
const synthesisQueue = [];
|
|
1554
|
+
let isSynthesizing = false;
|
|
1555
|
+
async function processQueue() {
|
|
1556
|
+
while (synthesisQueue.length > 0) {
|
|
1557
|
+
const text = synthesisQueue.shift();
|
|
1558
|
+
isSynthesizing = true;
|
|
1559
|
+
try {
|
|
1560
|
+
await streamingSynthesizer?.synthesize(text);
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
console.error("Synthesize error:", error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
isSynthesizing = false;
|
|
1566
|
+
}
|
|
1567
|
+
streamReader.on("textReady", (text) => {
|
|
1568
|
+
synthesisQueue.push(text);
|
|
1569
|
+
if (!isSynthesizing) {
|
|
1570
|
+
processQueue();
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
streamingSynthesizer.on("chunk", (chunk) => {
|
|
1574
|
+
if (streamPlayer) {
|
|
1575
|
+
streamPlayer.write(chunk);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
streamingSynthesizer.on("done", () => {
|
|
1579
|
+
if (streamPlayer) {
|
|
1580
|
+
streamPlayer.end();
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
const TuiEventBus = global.__opencode_tuieventbus__;
|
|
1584
|
+
if (TuiEventBus) {
|
|
1585
|
+
const eventBus = new TuiEventBus();
|
|
1586
|
+
let messagePartDeltaHandler;
|
|
1587
|
+
let messagePartEndHandler;
|
|
1588
|
+
messagePartDeltaHandler = (event) => {
|
|
1589
|
+
if (event?.properties) {
|
|
1590
|
+
streamReader?.handleDelta(
|
|
1591
|
+
event.sessionId || "",
|
|
1592
|
+
event.messageId || "",
|
|
1593
|
+
event.partId || "",
|
|
1594
|
+
event.properties.delta || ""
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
messagePartEndHandler = () => {
|
|
1599
|
+
streamReader?.handleEnd();
|
|
1600
|
+
};
|
|
1601
|
+
eventBus.on("message.part.delta", messagePartDeltaHandler);
|
|
1602
|
+
eventBus.on("message.part.end", messagePartEndHandler);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function isStreamEnabled() {
|
|
1606
|
+
return streamReader !== void 0 && streamingSynthesizer !== void 0 && streamPlayer !== void 0;
|
|
1607
|
+
}
|
|
1608
|
+
function isAutoReadEnabled() {
|
|
1609
|
+
return autoReadEnabled;
|
|
1610
|
+
}
|
|
1611
|
+
function getStreamStatus() {
|
|
1612
|
+
if (!streamReader) {
|
|
1613
|
+
return { isActive: false, bytesWritten: 0, state: "not_initialized" };
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
isActive: streamReader.isActive(),
|
|
1617
|
+
bytesWritten: streamPlayer?.getBytesWritten() ?? 0,
|
|
1618
|
+
state: streamReader.getState()
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function getStreamReader() {
|
|
1622
|
+
return streamReader;
|
|
1623
|
+
}
|
|
1624
|
+
function getStreamingSynthesizer() {
|
|
1625
|
+
return streamingSynthesizer;
|
|
1626
|
+
}
|
|
1627
|
+
function getStreamPlayer() {
|
|
1628
|
+
return streamPlayer;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/config.ts
|
|
1632
|
+
import * as fs3 from "fs";
|
|
1633
|
+
import * as path from "path";
|
|
1634
|
+
import * as os from "os";
|
|
1635
|
+
var DEFAULT_CONFIG = {
|
|
1636
|
+
enabled: true,
|
|
1637
|
+
autoPlay: false,
|
|
1638
|
+
autoRead: false,
|
|
1639
|
+
streamMode: true,
|
|
1640
|
+
streamBufferSize: 30,
|
|
1641
|
+
streamBufferTimeout: 2e3,
|
|
1642
|
+
speed: 1,
|
|
1643
|
+
volume: 80,
|
|
1644
|
+
pitch: 1
|
|
1645
|
+
};
|
|
1646
|
+
var CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "ocosay.jsonc");
|
|
1647
|
+
function generateDefaultConfig() {
|
|
1648
|
+
return {
|
|
1649
|
+
...DEFAULT_CONFIG,
|
|
1650
|
+
providers: {
|
|
1651
|
+
minimax: {
|
|
1652
|
+
apiKey: "",
|
|
1653
|
+
baseURL: "",
|
|
1654
|
+
voiceId: "",
|
|
1655
|
+
model: "stream",
|
|
1656
|
+
ttsModel: "speech-2.8-hd",
|
|
1657
|
+
audioFormat: "mp3"
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function stripComments(jsonc) {
|
|
1663
|
+
let result = "";
|
|
1664
|
+
let inString = false;
|
|
1665
|
+
let stringChar = "";
|
|
1666
|
+
let i = 0;
|
|
1667
|
+
while (i < jsonc.length) {
|
|
1668
|
+
const char = jsonc[i];
|
|
1669
|
+
if (!inString && (char === '"' || char === "'" || char === "`")) {
|
|
1670
|
+
inString = true;
|
|
1671
|
+
stringChar = char;
|
|
1672
|
+
result += char;
|
|
1673
|
+
i++;
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
if (inString && char === stringChar && jsonc[i - 1] !== "\\") {
|
|
1677
|
+
inString = false;
|
|
1678
|
+
result += char;
|
|
1679
|
+
i++;
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (inString) {
|
|
1683
|
+
result += char;
|
|
1684
|
+
i++;
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (char === "/" && jsonc[i + 1] === "/") {
|
|
1688
|
+
while (i < jsonc.length && jsonc[i] !== "\n") {
|
|
1689
|
+
i++;
|
|
1690
|
+
}
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (char === "/" && jsonc[i + 1] === "*") {
|
|
1694
|
+
i += 2;
|
|
1695
|
+
while (i < jsonc.length - 1 && !(jsonc[i] === "*" && jsonc[i + 1] === "/")) {
|
|
1696
|
+
i++;
|
|
1697
|
+
}
|
|
1698
|
+
i += 2;
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
result += char;
|
|
1702
|
+
i++;
|
|
1703
|
+
}
|
|
1704
|
+
return result;
|
|
1705
|
+
}
|
|
1706
|
+
function mergeWithDefaults(loaded, defaults) {
|
|
1707
|
+
return {
|
|
1708
|
+
enabled: loaded.enabled ?? defaults.enabled,
|
|
1709
|
+
autoPlay: loaded.autoPlay ?? defaults.autoPlay,
|
|
1710
|
+
autoRead: loaded.autoRead ?? defaults.autoRead,
|
|
1711
|
+
streamMode: loaded.streamMode ?? defaults.streamMode,
|
|
1712
|
+
streamBufferSize: loaded.streamBufferSize ?? defaults.streamBufferSize,
|
|
1713
|
+
streamBufferTimeout: loaded.streamBufferTimeout ?? defaults.streamBufferTimeout,
|
|
1714
|
+
speed: loaded.speed ?? defaults.speed,
|
|
1715
|
+
volume: loaded.volume ?? defaults.volume,
|
|
1716
|
+
pitch: loaded.pitch ?? defaults.pitch
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function loadOrCreateConfig() {
|
|
1720
|
+
const configDir = path.dirname(CONFIG_PATH);
|
|
1721
|
+
if (!fs3.existsSync(configDir)) {
|
|
1722
|
+
try {
|
|
1723
|
+
fs3.mkdirSync(configDir, { recursive: true });
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
throw new Error(`[ocosay] \u65E0\u6CD5\u521B\u5EFA\u914D\u7F6E\u76EE\u5F55 ${configDir}: ${err}`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (!fs3.existsSync(CONFIG_PATH)) {
|
|
1729
|
+
console.info("[ocosay] \u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF0C\u6B63\u5728\u521B\u5EFA\u9ED8\u8BA4\u914D\u7F6E...");
|
|
1730
|
+
const defaultConfig = generateDefaultConfig();
|
|
1731
|
+
const configContent = JSON.stringify(defaultConfig, null, 2);
|
|
1732
|
+
try {
|
|
1733
|
+
fs3.writeFileSync(CONFIG_PATH, configContent, "utf-8");
|
|
1734
|
+
try {
|
|
1735
|
+
fs3.chmodSync(CONFIG_PATH, 384);
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
console.warn(`[ocosay] \u65E0\u6CD5\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\u6743\u9650: ${err}`);
|
|
1738
|
+
}
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
throw new Error(`[ocosay] \u65E0\u6CD5\u5199\u5165\u914D\u7F6E\u6587\u4EF6 ${CONFIG_PATH}: ${err}`);
|
|
1741
|
+
}
|
|
1742
|
+
console.info(`[ocosay] \u914D\u7F6E\u6587\u4EF6\u5DF2\u521B\u5EFA: ${CONFIG_PATH}`);
|
|
1743
|
+
console.info("[ocosay] \u8BF7\u7F16\u8F91\u914D\u7F6E\u6587\u4EF6\u586B\u5165 API Key \u548C Base URL");
|
|
1744
|
+
return defaultConfig;
|
|
1745
|
+
}
|
|
1746
|
+
try {
|
|
1747
|
+
const content = fs3.readFileSync(CONFIG_PATH, "utf-8");
|
|
1748
|
+
const stripped = stripComments(content);
|
|
1749
|
+
const loaded = JSON.parse(stripped);
|
|
1750
|
+
const merged = mergeWithDefaults(loaded, DEFAULT_CONFIG);
|
|
143
1751
|
return {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
tts_stream_stop: ttsStreamStopTool,
|
|
154
|
-
tts_stream_status: ttsStreamStatusTool
|
|
155
|
-
},
|
|
156
|
-
config: async () => {
|
|
157
|
-
return;
|
|
1752
|
+
...merged,
|
|
1753
|
+
providers: {
|
|
1754
|
+
minimax: {
|
|
1755
|
+
apiKey: loaded.providers?.minimax?.apiKey ?? "",
|
|
1756
|
+
baseURL: loaded.providers?.minimax?.baseURL ?? "",
|
|
1757
|
+
voiceId: loaded.providers?.minimax?.voiceId ?? "",
|
|
1758
|
+
model: loaded.providers?.minimax?.model ?? "stream",
|
|
1759
|
+
ttsModel: loaded.providers?.minimax?.ttsModel ?? "speech-2.8-hd",
|
|
1760
|
+
audioFormat: loaded.providers?.minimax?.audioFormat ?? "mp3"
|
|
158
1761
|
}
|
|
1762
|
+
}
|
|
159
1763
|
};
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
console.error("[ocosay] \u914D\u7F6E\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E:", error);
|
|
1766
|
+
return generateDefaultConfig();
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/plugin.ts
|
|
1771
|
+
var pluginName = "ocosay";
|
|
1772
|
+
var ttsSpeakTool = tool({
|
|
1773
|
+
description: "\u5C06\u6587\u672C\u8F6C\u6362\u4E3A\u8BED\u97F3\u5E76\u64AD\u653E",
|
|
1774
|
+
args: {
|
|
1775
|
+
text: tool.schema.string().describe("\u8981\u8F6C\u6362\u7684\u6587\u672C\u5185\u5BB9"),
|
|
1776
|
+
provider: tool.schema.string().optional().describe("TTS \u63D0\u4F9B\u5546\u540D\u79F0"),
|
|
1777
|
+
voice: tool.schema.string().optional().describe("\u97F3\u8272 ID"),
|
|
1778
|
+
model: tool.schema.enum(["sync", "async", "stream"]).optional().describe("\u5408\u6210\u6A21\u5F0F"),
|
|
1779
|
+
speed: tool.schema.number().optional().describe("\u8BED\u901F 0.5-2.0"),
|
|
1780
|
+
volume: tool.schema.number().optional().describe("\u97F3\u91CF 0-100"),
|
|
1781
|
+
pitch: tool.schema.number().optional().describe("\u97F3\u8C03 0.5-2.0")
|
|
1782
|
+
},
|
|
1783
|
+
execute: async (args) => {
|
|
1784
|
+
const result = await handleToolCall("tts_speak", args);
|
|
1785
|
+
if (result.success === false) {
|
|
1786
|
+
throw new Error(result.error || "Unknown error");
|
|
1787
|
+
}
|
|
1788
|
+
return result;
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
var ttsStopTool = tool({
|
|
1792
|
+
description: "\u505C\u6B62\u5F53\u524D TTS \u64AD\u653E",
|
|
1793
|
+
args: {},
|
|
1794
|
+
execute: async () => {
|
|
1795
|
+
const result = await handleToolCall("tts_stop");
|
|
1796
|
+
if (result.success === false) {
|
|
1797
|
+
throw new Error(result.error || "Unknown error");
|
|
1798
|
+
}
|
|
1799
|
+
return result;
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
var ttsPauseTool = tool({
|
|
1803
|
+
description: "\u6682\u505C\u5F53\u524D TTS \u64AD\u653E",
|
|
1804
|
+
args: {},
|
|
1805
|
+
execute: async () => {
|
|
1806
|
+
const result = await handleToolCall("tts_pause");
|
|
1807
|
+
if (result.success === false) {
|
|
1808
|
+
throw new Error(result.error || "Unknown error");
|
|
1809
|
+
}
|
|
1810
|
+
return result;
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
var ttsResumeTool = tool({
|
|
1814
|
+
description: "\u6062\u590D\u6682\u505C\u7684 TTS \u64AD\u653E",
|
|
1815
|
+
args: {},
|
|
1816
|
+
execute: async () => {
|
|
1817
|
+
const result = await handleToolCall("tts_resume");
|
|
1818
|
+
if (result.success === false) {
|
|
1819
|
+
throw new Error(result.error || "Unknown error");
|
|
1820
|
+
}
|
|
1821
|
+
return result;
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
var ttsListVoicesTool = tool({
|
|
1825
|
+
description: "\u5217\u51FA\u53EF\u7528\u7684\u97F3\u8272",
|
|
1826
|
+
args: {
|
|
1827
|
+
provider: tool.schema.string().optional().describe("TTS \u63D0\u4F9B\u5546\u540D\u79F0")
|
|
1828
|
+
},
|
|
1829
|
+
execute: async (args) => {
|
|
1830
|
+
const result = await handleToolCall("tts_list_voices", args);
|
|
1831
|
+
if (result.success === false) {
|
|
1832
|
+
throw new Error(result.error || "Unknown error");
|
|
1833
|
+
}
|
|
1834
|
+
return result;
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
var ttsListProvidersTool = tool({
|
|
1838
|
+
description: "\u5217\u51FA\u6240\u6709\u5DF2\u6CE8\u518C\u7684 TTS \u63D0\u4F9B\u5546",
|
|
1839
|
+
args: {},
|
|
1840
|
+
execute: async () => {
|
|
1841
|
+
const result = await handleToolCall("tts_list_providers");
|
|
1842
|
+
if (result.success === false) {
|
|
1843
|
+
throw new Error(result.error || "Unknown error");
|
|
1844
|
+
}
|
|
1845
|
+
return result;
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
var ttsStatusTool = tool({
|
|
1849
|
+
description: "\u83B7\u53D6\u5F53\u524D TTS \u64AD\u653E\u72B6\u6001",
|
|
1850
|
+
args: {},
|
|
1851
|
+
execute: async () => {
|
|
1852
|
+
const result = await handleToolCall("tts_status");
|
|
1853
|
+
if (result.success === false) {
|
|
1854
|
+
throw new Error(result.error || "Unknown error");
|
|
1855
|
+
}
|
|
1856
|
+
return result;
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
var ttsStreamSpeakTool = tool({
|
|
1860
|
+
description: "\u542F\u52A8\u6D41\u5F0F\u6717\u8BFB\uFF08\u8C46\u5305\u6A21\u5F0F\uFF09\uFF0C\u8BA2\u9605AI\u56DE\u590D\u5E76\u8FB9\u751F\u6210\u8FB9\u6717\u8BFB",
|
|
1861
|
+
args: {
|
|
1862
|
+
text: tool.schema.string().optional().describe("\u521D\u59CB\u6587\u672C\uFF08\u53EF\u9009\uFF09"),
|
|
1863
|
+
voice: tool.schema.string().optional().describe("\u97F3\u8272ID"),
|
|
1864
|
+
model: tool.schema.enum(["sync", "async", "stream"]).optional().describe("\u5408\u6210\u6A21\u5F0F")
|
|
1865
|
+
},
|
|
1866
|
+
execute: async (args) => {
|
|
1867
|
+
const result = await handleToolCall("tts_stream_speak", args);
|
|
1868
|
+
if (result.success === false) {
|
|
1869
|
+
throw new Error(result.error || "Unknown error");
|
|
1870
|
+
}
|
|
1871
|
+
return result;
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
var ttsStreamStopTool = tool({
|
|
1875
|
+
description: "\u505C\u6B62\u5F53\u524D\u6D41\u5F0F\u6717\u8BFB",
|
|
1876
|
+
args: {},
|
|
1877
|
+
execute: async () => {
|
|
1878
|
+
const result = await handleToolCall("tts_stream_stop");
|
|
1879
|
+
if (result.success === false) {
|
|
1880
|
+
throw new Error(result.error || "Unknown error");
|
|
1881
|
+
}
|
|
1882
|
+
return result;
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
var ttsStreamStatusTool = tool({
|
|
1886
|
+
description: "\u83B7\u53D6\u5F53\u524D\u6D41\u5F0F\u6717\u8BFB\u72B6\u6001",
|
|
1887
|
+
args: {},
|
|
1888
|
+
execute: async () => {
|
|
1889
|
+
const result = await handleToolCall("tts_stream_status");
|
|
1890
|
+
if (result.success === false) {
|
|
1891
|
+
throw new Error(result.error || "Unknown error");
|
|
1892
|
+
}
|
|
1893
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
var OcosayPlugin = async (_input, _options) => {
|
|
1897
|
+
console.info(`${pluginName}: initializing...`);
|
|
1898
|
+
const config = loadOrCreateConfig();
|
|
1899
|
+
await initialize({
|
|
1900
|
+
autoRead: config.autoRead,
|
|
1901
|
+
providers: {
|
|
1902
|
+
minimax: {
|
|
1903
|
+
apiKey: config.providers.minimax.apiKey,
|
|
1904
|
+
baseURL: config.providers.minimax.baseURL || void 0,
|
|
1905
|
+
voiceId: config.providers.minimax.voiceId || void 0
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
return {
|
|
1910
|
+
tool: {
|
|
1911
|
+
tts_speak: ttsSpeakTool,
|
|
1912
|
+
tts_stop: ttsStopTool,
|
|
1913
|
+
tts_pause: ttsPauseTool,
|
|
1914
|
+
tts_resume: ttsResumeTool,
|
|
1915
|
+
tts_list_voices: ttsListVoicesTool,
|
|
1916
|
+
tts_list_providers: ttsListProvidersTool,
|
|
1917
|
+
tts_status: ttsStatusTool,
|
|
1918
|
+
tts_stream_speak: ttsStreamSpeakTool,
|
|
1919
|
+
tts_stream_stop: ttsStreamStopTool,
|
|
1920
|
+
tts_stream_status: ttsStreamStatusTool
|
|
1921
|
+
},
|
|
1922
|
+
config: async () => {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
};
|
|
1927
|
+
var plugin_default = { server: OcosayPlugin };
|
|
1928
|
+
export {
|
|
1929
|
+
plugin_default as default
|
|
160
1930
|
};
|
|
161
|
-
|
|
162
|
-
//# sourceMappingURL=plugin.js.map
|
|
1931
|
+
//# sourceMappingURL=plugin.js.map
|