dsh-audiogen 0.2.0 → 0.3.2
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/lib/client.js +1101 -573
- package/lib/client.js.map +1 -1
- package/lib/index.js +528 -125
- package/package.json +1 -1
- package/skills/tts/SKILL.md +42 -4
- package/src/agent-audio-tools.ts +94 -7
- package/src/audio-engine.ts +215 -29
- package/src/audio-models.ts +132 -24
- package/src/audio-presets.ts +27 -33
- package/src/audio-store.ts +2 -0
- package/src/client/AudioGenPanel.tsx +140 -36
- package/src/client/SettingsCard.tsx +520 -176
- package/src/client/audio-panel.module.css +36 -0
- package/src/client/locales.ts +88 -18
- package/src/client/settings-card.module.css +424 -607
- package/src/protocol.ts +44 -2
- package/src/routes.ts +49 -4
package/lib/index.js
CHANGED
|
@@ -90,13 +90,13 @@ function isPreset(channel, id) {
|
|
|
90
90
|
function isOpenAICompatible(channel, mode) {
|
|
91
91
|
return isPreset(channel, "openai") || /(^|\/)(v\d+\/)?audio\/speech$/i.test(channel.apiUrl.trim()) || channel.preset === "custom" && mode === "tts";
|
|
92
92
|
}
|
|
93
|
-
function isElevenLabs(channel) {
|
|
93
|
+
function isElevenLabs$1(channel) {
|
|
94
94
|
return isPreset(channel, "elevenlabs") || /elevenlabs/i.test(channel.apiUrl);
|
|
95
95
|
}
|
|
96
96
|
function isMiniMax$1(channel) {
|
|
97
97
|
return isPreset(channel, "minimax") || /minimax/i.test(channel.apiUrl);
|
|
98
98
|
}
|
|
99
|
-
function isStability(channel) {
|
|
99
|
+
function isStability$1(channel) {
|
|
100
100
|
return isPreset(channel, "stability") || /stability\.ai/i.test(channel.apiUrl);
|
|
101
101
|
}
|
|
102
102
|
function endpointBase(url) {
|
|
@@ -281,15 +281,150 @@ function minimaxApiBase(base) {
|
|
|
281
281
|
const trimmed = endpointBase(base);
|
|
282
282
|
return /\/v1$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
283
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Resolve the MiniMax voice_id for a TTS request.
|
|
286
|
+
* Priority: explicit voice param → upstream id (if it is not a model name) →
|
|
287
|
+
* model alias (if it is not a model name). MiniMax speech/music model ids
|
|
288
|
+
* (speech-2.8-hd, music-3.0, …) are never treated as voice ids.
|
|
289
|
+
*/
|
|
290
|
+
function resolveMiniMaxVoice(request) {
|
|
291
|
+
const explicit = request.voice?.trim();
|
|
292
|
+
if (explicit !== void 0 && explicit !== "") return explicit;
|
|
293
|
+
for (const candidate of [request.upstream, request.model]) {
|
|
294
|
+
const value = typeof candidate === "string" ? candidate.trim() : "";
|
|
295
|
+
if (value === "") continue;
|
|
296
|
+
if (/^(speech|music|t2a|tts)[-_]/i.test(value)) continue;
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Build the full MiniMax t2a_v2 body. Every official field is carried
|
|
302
|
+
* through — voice_setting (voice_id/speed/vol/pitch/emotion/text_normalization/
|
|
303
|
+
* latex_read), pronunciation_dict.tone, audio_setting (format/sample_rate/
|
|
304
|
+
* bitrate/channel/force_cbr), subtitle_enable, aigc_watermark, language_boost,
|
|
305
|
+
* voice_modify and timbre_weights — so callers and skills can reference them.
|
|
306
|
+
*/
|
|
307
|
+
function buildMiniMaxTTSBody(request, model, voiceId) {
|
|
308
|
+
const body = {
|
|
309
|
+
model,
|
|
310
|
+
text: request.prompt,
|
|
311
|
+
stream: false,
|
|
312
|
+
voice_setting: {
|
|
313
|
+
voice_id: voiceId,
|
|
314
|
+
speed: request.speed ?? 1,
|
|
315
|
+
vol: request.vol ?? 1,
|
|
316
|
+
pitch: request.pitch ?? 0,
|
|
317
|
+
...request.emotion !== void 0 && request.emotion.trim() !== "" ? { emotion: request.emotion.trim() } : {},
|
|
318
|
+
...request.textNormalization !== void 0 ? { text_normalization: request.textNormalization } : {},
|
|
319
|
+
...request.latexRead !== void 0 ? { latex_read: request.latexRead } : {}
|
|
320
|
+
},
|
|
321
|
+
audio_setting: {
|
|
322
|
+
format: request.format ?? "mp3",
|
|
323
|
+
sample_rate: request.sampleRate ?? 32e3,
|
|
324
|
+
bitrate: request.bitrate ?? 128e3,
|
|
325
|
+
channel: request.audioChannel ?? 1,
|
|
326
|
+
...request.forceCbr !== void 0 ? { force_cbr: request.forceCbr } : {}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
if (request.pronunciationTone !== void 0 && request.pronunciationTone.length > 0) body.pronunciation_dict = { tone: request.pronunciationTone };
|
|
330
|
+
if (request.subtitleEnable !== void 0) body.subtitle_enable = request.subtitleEnable;
|
|
331
|
+
if (request.aigcWatermark !== void 0) body.aigc_watermark = request.aigcWatermark;
|
|
332
|
+
if (request.languageBoost !== void 0 && request.languageBoost.trim() !== "") body.language_boost = request.languageBoost.trim();
|
|
333
|
+
if (request.voiceModify !== void 0) {
|
|
334
|
+
const modify = {};
|
|
335
|
+
if (request.voiceModify.pitch !== void 0) modify.pitch = request.voiceModify.pitch;
|
|
336
|
+
if (request.voiceModify.intensity !== void 0) modify.intensity = request.voiceModify.intensity;
|
|
337
|
+
if (request.voiceModify.timbre !== void 0) modify.timbre = request.voiceModify.timbre;
|
|
338
|
+
if (request.voiceModify.soundEffects !== void 0 && request.voiceModify.soundEffects.trim() !== "") modify.sound_effects = request.voiceModify.soundEffects.trim();
|
|
339
|
+
if (Object.keys(modify).length > 0) body.voice_modify = modify;
|
|
340
|
+
}
|
|
341
|
+
if (request.timbreWeights !== void 0 && request.timbreWeights.length > 0) body.timbre_weights = request.timbreWeights.filter((item) => typeof item?.voiceId === "string" && item.voiceId.trim() !== "" && typeof item.weight === "number").map((item) => ({
|
|
342
|
+
voice_id: item.voiceId.trim(),
|
|
343
|
+
weight: item.weight
|
|
344
|
+
}));
|
|
345
|
+
return body;
|
|
346
|
+
}
|
|
347
|
+
/** The MiniMax-specific fields only (model/text/stream excluded) — used as the
|
|
348
|
+
* new-api `metadata` payload when a gateway serves MiniMax TTS at /v1/audio/speech.
|
|
349
|
+
* The merge keeps the gateway-sent model/input, and voice_setting.voice_id is
|
|
350
|
+
* carried explicitly so relays that overwrite it still get the right voice. */
|
|
351
|
+
function buildMiniMaxTTSUpload(request, voiceId) {
|
|
352
|
+
const upload = buildMiniMaxTTSBody(request, "", voiceId);
|
|
353
|
+
delete upload.model;
|
|
354
|
+
delete upload.text;
|
|
355
|
+
delete upload.stream;
|
|
356
|
+
return upload;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* OpenAI-compatible MiniMax TTS path for New API style gateways that do not
|
|
360
|
+
* route the native /v1/t2a_v2. The full native field set is carried inside
|
|
361
|
+
* `metadata`, which new-api's MiniMax TTS relay merges into t2a_v2 upstream.
|
|
362
|
+
*/
|
|
363
|
+
async function minimaxTTSGateway(channel, request, signal, voiceId) {
|
|
364
|
+
const endpoint = `${minimaxApiBase(channel.apiUrl)}/audio/speech`;
|
|
365
|
+
const model = (request.upstream ?? request.model) || "speech-2.8-hd";
|
|
366
|
+
const metadata = buildMiniMaxTTSUpload(request, voiceId);
|
|
367
|
+
const body = {
|
|
368
|
+
model,
|
|
369
|
+
input: request.prompt,
|
|
370
|
+
voice: voiceId,
|
|
371
|
+
response_format: request.format ?? "mp3",
|
|
372
|
+
...request.speed !== void 0 ? { speed: request.speed } : {},
|
|
373
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {}
|
|
374
|
+
};
|
|
375
|
+
return normalizeAudioResponse(await fetchWithTimeout(endpoint, {
|
|
376
|
+
method: "POST",
|
|
377
|
+
redirect: "follow",
|
|
378
|
+
headers: {
|
|
379
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
380
|
+
"content-type": "application/json",
|
|
381
|
+
accept: "application/json, audio/mpeg"
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify(body),
|
|
384
|
+
signal
|
|
385
|
+
}, UPSTREAM_TIMEOUT_MS), {
|
|
386
|
+
apiKey: channel.apiKey,
|
|
387
|
+
fallbackMime: "audio/mpeg"
|
|
388
|
+
});
|
|
389
|
+
}
|
|
284
390
|
async function minimax(channel, request, signal) {
|
|
285
391
|
const base = minimaxApiBase(channel.apiUrl);
|
|
286
392
|
const model = (request.upstream ?? request.model) || (request.mode === "music" ? "music-3.0" : "speech-2.8-hd");
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
393
|
+
if (request.mode === "voice_design") {
|
|
394
|
+
const endpoint = `${base}/voice_design`;
|
|
395
|
+
const body = {
|
|
396
|
+
prompt: request.prompt,
|
|
397
|
+
preview_text: request.previewText ?? request.voice ?? "你好,这是新设计的音色试听。"
|
|
398
|
+
};
|
|
399
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
400
|
+
method: "POST",
|
|
401
|
+
redirect: "error",
|
|
402
|
+
headers: {
|
|
403
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
404
|
+
"content-type": "application/json",
|
|
405
|
+
accept: "application/json"
|
|
406
|
+
},
|
|
407
|
+
body: JSON.stringify(body),
|
|
408
|
+
signal
|
|
409
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
const detail = await response.text().catch(() => "");
|
|
412
|
+
throw new AudioGenError(`MiniMax voice design API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
413
|
+
}
|
|
414
|
+
const payload = await response.json();
|
|
415
|
+
if (payload.base_resp?.status_code !== void 0 && payload.base_resp.status_code !== 0) throw new AudioGenError(payload.base_resp.status_msg ?? `MiniMax returned status ${payload.base_resp.status_code}`, "audio-api-error");
|
|
416
|
+
const encoded = payload.trial_audio ?? "";
|
|
417
|
+
if (encoded === "") throw new AudioGenError("MiniMax voice design returned no trial audio", "audio-empty-result");
|
|
418
|
+
const isHex = /^[0-9a-fA-F]+$/.test(encoded) && encoded.length % 2 === 0;
|
|
419
|
+
return [{
|
|
420
|
+
data: new Uint8Array(Buffer.from(encoded, isHex ? "hex" : "base64")),
|
|
421
|
+
mime: "audio/mpeg",
|
|
422
|
+
...payload.voice_id === void 0 ? {} : { voiceId: payload.voice_id }
|
|
423
|
+
}];
|
|
424
|
+
}
|
|
290
425
|
if (request.mode === "music") {
|
|
291
|
-
endpoint = `${base}/music_generation`;
|
|
292
|
-
body = {
|
|
426
|
+
const endpoint = `${base}/music_generation`;
|
|
427
|
+
const body = {
|
|
293
428
|
model,
|
|
294
429
|
prompt: request.prompt,
|
|
295
430
|
...request.duration !== void 0 ? { duration: request.duration } : {},
|
|
@@ -299,26 +434,31 @@ async function minimax(channel, request, signal) {
|
|
|
299
434
|
bitrate: 256e3
|
|
300
435
|
}
|
|
301
436
|
};
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
437
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
redirect: "error",
|
|
440
|
+
headers: {
|
|
441
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
442
|
+
"content-type": "application/json",
|
|
443
|
+
accept: "application/json, audio/mpeg"
|
|
444
|
+
},
|
|
445
|
+
body: JSON.stringify(body),
|
|
446
|
+
signal
|
|
447
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
448
|
+
if (!response.ok) {
|
|
449
|
+
const detail = await response.text().catch(() => "");
|
|
450
|
+
throw new AudioGenError(`MiniMax music API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
451
|
+
}
|
|
452
|
+
return normalizeAudioResponse(response, {
|
|
453
|
+
apiKey: channel.apiKey,
|
|
454
|
+
fallbackMime: "audio/mpeg"
|
|
455
|
+
});
|
|
320
456
|
}
|
|
321
|
-
|
|
457
|
+
const voiceId = resolveMiniMaxVoice(request);
|
|
458
|
+
if (voiceId === void 0) throw new AudioGenError("MiniMax TTS 需要指定音色 voice_id(如 male-qn-qingse、female-shaonv):请在「音色」字段填写,或把音色加入渠道模型目录(alias 可任意、upstream 填 voice_id),也可点「获取可用模型」拉取账号音色列表。", "voice-required");
|
|
459
|
+
const endpoint = `${base}/t2a_v2`;
|
|
460
|
+
const body = buildMiniMaxTTSBody(request, model, voiceId);
|
|
461
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
322
462
|
method: "POST",
|
|
323
463
|
redirect: "error",
|
|
324
464
|
headers: {
|
|
@@ -328,10 +468,19 @@ async function minimax(channel, request, signal) {
|
|
|
328
468
|
},
|
|
329
469
|
body: JSON.stringify(body),
|
|
330
470
|
signal
|
|
331
|
-
}, UPSTREAM_TIMEOUT_MS)
|
|
471
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
472
|
+
if (response.ok) return normalizeAudioResponse(response, {
|
|
332
473
|
apiKey: channel.apiKey,
|
|
333
474
|
fallbackMime: "audio/mpeg"
|
|
334
475
|
});
|
|
476
|
+
const detail = await response.text().catch(() => "");
|
|
477
|
+
if (!(response.status === 404 && /invalid url|invalid_request_error/i.test(detail))) throw new AudioGenError(`MiniMax TTS API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
478
|
+
try {
|
|
479
|
+
return await minimaxTTSGateway(channel, request, signal, voiceId);
|
|
480
|
+
} catch (gatewayError) {
|
|
481
|
+
const detailText = gatewayError instanceof AudioGenError ? gatewayError.message : String(gatewayError);
|
|
482
|
+
throw new AudioGenError(`MiniMax 渠道「${channel.name}」网关未提供原生 TTS 接口:POST ${endpoint} 返回 HTTP 404(Invalid URL,网关未路由 /v1/t2a_v2);已回退 OpenAI 兼容 ${minimaxApiBase(channel.apiUrl)}/audio/speech 仍失败:${detailText.slice(0, 300)}。请把渠道 API 地址配置为官方 https://api.minimaxi.com(配合 MiniMax 官方密钥),或确认网关已将 /v1/audio/speech 映射到 MiniMax 音色渠道。`, "audio-api-error");
|
|
483
|
+
}
|
|
335
484
|
}
|
|
336
485
|
async function stabilityAudio(channel, request, signal) {
|
|
337
486
|
const base = endpointBase(channel.apiUrl);
|
|
@@ -392,71 +541,22 @@ async function generateAudio(channel, request, signal) {
|
|
|
392
541
|
if (channel.apiUrl.trim() === "") throw new AudioGenError("channel API URL is not configured", "audio-no-endpoint");
|
|
393
542
|
if (channel.apiKey.trim() === "") throw new AudioGenError("channel API key is not configured", "audio-no-key");
|
|
394
543
|
if (request.prompt.trim() === "") throw new AudioGenError("audio prompt/text is required", "audio-empty-prompt");
|
|
395
|
-
if (
|
|
544
|
+
if (request.mode === "voice_design" && !isMiniMax$1(channel)) throw new AudioGenError("音色设计当前仅支持 MiniMax 渠道", "voice-design-unsupported");
|
|
545
|
+
if (isElevenLabs$1(channel)) return elevenLabs(channel, request, signal);
|
|
396
546
|
if (isMiniMax$1(channel)) return minimax(channel, request, signal);
|
|
397
|
-
if (isStability(channel)) return stabilityAudio(channel, request, signal);
|
|
547
|
+
if (isStability$1(channel)) return stabilityAudio(channel, request, signal);
|
|
398
548
|
if (isOpenAICompatible(channel, request.mode)) return openAITTS(channel, request, signal);
|
|
399
549
|
return genericAudio(channel, request, signal);
|
|
400
550
|
}
|
|
401
551
|
//#endregion
|
|
402
552
|
//#region src/audio-presets.ts
|
|
403
553
|
const AUDIO_PRESETS = [
|
|
404
|
-
{
|
|
405
|
-
id: "openai-tts",
|
|
406
|
-
name: "OpenAI · TTS",
|
|
407
|
-
apiUrl: "https://api.openai.com/v1",
|
|
408
|
-
hint: "OpenAI 官方语音合成接口(/audio/speech)",
|
|
409
|
-
models: [
|
|
410
|
-
{
|
|
411
|
-
alias: "tts-1",
|
|
412
|
-
id: "tts-1",
|
|
413
|
-
category: "tts"
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
alias: "tts-1-hd",
|
|
417
|
-
id: "tts-1-hd",
|
|
418
|
-
category: "tts"
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
alias: "gpt-4o-mini-tts",
|
|
422
|
-
id: "gpt-4o-mini-tts",
|
|
423
|
-
category: "tts"
|
|
424
|
-
}
|
|
425
|
-
]
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
id: "elevenlabs",
|
|
429
|
-
name: "ElevenLabs",
|
|
430
|
-
apiUrl: "https://api.elevenlabs.io/v1",
|
|
431
|
-
hint: "ElevenLabs TTS;模型列表请填写你的 Voice ID(如 Rachel / Adam 等别名)",
|
|
432
|
-
models: [
|
|
433
|
-
{
|
|
434
|
-
alias: "Rachel",
|
|
435
|
-
id: "21m00Tcm4TlvDq8ikWAM",
|
|
436
|
-
category: "tts"
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
alias: "Adam",
|
|
440
|
-
id: "pNInz6obpgDQGcFmaJgB",
|
|
441
|
-
category: "tts"
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
alias: "Antoni",
|
|
445
|
-
id: "ErXwobaYiN019PkySvjV",
|
|
446
|
-
category: "tts"
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
alias: "Bella",
|
|
450
|
-
id: "EXAVITQu4vr4xnSDxMaL",
|
|
451
|
-
category: "tts"
|
|
452
|
-
}
|
|
453
|
-
]
|
|
454
|
-
},
|
|
455
554
|
{
|
|
456
555
|
id: "minimax",
|
|
457
556
|
name: "MiniMax",
|
|
458
557
|
apiUrl: "https://api.minimaxi.com",
|
|
459
|
-
|
|
558
|
+
site: "https://www.minimaxi.com",
|
|
559
|
+
hint: "MiniMax 官方音频:音色设计 / TTS / 音乐生成;建议点击「获取可用模型」拉取账号音色与模型",
|
|
460
560
|
models: [
|
|
461
561
|
{
|
|
462
562
|
alias: "speech-2.8-hd",
|
|
@@ -515,11 +615,56 @@ const AUDIO_PRESETS = [
|
|
|
515
615
|
}
|
|
516
616
|
]
|
|
517
617
|
},
|
|
618
|
+
{
|
|
619
|
+
id: "elevenlabs",
|
|
620
|
+
name: "ElevenLabs",
|
|
621
|
+
apiUrl: "https://api.elevenlabs.io/v1",
|
|
622
|
+
site: "https://elevenlabsai.cn",
|
|
623
|
+
hint: "ElevenLabs 语音合成(TTS);建议点击「获取可用模型」拉取音色与模型",
|
|
624
|
+
models: [
|
|
625
|
+
{
|
|
626
|
+
alias: "Rachel",
|
|
627
|
+
id: "21m00Tcm4TlvDq8ikWAM",
|
|
628
|
+
category: "tts"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
alias: "Adam",
|
|
632
|
+
id: "pNInz6obpgDQGcFmaJgB",
|
|
633
|
+
category: "tts"
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
alias: "Antoni",
|
|
637
|
+
id: "ErXwobaYiN019PkySvjV",
|
|
638
|
+
category: "tts"
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
alias: "Bella",
|
|
642
|
+
id: "EXAVITQu4vr4xnSDxMaL",
|
|
643
|
+
category: "tts"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
alias: "eleven_multilingual_v2",
|
|
647
|
+
id: "eleven_multilingual_v2",
|
|
648
|
+
category: "tts"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
alias: "eleven_turbo_v2_5",
|
|
652
|
+
id: "eleven_turbo_v2_5",
|
|
653
|
+
category: "tts"
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
alias: "eleven_flash_v2_5",
|
|
657
|
+
id: "eleven_flash_v2_5",
|
|
658
|
+
category: "tts"
|
|
659
|
+
}
|
|
660
|
+
]
|
|
661
|
+
},
|
|
518
662
|
{
|
|
519
663
|
id: "stability-audio",
|
|
520
|
-
name: "Stability AI
|
|
664
|
+
name: "Stability AI(stable-audio)",
|
|
521
665
|
apiUrl: "https://api.stability.ai/v2beta/audio",
|
|
522
|
-
|
|
666
|
+
site: "https://stability.ai/stable-audio",
|
|
667
|
+
hint: "Stability AI 音乐 / 音效生成(stable-audio 系列)",
|
|
523
668
|
models: [{
|
|
524
669
|
alias: "stable-audio-2.0",
|
|
525
670
|
id: "stable-audio-2.0",
|
|
@@ -529,13 +674,6 @@ const AUDIO_PRESETS = [
|
|
|
529
674
|
id: "stable-audio-1.0",
|
|
530
675
|
category: "music"
|
|
531
676
|
}]
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
id: "custom",
|
|
535
|
-
name: "自定义渠道",
|
|
536
|
-
apiUrl: "",
|
|
537
|
-
hint: "任意兼容接口;支持 OpenAI 兼容 TTS,或返回音频字节 / JSON 的通用 POST",
|
|
538
|
-
models: []
|
|
539
677
|
}
|
|
540
678
|
];
|
|
541
679
|
/** Look up one built-in provider by id. */
|
|
@@ -547,17 +685,32 @@ function audioPresetById(id) {
|
|
|
547
685
|
function isMiniMax(channel) {
|
|
548
686
|
return channel.preset === "minimax" || /minimax/i.test(channel.apiUrl);
|
|
549
687
|
}
|
|
688
|
+
function isElevenLabs(channel) {
|
|
689
|
+
return channel.preset === "elevenlabs" || /elevenlabs/i.test(channel.apiUrl);
|
|
690
|
+
}
|
|
691
|
+
function isStability(channel) {
|
|
692
|
+
return channel.preset === "stability" || /stability\.ai/i.test(channel.apiUrl);
|
|
693
|
+
}
|
|
550
694
|
function baseUrl(url) {
|
|
551
695
|
return url.trim().replace(/\/+$/, "");
|
|
552
696
|
}
|
|
697
|
+
/** Whether an upstream model id is audio-related at all. */
|
|
553
698
|
function categoryFor(id) {
|
|
554
699
|
const value = id.toLowerCase();
|
|
555
|
-
if (/(tts|speech|voice|t2a)/i.test(value)) return "tts";
|
|
556
|
-
if (/(music|song|cover|lyrics)/i.test(value)) return "music";
|
|
700
|
+
if (/(tts|speech|voice|t2a|talk|narration)/i.test(value)) return "tts";
|
|
701
|
+
if (/(music|song|cover|lyrics|audio|melody|beat)/i.test(value)) return "music";
|
|
557
702
|
if (/(sfx|sound.?effect|effect|foley)/i.test(value)) return "sfx";
|
|
558
703
|
}
|
|
704
|
+
async function fetchJson(url, init) {
|
|
705
|
+
const response = await fetch(url, init);
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
const text = await response.text().catch(() => "");
|
|
708
|
+
throw new Error(`HTTP ${response.status}${text === "" ? "" : `: ${text.slice(0, 300)}`}`);
|
|
709
|
+
}
|
|
710
|
+
return response.json();
|
|
711
|
+
}
|
|
559
712
|
async function postJson(url, apiKey, body) {
|
|
560
|
-
|
|
713
|
+
return fetchJson(url, {
|
|
561
714
|
method: "POST",
|
|
562
715
|
headers: {
|
|
563
716
|
authorization: `Bearer ${apiKey.trim()}`,
|
|
@@ -565,18 +718,20 @@ async function postJson(url, apiKey, body) {
|
|
|
565
718
|
},
|
|
566
719
|
body: JSON.stringify(body)
|
|
567
720
|
});
|
|
568
|
-
if (!response.ok) {
|
|
569
|
-
const text = await response.text().catch(() => "");
|
|
570
|
-
throw new Error(`HTTP ${response.status}${text === "" ? "" : `: ${text.slice(0, 300)}`}`);
|
|
571
|
-
}
|
|
572
|
-
return response.json();
|
|
573
721
|
}
|
|
574
722
|
/** Discover available models/voices for a channel. */
|
|
575
723
|
async function discoverAudioModels(channel) {
|
|
576
724
|
if (channel.apiUrl.trim() === "") throw new Error("API URL is not configured");
|
|
577
725
|
if (channel.apiKey.trim() === "") throw new Error("API key is not configured");
|
|
578
|
-
if (isMiniMax(channel))
|
|
579
|
-
|
|
726
|
+
if (isMiniMax(channel)) return discoverMiniMax(channel);
|
|
727
|
+
if (isElevenLabs(channel)) return discoverElevenLabs(channel);
|
|
728
|
+
if (isStability(channel)) return discoverStability(channel);
|
|
729
|
+
return discoverOpenAICompatible(channel);
|
|
730
|
+
}
|
|
731
|
+
async function discoverMiniMax(channel) {
|
|
732
|
+
const url = `${baseUrl(channel.apiUrl).replace(/\/v1$/i, "")}/v1/get_voice`;
|
|
733
|
+
try {
|
|
734
|
+
const payload = await postJson(url, channel.apiKey, { voice_type: "all" });
|
|
580
735
|
if (payload.base_resp?.status_code !== void 0 && payload.base_resp.status_code !== 0) throw new Error(payload.base_resp.status_msg ?? `MiniMax returned status ${payload.base_resp.status_code}`);
|
|
581
736
|
const models = [];
|
|
582
737
|
for (const voice of payload.system_voice ?? []) {
|
|
@@ -616,18 +771,80 @@ async function discoverAudioModels(channel) {
|
|
|
616
771
|
});
|
|
617
772
|
return {
|
|
618
773
|
models: dedupe(models),
|
|
619
|
-
source: "MiniMax get_voice +
|
|
774
|
+
source: "MiniMax get_voice + music 目录"
|
|
775
|
+
};
|
|
776
|
+
} catch (error) {
|
|
777
|
+
const fallback = (audioPresetById("minimax")?.models ?? []).map((model) => ({ ...model }));
|
|
778
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
779
|
+
return {
|
|
780
|
+
models: dedupe(fallback),
|
|
781
|
+
source: `内置 MiniMax 目录(音色发现失败:${message.slice(0, 160)})`
|
|
620
782
|
};
|
|
621
783
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
784
|
+
}
|
|
785
|
+
async function discoverElevenLabs(channel) {
|
|
786
|
+
const base = baseUrl(channel.apiUrl);
|
|
787
|
+
const headers = { "xi-api-key": channel.apiKey.trim() };
|
|
788
|
+
const failures = [];
|
|
789
|
+
const models = [];
|
|
790
|
+
try {
|
|
791
|
+
const payload = await fetchJson(`${base}/models`, { headers });
|
|
792
|
+
for (const item of Array.isArray(payload) ? payload : []) {
|
|
793
|
+
const id = item.model_id?.trim() ?? "";
|
|
794
|
+
if (id === "") continue;
|
|
795
|
+
if (item.capabilities?.text_to_speech !== true && item.capabilities?.voice_change !== true) continue;
|
|
796
|
+
models.push({
|
|
797
|
+
alias: item.name?.trim() || id,
|
|
798
|
+
id,
|
|
799
|
+
category: "tts",
|
|
800
|
+
...item.description !== void 0 && item.description.trim() !== "" ? { description: item.description.trim() } : {}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
failures.push(`模型列表:${error instanceof Error ? error.message : String(error)}`);
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
const payload = await fetchJson(`${base}/voices`, { headers });
|
|
808
|
+
for (const voice of Array.isArray(payload?.voices) ? payload.voices : []) {
|
|
809
|
+
const id = voice.voice_id?.trim() ?? "";
|
|
810
|
+
if (id === "") continue;
|
|
811
|
+
models.push({
|
|
812
|
+
alias: voice.name?.trim() || id,
|
|
813
|
+
id,
|
|
814
|
+
category: "tts",
|
|
815
|
+
...voice.description !== void 0 && voice.description.trim() !== "" ? { description: voice.description.trim() } : {}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
} catch (error) {
|
|
819
|
+
failures.push(`音色列表:${error instanceof Error ? error.message : String(error)}`);
|
|
820
|
+
}
|
|
821
|
+
if (models.length === 0) {
|
|
822
|
+
const fallback = (audioPresetById("elevenlabs")?.models ?? []).map((model) => ({ ...model }));
|
|
823
|
+
const detail = failures.length === 0 ? "" : `(发现失败:${failures.join(";").slice(0, 160)})`;
|
|
824
|
+
return {
|
|
825
|
+
models: dedupe(fallback),
|
|
826
|
+
source: `内置 ElevenLabs 目录${detail}`
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
models: dedupe(models),
|
|
831
|
+
source: "ElevenLabs /models + /voices"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
async function discoverStability(channel) {
|
|
835
|
+
return {
|
|
836
|
+
models: dedupe((audioPresetById("stability-audio")?.models ?? []).map((model) => ({ ...model }))),
|
|
837
|
+
source: "Stability stable-audio 内置目录"
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async function discoverOpenAICompatible(channel) {
|
|
841
|
+
const payload = await fetchJson(`${baseUrl(channel.apiUrl)}/models`, { headers: { authorization: `Bearer ${channel.apiKey.trim()}` } });
|
|
626
842
|
const models = [];
|
|
627
843
|
for (const item of payload.data ?? []) {
|
|
628
844
|
const id = item.id?.trim() ?? "";
|
|
629
845
|
if (id === "") continue;
|
|
630
|
-
const category = categoryFor(id)
|
|
846
|
+
const category = categoryFor(id);
|
|
847
|
+
if (category === void 0) continue;
|
|
631
848
|
models.push({
|
|
632
849
|
alias: id,
|
|
633
850
|
id,
|
|
@@ -636,7 +853,7 @@ async function discoverAudioModels(channel) {
|
|
|
636
853
|
}
|
|
637
854
|
return {
|
|
638
855
|
models: dedupe(models),
|
|
639
|
-
source: "OpenAI-compatible /models"
|
|
856
|
+
source: "OpenAI-compatible /models(仅音频相关)"
|
|
640
857
|
};
|
|
641
858
|
}
|
|
642
859
|
function dedupe(models) {
|
|
@@ -731,13 +948,15 @@ async function appendHistory(entry) {
|
|
|
731
948
|
model: entry.model,
|
|
732
949
|
prompt: entry.prompt,
|
|
733
950
|
...entry.voice === void 0 ? {} : { voice: entry.voice },
|
|
951
|
+
...entry.voiceId === void 0 ? {} : { voiceId: entry.voiceId },
|
|
734
952
|
...entry.speed === void 0 ? {} : { speed: entry.speed },
|
|
735
953
|
...entry.duration === void 0 ? {} : { duration: entry.duration },
|
|
736
954
|
...entry.format === void 0 ? {} : { format: entry.format },
|
|
737
955
|
audio: entry.audio.map((audio) => ({
|
|
738
956
|
url: audio.url,
|
|
739
957
|
mime: audio.mime,
|
|
740
|
-
...audio.duration === void 0 ? {} : { duration: audio.duration }
|
|
958
|
+
...audio.duration === void 0 ? {} : { duration: audio.duration },
|
|
959
|
+
...audio.voiceId === void 0 ? {} : { voiceId: audio.voiceId }
|
|
741
960
|
})),
|
|
742
961
|
...entry.channelId === void 0 ? {} : { channelId: entry.channelId },
|
|
743
962
|
...entry.channel === void 0 ? {} : { channel: entry.channel }
|
|
@@ -809,18 +1028,49 @@ function messageOf(error) {
|
|
|
809
1028
|
return error instanceof Error ? error.message : String(error);
|
|
810
1029
|
}
|
|
811
1030
|
function parseGenerateRequest(body) {
|
|
812
|
-
const mode = body.mode === "music" ? "music" : body.mode === "sfx" ? "sfx" : "tts";
|
|
1031
|
+
const mode = body.mode === "music" ? "music" : body.mode === "sfx" ? "sfx" : body.mode === "voice_design" ? "voice_design" : "tts";
|
|
813
1032
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
814
1033
|
if (prompt === "") return void 0;
|
|
1034
|
+
const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1035
|
+
const str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1036
|
+
const flag = (value) => typeof value === "boolean" ? value : void 0;
|
|
1037
|
+
const tone = Array.isArray(body.pronunciationTone) ? body.pronunciationTone.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()) : void 0;
|
|
1038
|
+
const voiceModifyRaw = body.voiceModify;
|
|
1039
|
+
const voiceModify = typeof voiceModifyRaw === "object" && voiceModifyRaw !== null ? {
|
|
1040
|
+
...num(voiceModifyRaw.pitch) !== void 0 ? { pitch: num(voiceModifyRaw.pitch) } : {},
|
|
1041
|
+
...num(voiceModifyRaw.intensity) !== void 0 ? { intensity: num(voiceModifyRaw.intensity) } : {},
|
|
1042
|
+
...num(voiceModifyRaw.timbre) !== void 0 ? { timbre: num(voiceModifyRaw.timbre) } : {},
|
|
1043
|
+
...str(voiceModifyRaw.soundEffects) !== void 0 ? { soundEffects: str(voiceModifyRaw.soundEffects) } : {}
|
|
1044
|
+
} : void 0;
|
|
1045
|
+
const timbreWeights = Array.isArray(body.timbreWeights) ? body.timbreWeights.filter((item) => typeof item === "object" && item !== null && typeof item.voiceId === "string" && typeof item.weight === "number").map((item) => ({
|
|
1046
|
+
voiceId: item.voiceId.trim(),
|
|
1047
|
+
weight: item.weight
|
|
1048
|
+
})).filter((item) => item.voiceId !== "") : void 0;
|
|
815
1049
|
return {
|
|
816
1050
|
mode,
|
|
817
1051
|
model: typeof body.model === "string" ? body.model.trim() : "",
|
|
818
1052
|
prompt,
|
|
819
1053
|
...typeof body.voice === "string" && body.voice.trim() !== "" ? { voice: body.voice.trim() } : {},
|
|
820
|
-
...typeof body.
|
|
821
|
-
...
|
|
1054
|
+
...typeof body.previewText === "string" && body.previewText.trim() !== "" ? { previewText: body.previewText.trim() } : {},
|
|
1055
|
+
...num(body.speed) !== void 0 ? { speed: num(body.speed) } : {},
|
|
1056
|
+
...num(body.duration) !== void 0 ? { duration: num(body.duration) } : {},
|
|
822
1057
|
...typeof body.format === "string" && body.format.trim() !== "" ? { format: body.format.trim() } : {},
|
|
823
|
-
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {}
|
|
1058
|
+
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
1059
|
+
...str(body.emotion) !== void 0 ? { emotion: str(body.emotion) } : {},
|
|
1060
|
+
...num(body.vol) !== void 0 ? { vol: num(body.vol) } : {},
|
|
1061
|
+
...num(body.pitch) !== void 0 ? { pitch: num(body.pitch) } : {},
|
|
1062
|
+
...flag(body.textNormalization) !== void 0 ? { textNormalization: flag(body.textNormalization) } : {},
|
|
1063
|
+
...flag(body.latexRead) !== void 0 ? { latexRead: flag(body.latexRead) } : {},
|
|
1064
|
+
...tone !== void 0 && tone.length > 0 ? { pronunciationTone: tone } : {},
|
|
1065
|
+
...num(body.sampleRate) !== void 0 ? { sampleRate: num(body.sampleRate) } : {},
|
|
1066
|
+
...num(body.bitrate) !== void 0 ? { bitrate: num(body.bitrate) } : {},
|
|
1067
|
+
...num(body.audioChannel) !== void 0 ? { audioChannel: num(body.audioChannel) } : {},
|
|
1068
|
+
...flag(body.forceCbr) !== void 0 ? { forceCbr: flag(body.forceCbr) } : {},
|
|
1069
|
+
...flag(body.subtitleEnable) !== void 0 ? { subtitleEnable: flag(body.subtitleEnable) } : {},
|
|
1070
|
+
...flag(body.aigcWatermark) !== void 0 ? { aigcWatermark: flag(body.aigcWatermark) } : {},
|
|
1071
|
+
...str(body.languageBoost) !== void 0 ? { languageBoost: str(body.languageBoost) } : {},
|
|
1072
|
+
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
1073
|
+
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
824
1074
|
};
|
|
825
1075
|
}
|
|
826
1076
|
function toView(descriptor) {
|
|
@@ -863,6 +1113,21 @@ function resolveChannelRequest(request, view) {
|
|
|
863
1113
|
const target = explicit ?? defaults;
|
|
864
1114
|
const asked = request.model.trim();
|
|
865
1115
|
if (asked === "") {
|
|
1116
|
+
if (request.mode === "voice_design") {
|
|
1117
|
+
if (target === void 0) return {
|
|
1118
|
+
ok: false,
|
|
1119
|
+
code: "no-channels",
|
|
1120
|
+
message: "尚未配置任何渠道"
|
|
1121
|
+
};
|
|
1122
|
+
return {
|
|
1123
|
+
ok: true,
|
|
1124
|
+
request: {
|
|
1125
|
+
...request,
|
|
1126
|
+
channelId: target.id,
|
|
1127
|
+
channel: target.name
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
866
1131
|
const alias = target?.models[0]?.alias ?? "";
|
|
867
1132
|
if (alias === "") return {
|
|
868
1133
|
ok: false,
|
|
@@ -946,7 +1211,7 @@ function makeRoutes(deps) {
|
|
|
946
1211
|
const stored = view.channels.find((candidate) => candidate.id === (typeof body?.channelId === "string" ? body.channelId : void 0)) ?? view.channels.find((candidate) => candidate.id === view.defaultChannelId) ?? view.channels[0];
|
|
947
1212
|
const channel = {
|
|
948
1213
|
id: stored?.id ?? "preview",
|
|
949
|
-
preset: stored?.preset ?? "",
|
|
1214
|
+
preset: typeof body?.preset === "string" ? body.preset.trim() : stored?.preset ?? "",
|
|
950
1215
|
name: stored?.name ?? "",
|
|
951
1216
|
apiUrl: typeof body?.apiUrl === "string" && body.apiUrl.trim() !== "" ? body.apiUrl.trim() : stored?.apiUrl ?? "",
|
|
952
1217
|
apiKey: typeof body?.apiKey === "string" && body.apiKey.trim() !== "" ? body.apiKey.trim() : stored?.apiKey ?? "",
|
|
@@ -1063,7 +1328,8 @@ function makeRoutes(deps) {
|
|
|
1063
1328
|
b64: Buffer.from(output.data).toString("base64"),
|
|
1064
1329
|
mime: saved.mime,
|
|
1065
1330
|
bytes: saved.bytes,
|
|
1066
|
-
url: `${AUDIO_API.file}/${encodeURIComponent(saved.file)}
|
|
1331
|
+
url: `${AUDIO_API.file}/${encodeURIComponent(saved.file)}`,
|
|
1332
|
+
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
1067
1333
|
});
|
|
1068
1334
|
}
|
|
1069
1335
|
let history;
|
|
@@ -1220,7 +1486,8 @@ const resultSchema = {
|
|
|
1220
1486
|
enum: [
|
|
1221
1487
|
"tts",
|
|
1222
1488
|
"music",
|
|
1223
|
-
"sfx"
|
|
1489
|
+
"sfx",
|
|
1490
|
+
"voice_design"
|
|
1224
1491
|
]
|
|
1225
1492
|
},
|
|
1226
1493
|
model: {
|
|
@@ -1249,7 +1516,8 @@ const resultSchema = {
|
|
|
1249
1516
|
bytes: {
|
|
1250
1517
|
type: "integer",
|
|
1251
1518
|
required: true
|
|
1252
|
-
}
|
|
1519
|
+
},
|
|
1520
|
+
voiceId: { type: "string" }
|
|
1253
1521
|
}
|
|
1254
1522
|
}
|
|
1255
1523
|
},
|
|
@@ -1287,7 +1555,7 @@ function ensureConfigured(config) {
|
|
|
1287
1555
|
function registerAgentAudioTools(ctx, resolve) {
|
|
1288
1556
|
return ctx.tools.register(defineTool({
|
|
1289
1557
|
name: "generate_audio",
|
|
1290
|
-
description: "Generate audio with the configured audio provider. Supports text-to-speech, music generation and
|
|
1558
|
+
description: "Generate audio with the configured audio provider. Supports text-to-speech, music generation, sound effects and MiniMax voice design. The tool call waits for the upstream result and returns same-origin audio URLs; pass those URLs to the user for playback or download. If multiple models are configured, first ask the user which one to use or pass model explicitly.",
|
|
1291
1559
|
parameters: {
|
|
1292
1560
|
prompt: {
|
|
1293
1561
|
type: "string",
|
|
@@ -1299,7 +1567,8 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1299
1567
|
enum: [
|
|
1300
1568
|
"tts",
|
|
1301
1569
|
"music",
|
|
1302
|
-
"sfx"
|
|
1570
|
+
"sfx",
|
|
1571
|
+
"voice_design"
|
|
1303
1572
|
],
|
|
1304
1573
|
description: "Generation mode. Defaults to tts."
|
|
1305
1574
|
},
|
|
@@ -1309,11 +1578,15 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1309
1578
|
},
|
|
1310
1579
|
voice: {
|
|
1311
1580
|
type: "string",
|
|
1312
|
-
description: "Optional voice id/name for TTS providers."
|
|
1581
|
+
description: "Optional voice id/name for TTS providers. Required for MiniMax TTS (e.g. male-qn-qingse, female-shaonv); fetch the account voices in Settings > Plugins > AI Audio."
|
|
1582
|
+
},
|
|
1583
|
+
preview_text: {
|
|
1584
|
+
type: "string",
|
|
1585
|
+
description: "Optional preview text for voice_design."
|
|
1313
1586
|
},
|
|
1314
1587
|
speed: {
|
|
1315
1588
|
type: "number",
|
|
1316
|
-
description: "Optional speaking rate / speed multiplier where supported."
|
|
1589
|
+
description: "Optional speaking rate / speed multiplier where supported. MiniMax range 0.5-2.0 (default 1)."
|
|
1317
1590
|
},
|
|
1318
1591
|
duration: {
|
|
1319
1592
|
type: "number",
|
|
@@ -1322,6 +1595,95 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1322
1595
|
format: {
|
|
1323
1596
|
type: "string",
|
|
1324
1597
|
description: "Output format such as mp3 or wav."
|
|
1598
|
+
},
|
|
1599
|
+
emotion: {
|
|
1600
|
+
type: "string",
|
|
1601
|
+
description: "MiniMax TTS emotion, e.g. happy/sad/angry/nervous/fearful/bored (voice_setting.emotion)."
|
|
1602
|
+
},
|
|
1603
|
+
vol: {
|
|
1604
|
+
type: "number",
|
|
1605
|
+
description: "MiniMax TTS volume 0-10, default 1 (voice_setting.vol)."
|
|
1606
|
+
},
|
|
1607
|
+
pitch: {
|
|
1608
|
+
type: "integer",
|
|
1609
|
+
description: "MiniMax TTS pitch shift -12..12 semitones, default 0 (voice_setting.pitch)."
|
|
1610
|
+
},
|
|
1611
|
+
text_normalization: {
|
|
1612
|
+
type: "boolean",
|
|
1613
|
+
description: "MiniMax TTS text normalization switch (voice_setting.text_normalization)."
|
|
1614
|
+
},
|
|
1615
|
+
latex_read: {
|
|
1616
|
+
type: "boolean",
|
|
1617
|
+
description: "MiniMax TTS math formula reading switch (voice_setting.latex_read)."
|
|
1618
|
+
},
|
|
1619
|
+
pronunciation_tone: {
|
|
1620
|
+
type: "array",
|
|
1621
|
+
items: { type: "string" },
|
|
1622
|
+
description: "MiniMax TTS pronunciation dictionary tone entries, each \"word/pronunciation\", e.g. [\"处理/(chu3)(li3)\", \"危险/dangerous\"] (pronunciation_dict.tone)."
|
|
1623
|
+
},
|
|
1624
|
+
sample_rate: {
|
|
1625
|
+
type: "integer",
|
|
1626
|
+
description: "MiniMax TTS sample rate: 16000/24000/32000/44100/48000, default 32000 (audio_setting.sample_rate)."
|
|
1627
|
+
},
|
|
1628
|
+
bitrate: {
|
|
1629
|
+
type: "integer",
|
|
1630
|
+
description: "MiniMax TTS bitrate in bps: 64000-320000, default 128000 (audio_setting.bitrate)."
|
|
1631
|
+
},
|
|
1632
|
+
channel: {
|
|
1633
|
+
type: "integer",
|
|
1634
|
+
description: "MiniMax TTS audio channels: 1 or 2, default 1 (audio_setting.channel)."
|
|
1635
|
+
},
|
|
1636
|
+
force_cbr: {
|
|
1637
|
+
type: "boolean",
|
|
1638
|
+
description: "MiniMax TTS force CBR encoding (audio_setting.force_cbr)."
|
|
1639
|
+
},
|
|
1640
|
+
subtitle_enable: {
|
|
1641
|
+
type: "boolean",
|
|
1642
|
+
description: "MiniMax TTS subtitle output switch (subtitle_enable)."
|
|
1643
|
+
},
|
|
1644
|
+
aigc_watermark: {
|
|
1645
|
+
type: "boolean",
|
|
1646
|
+
description: "MiniMax TTS AIGC watermark switch (aigc_watermark)."
|
|
1647
|
+
},
|
|
1648
|
+
language_boost: {
|
|
1649
|
+
type: "string",
|
|
1650
|
+
description: "MiniMax TTS language boost, e.g. 中英混读 (language_boost, model-dependent)."
|
|
1651
|
+
},
|
|
1652
|
+
voice_modify: {
|
|
1653
|
+
type: "object",
|
|
1654
|
+
additionalProperties: false,
|
|
1655
|
+
properties: {
|
|
1656
|
+
pitch: {
|
|
1657
|
+
type: "integer",
|
|
1658
|
+
description: "Pitch shift for voice modification."
|
|
1659
|
+
},
|
|
1660
|
+
intensity: {
|
|
1661
|
+
type: "integer",
|
|
1662
|
+
description: "Intensity for voice modification."
|
|
1663
|
+
},
|
|
1664
|
+
timbre: {
|
|
1665
|
+
type: "integer",
|
|
1666
|
+
description: "Timbre shift for voice modification."
|
|
1667
|
+
},
|
|
1668
|
+
sound_effects: {
|
|
1669
|
+
type: "string",
|
|
1670
|
+
description: "Sound effect for voice modification, e.g. 耳语."
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
description: "MiniMax TTS voice modification (voice_modify, supported by speech-2.8+)."
|
|
1674
|
+
},
|
|
1675
|
+
timbre_weights: {
|
|
1676
|
+
type: "array",
|
|
1677
|
+
items: {
|
|
1678
|
+
type: "object",
|
|
1679
|
+
additionalProperties: false,
|
|
1680
|
+
properties: {
|
|
1681
|
+
voice_id: { type: "string" },
|
|
1682
|
+
weight: { type: "integer" }
|
|
1683
|
+
},
|
|
1684
|
+
required: ["voice_id", "weight"]
|
|
1685
|
+
},
|
|
1686
|
+
description: "MiniMax TTS dual-voice blend weights (timbre_weights)."
|
|
1325
1687
|
}
|
|
1326
1688
|
},
|
|
1327
1689
|
output: {
|
|
@@ -1333,18 +1695,57 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1333
1695
|
async execute(args, exec) {
|
|
1334
1696
|
const config = resolve();
|
|
1335
1697
|
ensureConfigured(config);
|
|
1336
|
-
const
|
|
1698
|
+
const mode = args.mode === "music" ? "music" : args.mode === "sfx" ? "sfx" : args.mode === "voice_design" ? "voice_design" : "tts";
|
|
1699
|
+
const picked = mode === "voice_design" ? (() => {
|
|
1700
|
+
const usable = config.channels.filter((channel) => channel.apiUrl.trim() !== "" && channel.apiKey.trim() !== "");
|
|
1701
|
+
const target = usable.find((channel) => channel.id === config.defaultChannelId) ?? usable[0];
|
|
1702
|
+
if (target === void 0) throw new AudioGenError("No usable audio channel is configured for voice design.", "no-channel-available");
|
|
1703
|
+
return {
|
|
1704
|
+
channel: target,
|
|
1705
|
+
alias: "",
|
|
1706
|
+
upstream: ""
|
|
1707
|
+
};
|
|
1708
|
+
})() : resolveModel(config, args.model);
|
|
1709
|
+
const voiceModify = typeof args.voice_modify === "object" && args.voice_modify !== null ? (() => {
|
|
1710
|
+
const raw = args.voice_modify;
|
|
1711
|
+
const out = {};
|
|
1712
|
+
if (typeof raw.pitch === "number") out.pitch = raw.pitch;
|
|
1713
|
+
if (typeof raw.intensity === "number") out.intensity = raw.intensity;
|
|
1714
|
+
if (typeof raw.timbre === "number") out.timbre = raw.timbre;
|
|
1715
|
+
if (typeof raw.sound_effects === "string" && raw.sound_effects.trim() !== "") out.soundEffects = raw.sound_effects.trim();
|
|
1716
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1717
|
+
})() : void 0;
|
|
1718
|
+
const timbreWeights = Array.isArray(args.timbre_weights) ? args.timbre_weights.filter((item) => typeof item === "object" && item !== null && typeof item.voice_id === "string" && typeof item.weight === "number").map((item) => ({
|
|
1719
|
+
voiceId: item.voice_id.trim(),
|
|
1720
|
+
weight: item.weight
|
|
1721
|
+
})).filter((item) => item.voiceId !== "") : void 0;
|
|
1337
1722
|
const request = {
|
|
1338
|
-
mode
|
|
1723
|
+
mode,
|
|
1339
1724
|
model: picked.alias,
|
|
1340
1725
|
upstream: picked.upstream,
|
|
1341
1726
|
channelId: picked.channel.id,
|
|
1342
1727
|
channel: picked.channel.name,
|
|
1343
1728
|
prompt: args.prompt.trim(),
|
|
1344
1729
|
...typeof args.voice === "string" && args.voice.trim() !== "" ? { voice: args.voice.trim() } : {},
|
|
1730
|
+
...typeof args.preview_text === "string" && args.preview_text.trim() !== "" ? { previewText: args.preview_text.trim() } : {},
|
|
1345
1731
|
...typeof args.speed === "number" ? { speed: args.speed } : {},
|
|
1346
1732
|
...typeof args.duration === "number" ? { duration: args.duration } : {},
|
|
1347
|
-
...typeof args.format === "string" && args.format.trim() !== "" ? { format: args.format.trim() } : {}
|
|
1733
|
+
...typeof args.format === "string" && args.format.trim() !== "" ? { format: args.format.trim() } : {},
|
|
1734
|
+
...typeof args.emotion === "string" && args.emotion.trim() !== "" ? { emotion: args.emotion.trim() } : {},
|
|
1735
|
+
...typeof args.vol === "number" && Number.isFinite(args.vol) ? { vol: args.vol } : {},
|
|
1736
|
+
...typeof args.pitch === "number" && Number.isFinite(args.pitch) ? { pitch: args.pitch } : {},
|
|
1737
|
+
...typeof args.text_normalization === "boolean" ? { textNormalization: args.text_normalization } : {},
|
|
1738
|
+
...typeof args.latex_read === "boolean" ? { latexRead: args.latex_read } : {},
|
|
1739
|
+
...Array.isArray(args.pronunciation_tone) && args.pronunciation_tone.length > 0 ? { pronunciationTone: args.pronunciation_tone.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()) } : {},
|
|
1740
|
+
...typeof args.sample_rate === "number" && Number.isFinite(args.sample_rate) ? { sampleRate: args.sample_rate } : {},
|
|
1741
|
+
...typeof args.bitrate === "number" && Number.isFinite(args.bitrate) ? { bitrate: args.bitrate } : {},
|
|
1742
|
+
...typeof args.channel === "number" && Number.isFinite(args.channel) ? { audioChannel: args.channel } : {},
|
|
1743
|
+
...typeof args.force_cbr === "boolean" ? { forceCbr: args.force_cbr } : {},
|
|
1744
|
+
...typeof args.subtitle_enable === "boolean" ? { subtitleEnable: args.subtitle_enable } : {},
|
|
1745
|
+
...typeof args.aigc_watermark === "boolean" ? { aigcWatermark: args.aigc_watermark } : {},
|
|
1746
|
+
...typeof args.language_boost === "string" && args.language_boost.trim() !== "" ? { languageBoost: args.language_boost.trim() } : {},
|
|
1747
|
+
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
1748
|
+
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
1348
1749
|
};
|
|
1349
1750
|
try {
|
|
1350
1751
|
const outputs = await generateAudio(picked.channel, request, exec.signal);
|
|
@@ -1355,7 +1756,8 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1355
1756
|
id: saved.id,
|
|
1356
1757
|
url: `/api/dsh-audiogen/audio/${encodeURIComponent(saved.file)}`,
|
|
1357
1758
|
mime: saved.mime,
|
|
1358
|
-
bytes: saved.bytes
|
|
1759
|
+
bytes: saved.bytes,
|
|
1760
|
+
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
1359
1761
|
});
|
|
1360
1762
|
}
|
|
1361
1763
|
try {
|
|
@@ -1374,7 +1776,8 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1374
1776
|
b64: Buffer.from(output.data).toString("base64"),
|
|
1375
1777
|
mime: audio[index].mime,
|
|
1376
1778
|
bytes: audio[index].bytes,
|
|
1377
|
-
url: audio[index].url
|
|
1779
|
+
url: audio[index].url,
|
|
1780
|
+
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
1378
1781
|
})),
|
|
1379
1782
|
channelId: picked.channel.id,
|
|
1380
1783
|
channel: picked.channel.name
|