dsh-audiogen 0.3.0 → 0.3.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/lib/client.js +1069 -550
- package/lib/client.js.map +1 -1
- package/lib/index.js +483 -118
- package/package.json +1 -1
- package/skills/music/SKILL.md +30 -7
- package/skills/tts/SKILL.md +42 -4
- package/src/agent-audio-tools.ts +80 -3
- package/src/audio-engine.ts +185 -26
- package/src/audio-models.ts +132 -24
- package/src/audio-presets.ts +27 -33
- package/src/client/AudioGenPanel.tsx +92 -3
- package/src/client/SettingsCard.tsx +520 -176
- package/src/client/audio-panel.module.css +36 -0
- package/src/client/locales.ts +84 -16
- package/src/client/settings-card.module.css +424 -607
- package/src/protocol.ts +39 -1
- package/src/routes.ts +44 -3
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) {
|
|
@@ -116,6 +116,7 @@ function findBase64Audio(value) {
|
|
|
116
116
|
const record = value;
|
|
117
117
|
for (const key of [
|
|
118
118
|
"audio",
|
|
119
|
+
"music",
|
|
119
120
|
"b64_json",
|
|
120
121
|
"base64",
|
|
121
122
|
"data",
|
|
@@ -281,10 +282,115 @@ function minimaxApiBase(base) {
|
|
|
281
282
|
const trimmed = endpointBase(base);
|
|
282
283
|
return /\/v1$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
283
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Resolve the MiniMax voice_id for a TTS request.
|
|
287
|
+
* Priority: explicit voice param → upstream id (if it is not a model name) →
|
|
288
|
+
* model alias (if it is not a model name). MiniMax speech/music model ids
|
|
289
|
+
* (speech-2.8-hd, music-3.0, …) are never treated as voice ids.
|
|
290
|
+
*/
|
|
291
|
+
function resolveMiniMaxVoice(request) {
|
|
292
|
+
const explicit = request.voice?.trim();
|
|
293
|
+
if (explicit !== void 0 && explicit !== "") return explicit;
|
|
294
|
+
for (const candidate of [request.upstream, request.model]) {
|
|
295
|
+
const value = typeof candidate === "string" ? candidate.trim() : "";
|
|
296
|
+
if (value === "") continue;
|
|
297
|
+
if (/^(speech|music|t2a|tts)[-_]/i.test(value)) continue;
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Build the full MiniMax t2a_v2 body. Every official field is carried
|
|
303
|
+
* through — voice_setting (voice_id/speed/vol/pitch/emotion/text_normalization/
|
|
304
|
+
* latex_read), pronunciation_dict.tone, audio_setting (format/sample_rate/
|
|
305
|
+
* bitrate/channel/force_cbr), subtitle_enable, aigc_watermark, language_boost,
|
|
306
|
+
* voice_modify and timbre_weights — so callers and skills can reference them.
|
|
307
|
+
*/
|
|
308
|
+
function buildMiniMaxTTSBody(request, model, voiceId) {
|
|
309
|
+
const body = {
|
|
310
|
+
model,
|
|
311
|
+
text: request.prompt,
|
|
312
|
+
stream: false,
|
|
313
|
+
voice_setting: {
|
|
314
|
+
voice_id: voiceId,
|
|
315
|
+
speed: request.speed ?? 1,
|
|
316
|
+
vol: request.vol ?? 1,
|
|
317
|
+
pitch: request.pitch ?? 0,
|
|
318
|
+
...request.emotion !== void 0 && request.emotion.trim() !== "" ? { emotion: request.emotion.trim() } : {},
|
|
319
|
+
...request.textNormalization !== void 0 ? { text_normalization: request.textNormalization } : {},
|
|
320
|
+
...request.latexRead !== void 0 ? { latex_read: request.latexRead } : {}
|
|
321
|
+
},
|
|
322
|
+
audio_setting: {
|
|
323
|
+
format: request.format ?? "mp3",
|
|
324
|
+
sample_rate: request.sampleRate ?? 32e3,
|
|
325
|
+
bitrate: request.bitrate ?? 128e3,
|
|
326
|
+
channel: request.audioChannel ?? 1,
|
|
327
|
+
...request.forceCbr !== void 0 ? { force_cbr: request.forceCbr } : {}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
if (request.pronunciationTone !== void 0 && request.pronunciationTone.length > 0) body.pronunciation_dict = { tone: request.pronunciationTone };
|
|
331
|
+
if (request.subtitleEnable !== void 0) body.subtitle_enable = request.subtitleEnable;
|
|
332
|
+
if (request.aigcWatermark !== void 0) body.aigc_watermark = request.aigcWatermark;
|
|
333
|
+
if (request.languageBoost !== void 0 && request.languageBoost.trim() !== "") body.language_boost = request.languageBoost.trim();
|
|
334
|
+
if (request.voiceModify !== void 0) {
|
|
335
|
+
const modify = {};
|
|
336
|
+
if (request.voiceModify.pitch !== void 0) modify.pitch = request.voiceModify.pitch;
|
|
337
|
+
if (request.voiceModify.intensity !== void 0) modify.intensity = request.voiceModify.intensity;
|
|
338
|
+
if (request.voiceModify.timbre !== void 0) modify.timbre = request.voiceModify.timbre;
|
|
339
|
+
if (request.voiceModify.soundEffects !== void 0 && request.voiceModify.soundEffects.trim() !== "") modify.sound_effects = request.voiceModify.soundEffects.trim();
|
|
340
|
+
if (Object.keys(modify).length > 0) body.voice_modify = modify;
|
|
341
|
+
}
|
|
342
|
+
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) => ({
|
|
343
|
+
voice_id: item.voiceId.trim(),
|
|
344
|
+
weight: item.weight
|
|
345
|
+
}));
|
|
346
|
+
return body;
|
|
347
|
+
}
|
|
348
|
+
/** The MiniMax-specific fields only (model/text/stream excluded) — used as the
|
|
349
|
+
* new-api `metadata` payload when a gateway serves MiniMax TTS at /v1/audio/speech.
|
|
350
|
+
* The merge keeps the gateway-sent model/input, and voice_setting.voice_id is
|
|
351
|
+
* carried explicitly so relays that overwrite it still get the right voice. */
|
|
352
|
+
function buildMiniMaxTTSUpload(request, voiceId) {
|
|
353
|
+
const upload = buildMiniMaxTTSBody(request, "", voiceId);
|
|
354
|
+
delete upload.model;
|
|
355
|
+
delete upload.text;
|
|
356
|
+
delete upload.stream;
|
|
357
|
+
return upload;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* OpenAI-compatible MiniMax TTS path for New API style gateways that do not
|
|
361
|
+
* route the native /v1/t2a_v2. The full native field set is carried inside
|
|
362
|
+
* `metadata`, which new-api's MiniMax TTS relay merges into t2a_v2 upstream.
|
|
363
|
+
*/
|
|
364
|
+
async function minimaxTTSGateway(channel, request, signal, voiceId) {
|
|
365
|
+
const endpoint = `${minimaxApiBase(channel.apiUrl)}/audio/speech`;
|
|
366
|
+
const model = (request.upstream ?? request.model) || "speech-2.8-hd";
|
|
367
|
+
const metadata = buildMiniMaxTTSUpload(request, voiceId);
|
|
368
|
+
const body = {
|
|
369
|
+
model,
|
|
370
|
+
input: request.prompt,
|
|
371
|
+
voice: voiceId,
|
|
372
|
+
response_format: request.format ?? "mp3",
|
|
373
|
+
...request.speed !== void 0 ? { speed: request.speed } : {},
|
|
374
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {}
|
|
375
|
+
};
|
|
376
|
+
return normalizeAudioResponse(await fetchWithTimeout(endpoint, {
|
|
377
|
+
method: "POST",
|
|
378
|
+
redirect: "follow",
|
|
379
|
+
headers: {
|
|
380
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
381
|
+
"content-type": "application/json",
|
|
382
|
+
accept: "application/json, audio/mpeg"
|
|
383
|
+
},
|
|
384
|
+
body: JSON.stringify(body),
|
|
385
|
+
signal
|
|
386
|
+
}, UPSTREAM_TIMEOUT_MS), {
|
|
387
|
+
apiKey: channel.apiKey,
|
|
388
|
+
fallbackMime: "audio/mpeg"
|
|
389
|
+
});
|
|
390
|
+
}
|
|
284
391
|
async function minimax(channel, request, signal) {
|
|
285
392
|
const base = minimaxApiBase(channel.apiUrl);
|
|
286
393
|
const model = (request.upstream ?? request.model) || (request.mode === "music" ? "music-3.0" : "speech-2.8-hd");
|
|
287
|
-
const voice = request.voice ?? request.model ?? "";
|
|
288
394
|
if (request.mode === "voice_design") {
|
|
289
395
|
const endpoint = `${base}/voice_design`;
|
|
290
396
|
const body = {
|
|
@@ -317,40 +423,64 @@ async function minimax(channel, request, signal) {
|
|
|
317
423
|
...payload.voice_id === void 0 ? {} : { voiceId: payload.voice_id }
|
|
318
424
|
}];
|
|
319
425
|
}
|
|
320
|
-
let endpoint;
|
|
321
|
-
let body;
|
|
322
426
|
if (request.mode === "music") {
|
|
323
|
-
|
|
324
|
-
|
|
427
|
+
const MUSIC_FORMATS = /* @__PURE__ */ new Set([
|
|
428
|
+
"mp3",
|
|
429
|
+
"wav",
|
|
430
|
+
"pcm"
|
|
431
|
+
]);
|
|
432
|
+
const MUSIC_SAMPLE_RATES = /* @__PURE__ */ new Set([
|
|
433
|
+
16e3,
|
|
434
|
+
24e3,
|
|
435
|
+
32e3,
|
|
436
|
+
44100
|
|
437
|
+
]);
|
|
438
|
+
const MUSIC_BITRATES = /* @__PURE__ */ new Set([
|
|
439
|
+
32e3,
|
|
440
|
+
64e3,
|
|
441
|
+
128e3,
|
|
442
|
+
256e3
|
|
443
|
+
]);
|
|
444
|
+
const lyrics = request.lyrics?.trim() ?? "";
|
|
445
|
+
if (lyrics === "" && request.isInstrumental !== true) throw new AudioGenError("MiniMax 音乐生成需要歌词(lyrics 参数),或在「纯音乐」模式(is_instrumental=true)下生成;也可让面板/Agent 先为提示词创作一段歌词。", "lyrics-required");
|
|
446
|
+
const endpoint = `${base}/music_generation`;
|
|
447
|
+
const body = {
|
|
325
448
|
model,
|
|
326
449
|
prompt: request.prompt,
|
|
450
|
+
...lyrics === "" ? {} : { lyrics },
|
|
451
|
+
...request.isInstrumental !== void 0 ? { is_instrumental: request.isInstrumental } : {},
|
|
327
452
|
...request.duration !== void 0 ? { duration: request.duration } : {},
|
|
328
453
|
audio_setting: {
|
|
329
|
-
format: request.format ?? "mp3",
|
|
330
|
-
sample_rate: 44100,
|
|
331
|
-
bitrate: 256e3
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
} else {
|
|
335
|
-
endpoint = `${base}/t2a_v2`;
|
|
336
|
-
body = {
|
|
337
|
-
model,
|
|
338
|
-
text: request.prompt,
|
|
339
|
-
stream: false,
|
|
340
|
-
...voice === "" ? {} : { voice_setting: {
|
|
341
|
-
voice_id: voice,
|
|
342
|
-
...request.speed !== void 0 ? { speed: request.speed } : {},
|
|
343
|
-
vol: 1,
|
|
344
|
-
pitch: 0
|
|
345
|
-
} },
|
|
346
|
-
audio_setting: {
|
|
347
|
-
format: request.format ?? "mp3",
|
|
348
|
-
sample_rate: 32e3,
|
|
349
|
-
bitrate: 128e3
|
|
454
|
+
format: MUSIC_FORMATS.has(request.format ?? "mp3") ? request.format ?? "mp3" : "mp3",
|
|
455
|
+
sample_rate: MUSIC_SAMPLE_RATES.has(request.sampleRate ?? 44100) ? request.sampleRate ?? 44100 : 44100,
|
|
456
|
+
bitrate: MUSIC_BITRATES.has(request.bitrate ?? 256e3) ? request.bitrate ?? 256e3 : 256e3
|
|
350
457
|
}
|
|
351
458
|
};
|
|
459
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
redirect: "error",
|
|
462
|
+
headers: {
|
|
463
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
464
|
+
"content-type": "application/json",
|
|
465
|
+
accept: "application/json, audio/mpeg"
|
|
466
|
+
},
|
|
467
|
+
body: JSON.stringify(body),
|
|
468
|
+
signal
|
|
469
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
const detail = await response.text().catch(() => "");
|
|
472
|
+
throw new AudioGenError(`MiniMax music API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
473
|
+
}
|
|
474
|
+
return normalizeAudioResponse(response, {
|
|
475
|
+
apiKey: channel.apiKey,
|
|
476
|
+
fallbackMime: "audio/mpeg"
|
|
477
|
+
});
|
|
352
478
|
}
|
|
353
|
-
|
|
479
|
+
const voiceId = resolveMiniMaxVoice(request);
|
|
480
|
+
if (voiceId === void 0) throw new AudioGenError("MiniMax TTS 需要指定音色 voice_id(如 male-qn-qingse、female-shaonv):请在「音色」字段填写,或把音色加入渠道模型目录(alias 可任意、upstream 填 voice_id),也可点「获取可用模型」拉取账号音色列表。", "voice-required");
|
|
481
|
+
const endpoint = `${base}/t2a_v2`;
|
|
482
|
+
const body = buildMiniMaxTTSBody(request, model, voiceId);
|
|
483
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
354
484
|
method: "POST",
|
|
355
485
|
redirect: "error",
|
|
356
486
|
headers: {
|
|
@@ -360,10 +490,19 @@ async function minimax(channel, request, signal) {
|
|
|
360
490
|
},
|
|
361
491
|
body: JSON.stringify(body),
|
|
362
492
|
signal
|
|
363
|
-
}, UPSTREAM_TIMEOUT_MS)
|
|
493
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
494
|
+
if (response.ok) return normalizeAudioResponse(response, {
|
|
364
495
|
apiKey: channel.apiKey,
|
|
365
496
|
fallbackMime: "audio/mpeg"
|
|
366
497
|
});
|
|
498
|
+
const detail = await response.text().catch(() => "");
|
|
499
|
+
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");
|
|
500
|
+
try {
|
|
501
|
+
return await minimaxTTSGateway(channel, request, signal, voiceId);
|
|
502
|
+
} catch (gatewayError) {
|
|
503
|
+
const detailText = gatewayError instanceof AudioGenError ? gatewayError.message : String(gatewayError);
|
|
504
|
+
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");
|
|
505
|
+
}
|
|
367
506
|
}
|
|
368
507
|
async function stabilityAudio(channel, request, signal) {
|
|
369
508
|
const base = endpointBase(channel.apiUrl);
|
|
@@ -425,71 +564,21 @@ async function generateAudio(channel, request, signal) {
|
|
|
425
564
|
if (channel.apiKey.trim() === "") throw new AudioGenError("channel API key is not configured", "audio-no-key");
|
|
426
565
|
if (request.prompt.trim() === "") throw new AudioGenError("audio prompt/text is required", "audio-empty-prompt");
|
|
427
566
|
if (request.mode === "voice_design" && !isMiniMax$1(channel)) throw new AudioGenError("音色设计当前仅支持 MiniMax 渠道", "voice-design-unsupported");
|
|
428
|
-
if (isElevenLabs(channel)) return elevenLabs(channel, request, signal);
|
|
567
|
+
if (isElevenLabs$1(channel)) return elevenLabs(channel, request, signal);
|
|
429
568
|
if (isMiniMax$1(channel)) return minimax(channel, request, signal);
|
|
430
|
-
if (isStability(channel)) return stabilityAudio(channel, request, signal);
|
|
569
|
+
if (isStability$1(channel)) return stabilityAudio(channel, request, signal);
|
|
431
570
|
if (isOpenAICompatible(channel, request.mode)) return openAITTS(channel, request, signal);
|
|
432
571
|
return genericAudio(channel, request, signal);
|
|
433
572
|
}
|
|
434
573
|
//#endregion
|
|
435
574
|
//#region src/audio-presets.ts
|
|
436
575
|
const AUDIO_PRESETS = [
|
|
437
|
-
{
|
|
438
|
-
id: "openai-tts",
|
|
439
|
-
name: "OpenAI · TTS",
|
|
440
|
-
apiUrl: "https://api.openai.com/v1",
|
|
441
|
-
hint: "OpenAI 官方语音合成接口(/audio/speech)",
|
|
442
|
-
models: [
|
|
443
|
-
{
|
|
444
|
-
alias: "tts-1",
|
|
445
|
-
id: "tts-1",
|
|
446
|
-
category: "tts"
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
alias: "tts-1-hd",
|
|
450
|
-
id: "tts-1-hd",
|
|
451
|
-
category: "tts"
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
alias: "gpt-4o-mini-tts",
|
|
455
|
-
id: "gpt-4o-mini-tts",
|
|
456
|
-
category: "tts"
|
|
457
|
-
}
|
|
458
|
-
]
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
id: "elevenlabs",
|
|
462
|
-
name: "ElevenLabs",
|
|
463
|
-
apiUrl: "https://api.elevenlabs.io/v1",
|
|
464
|
-
hint: "ElevenLabs TTS;模型列表请填写你的 Voice ID(如 Rachel / Adam 等别名)",
|
|
465
|
-
models: [
|
|
466
|
-
{
|
|
467
|
-
alias: "Rachel",
|
|
468
|
-
id: "21m00Tcm4TlvDq8ikWAM",
|
|
469
|
-
category: "tts"
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
alias: "Adam",
|
|
473
|
-
id: "pNInz6obpgDQGcFmaJgB",
|
|
474
|
-
category: "tts"
|
|
475
|
-
},
|
|
476
|
-
{
|
|
477
|
-
alias: "Antoni",
|
|
478
|
-
id: "ErXwobaYiN019PkySvjV",
|
|
479
|
-
category: "tts"
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
alias: "Bella",
|
|
483
|
-
id: "EXAVITQu4vr4xnSDxMaL",
|
|
484
|
-
category: "tts"
|
|
485
|
-
}
|
|
486
|
-
]
|
|
487
|
-
},
|
|
488
576
|
{
|
|
489
577
|
id: "minimax",
|
|
490
578
|
name: "MiniMax",
|
|
491
579
|
apiUrl: "https://api.minimaxi.com",
|
|
492
|
-
|
|
580
|
+
site: "https://www.minimaxi.com",
|
|
581
|
+
hint: "MiniMax 官方音频:音色设计 / TTS / 音乐生成;建议点击「获取可用模型」拉取账号音色与模型",
|
|
493
582
|
models: [
|
|
494
583
|
{
|
|
495
584
|
alias: "speech-2.8-hd",
|
|
@@ -548,11 +637,56 @@ const AUDIO_PRESETS = [
|
|
|
548
637
|
}
|
|
549
638
|
]
|
|
550
639
|
},
|
|
640
|
+
{
|
|
641
|
+
id: "elevenlabs",
|
|
642
|
+
name: "ElevenLabs",
|
|
643
|
+
apiUrl: "https://api.elevenlabs.io/v1",
|
|
644
|
+
site: "https://elevenlabsai.cn",
|
|
645
|
+
hint: "ElevenLabs 语音合成(TTS);建议点击「获取可用模型」拉取音色与模型",
|
|
646
|
+
models: [
|
|
647
|
+
{
|
|
648
|
+
alias: "Rachel",
|
|
649
|
+
id: "21m00Tcm4TlvDq8ikWAM",
|
|
650
|
+
category: "tts"
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
alias: "Adam",
|
|
654
|
+
id: "pNInz6obpgDQGcFmaJgB",
|
|
655
|
+
category: "tts"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
alias: "Antoni",
|
|
659
|
+
id: "ErXwobaYiN019PkySvjV",
|
|
660
|
+
category: "tts"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
alias: "Bella",
|
|
664
|
+
id: "EXAVITQu4vr4xnSDxMaL",
|
|
665
|
+
category: "tts"
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
alias: "eleven_multilingual_v2",
|
|
669
|
+
id: "eleven_multilingual_v2",
|
|
670
|
+
category: "tts"
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
alias: "eleven_turbo_v2_5",
|
|
674
|
+
id: "eleven_turbo_v2_5",
|
|
675
|
+
category: "tts"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
alias: "eleven_flash_v2_5",
|
|
679
|
+
id: "eleven_flash_v2_5",
|
|
680
|
+
category: "tts"
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
},
|
|
551
684
|
{
|
|
552
685
|
id: "stability-audio",
|
|
553
|
-
name: "Stability AI
|
|
686
|
+
name: "Stability AI(stable-audio)",
|
|
554
687
|
apiUrl: "https://api.stability.ai/v2beta/audio",
|
|
555
|
-
|
|
688
|
+
site: "https://stability.ai/stable-audio",
|
|
689
|
+
hint: "Stability AI 音乐 / 音效生成(stable-audio 系列)",
|
|
556
690
|
models: [{
|
|
557
691
|
alias: "stable-audio-2.0",
|
|
558
692
|
id: "stable-audio-2.0",
|
|
@@ -562,13 +696,6 @@ const AUDIO_PRESETS = [
|
|
|
562
696
|
id: "stable-audio-1.0",
|
|
563
697
|
category: "music"
|
|
564
698
|
}]
|
|
565
|
-
},
|
|
566
|
-
{
|
|
567
|
-
id: "custom",
|
|
568
|
-
name: "自定义渠道",
|
|
569
|
-
apiUrl: "",
|
|
570
|
-
hint: "任意兼容接口;支持 OpenAI 兼容 TTS,或返回音频字节 / JSON 的通用 POST",
|
|
571
|
-
models: []
|
|
572
699
|
}
|
|
573
700
|
];
|
|
574
701
|
/** Look up one built-in provider by id. */
|
|
@@ -580,17 +707,32 @@ function audioPresetById(id) {
|
|
|
580
707
|
function isMiniMax(channel) {
|
|
581
708
|
return channel.preset === "minimax" || /minimax/i.test(channel.apiUrl);
|
|
582
709
|
}
|
|
710
|
+
function isElevenLabs(channel) {
|
|
711
|
+
return channel.preset === "elevenlabs" || /elevenlabs/i.test(channel.apiUrl);
|
|
712
|
+
}
|
|
713
|
+
function isStability(channel) {
|
|
714
|
+
return channel.preset === "stability" || /stability\.ai/i.test(channel.apiUrl);
|
|
715
|
+
}
|
|
583
716
|
function baseUrl(url) {
|
|
584
717
|
return url.trim().replace(/\/+$/, "");
|
|
585
718
|
}
|
|
719
|
+
/** Whether an upstream model id is audio-related at all. */
|
|
586
720
|
function categoryFor(id) {
|
|
587
721
|
const value = id.toLowerCase();
|
|
588
|
-
if (/(tts|speech|voice|t2a)/i.test(value)) return "tts";
|
|
589
|
-
if (/(music|song|cover|lyrics)/i.test(value)) return "music";
|
|
722
|
+
if (/(tts|speech|voice|t2a|talk|narration)/i.test(value)) return "tts";
|
|
723
|
+
if (/(music|song|cover|lyrics|audio|melody|beat)/i.test(value)) return "music";
|
|
590
724
|
if (/(sfx|sound.?effect|effect|foley)/i.test(value)) return "sfx";
|
|
591
725
|
}
|
|
726
|
+
async function fetchJson(url, init) {
|
|
727
|
+
const response = await fetch(url, init);
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
const text = await response.text().catch(() => "");
|
|
730
|
+
throw new Error(`HTTP ${response.status}${text === "" ? "" : `: ${text.slice(0, 300)}`}`);
|
|
731
|
+
}
|
|
732
|
+
return response.json();
|
|
733
|
+
}
|
|
592
734
|
async function postJson(url, apiKey, body) {
|
|
593
|
-
|
|
735
|
+
return fetchJson(url, {
|
|
594
736
|
method: "POST",
|
|
595
737
|
headers: {
|
|
596
738
|
authorization: `Bearer ${apiKey.trim()}`,
|
|
@@ -598,18 +740,20 @@ async function postJson(url, apiKey, body) {
|
|
|
598
740
|
},
|
|
599
741
|
body: JSON.stringify(body)
|
|
600
742
|
});
|
|
601
|
-
if (!response.ok) {
|
|
602
|
-
const text = await response.text().catch(() => "");
|
|
603
|
-
throw new Error(`HTTP ${response.status}${text === "" ? "" : `: ${text.slice(0, 300)}`}`);
|
|
604
|
-
}
|
|
605
|
-
return response.json();
|
|
606
743
|
}
|
|
607
744
|
/** Discover available models/voices for a channel. */
|
|
608
745
|
async function discoverAudioModels(channel) {
|
|
609
746
|
if (channel.apiUrl.trim() === "") throw new Error("API URL is not configured");
|
|
610
747
|
if (channel.apiKey.trim() === "") throw new Error("API key is not configured");
|
|
611
|
-
if (isMiniMax(channel))
|
|
612
|
-
|
|
748
|
+
if (isMiniMax(channel)) return discoverMiniMax(channel);
|
|
749
|
+
if (isElevenLabs(channel)) return discoverElevenLabs(channel);
|
|
750
|
+
if (isStability(channel)) return discoverStability(channel);
|
|
751
|
+
return discoverOpenAICompatible(channel);
|
|
752
|
+
}
|
|
753
|
+
async function discoverMiniMax(channel) {
|
|
754
|
+
const url = `${baseUrl(channel.apiUrl).replace(/\/v1$/i, "")}/v1/get_voice`;
|
|
755
|
+
try {
|
|
756
|
+
const payload = await postJson(url, channel.apiKey, { voice_type: "all" });
|
|
613
757
|
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}`);
|
|
614
758
|
const models = [];
|
|
615
759
|
for (const voice of payload.system_voice ?? []) {
|
|
@@ -649,18 +793,80 @@ async function discoverAudioModels(channel) {
|
|
|
649
793
|
});
|
|
650
794
|
return {
|
|
651
795
|
models: dedupe(models),
|
|
652
|
-
source: "MiniMax get_voice +
|
|
796
|
+
source: "MiniMax get_voice + music 目录"
|
|
797
|
+
};
|
|
798
|
+
} catch (error) {
|
|
799
|
+
const fallback = (audioPresetById("minimax")?.models ?? []).map((model) => ({ ...model }));
|
|
800
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
801
|
+
return {
|
|
802
|
+
models: dedupe(fallback),
|
|
803
|
+
source: `内置 MiniMax 目录(音色发现失败:${message.slice(0, 160)})`
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async function discoverElevenLabs(channel) {
|
|
808
|
+
const base = baseUrl(channel.apiUrl);
|
|
809
|
+
const headers = { "xi-api-key": channel.apiKey.trim() };
|
|
810
|
+
const failures = [];
|
|
811
|
+
const models = [];
|
|
812
|
+
try {
|
|
813
|
+
const payload = await fetchJson(`${base}/models`, { headers });
|
|
814
|
+
for (const item of Array.isArray(payload) ? payload : []) {
|
|
815
|
+
const id = item.model_id?.trim() ?? "";
|
|
816
|
+
if (id === "") continue;
|
|
817
|
+
if (item.capabilities?.text_to_speech !== true && item.capabilities?.voice_change !== true) continue;
|
|
818
|
+
models.push({
|
|
819
|
+
alias: item.name?.trim() || id,
|
|
820
|
+
id,
|
|
821
|
+
category: "tts",
|
|
822
|
+
...item.description !== void 0 && item.description.trim() !== "" ? { description: item.description.trim() } : {}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
} catch (error) {
|
|
826
|
+
failures.push(`模型列表:${error instanceof Error ? error.message : String(error)}`);
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
const payload = await fetchJson(`${base}/voices`, { headers });
|
|
830
|
+
for (const voice of Array.isArray(payload?.voices) ? payload.voices : []) {
|
|
831
|
+
const id = voice.voice_id?.trim() ?? "";
|
|
832
|
+
if (id === "") continue;
|
|
833
|
+
models.push({
|
|
834
|
+
alias: voice.name?.trim() || id,
|
|
835
|
+
id,
|
|
836
|
+
category: "tts",
|
|
837
|
+
...voice.description !== void 0 && voice.description.trim() !== "" ? { description: voice.description.trim() } : {}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
failures.push(`音色列表:${error instanceof Error ? error.message : String(error)}`);
|
|
842
|
+
}
|
|
843
|
+
if (models.length === 0) {
|
|
844
|
+
const fallback = (audioPresetById("elevenlabs")?.models ?? []).map((model) => ({ ...model }));
|
|
845
|
+
const detail = failures.length === 0 ? "" : `(发现失败:${failures.join(";").slice(0, 160)})`;
|
|
846
|
+
return {
|
|
847
|
+
models: dedupe(fallback),
|
|
848
|
+
source: `内置 ElevenLabs 目录${detail}`
|
|
653
849
|
};
|
|
654
850
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
851
|
+
return {
|
|
852
|
+
models: dedupe(models),
|
|
853
|
+
source: "ElevenLabs /models + /voices"
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
async function discoverStability(channel) {
|
|
857
|
+
return {
|
|
858
|
+
models: dedupe((audioPresetById("stability-audio")?.models ?? []).map((model) => ({ ...model }))),
|
|
859
|
+
source: "Stability stable-audio 内置目录"
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
async function discoverOpenAICompatible(channel) {
|
|
863
|
+
const payload = await fetchJson(`${baseUrl(channel.apiUrl)}/models`, { headers: { authorization: `Bearer ${channel.apiKey.trim()}` } });
|
|
659
864
|
const models = [];
|
|
660
865
|
for (const item of payload.data ?? []) {
|
|
661
866
|
const id = item.id?.trim() ?? "";
|
|
662
867
|
if (id === "") continue;
|
|
663
|
-
const category = categoryFor(id)
|
|
868
|
+
const category = categoryFor(id);
|
|
869
|
+
if (category === void 0) continue;
|
|
664
870
|
models.push({
|
|
665
871
|
alias: id,
|
|
666
872
|
id,
|
|
@@ -669,7 +875,7 @@ async function discoverAudioModels(channel) {
|
|
|
669
875
|
}
|
|
670
876
|
return {
|
|
671
877
|
models: dedupe(models),
|
|
672
|
-
source: "OpenAI-compatible /models"
|
|
878
|
+
source: "OpenAI-compatible /models(仅音频相关)"
|
|
673
879
|
};
|
|
674
880
|
}
|
|
675
881
|
function dedupe(models) {
|
|
@@ -847,16 +1053,48 @@ function parseGenerateRequest(body) {
|
|
|
847
1053
|
const mode = body.mode === "music" ? "music" : body.mode === "sfx" ? "sfx" : body.mode === "voice_design" ? "voice_design" : "tts";
|
|
848
1054
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
849
1055
|
if (prompt === "") return void 0;
|
|
1056
|
+
const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1057
|
+
const str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1058
|
+
const flag = (value) => typeof value === "boolean" ? value : void 0;
|
|
1059
|
+
const tone = Array.isArray(body.pronunciationTone) ? body.pronunciationTone.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()) : void 0;
|
|
1060
|
+
const voiceModifyRaw = body.voiceModify;
|
|
1061
|
+
const voiceModify = typeof voiceModifyRaw === "object" && voiceModifyRaw !== null ? {
|
|
1062
|
+
...num(voiceModifyRaw.pitch) !== void 0 ? { pitch: num(voiceModifyRaw.pitch) } : {},
|
|
1063
|
+
...num(voiceModifyRaw.intensity) !== void 0 ? { intensity: num(voiceModifyRaw.intensity) } : {},
|
|
1064
|
+
...num(voiceModifyRaw.timbre) !== void 0 ? { timbre: num(voiceModifyRaw.timbre) } : {},
|
|
1065
|
+
...str(voiceModifyRaw.soundEffects) !== void 0 ? { soundEffects: str(voiceModifyRaw.soundEffects) } : {}
|
|
1066
|
+
} : void 0;
|
|
1067
|
+
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) => ({
|
|
1068
|
+
voiceId: item.voiceId.trim(),
|
|
1069
|
+
weight: item.weight
|
|
1070
|
+
})).filter((item) => item.voiceId !== "") : void 0;
|
|
850
1071
|
return {
|
|
851
1072
|
mode,
|
|
852
1073
|
model: typeof body.model === "string" ? body.model.trim() : "",
|
|
853
1074
|
prompt,
|
|
854
1075
|
...typeof body.voice === "string" && body.voice.trim() !== "" ? { voice: body.voice.trim() } : {},
|
|
855
1076
|
...typeof body.previewText === "string" && body.previewText.trim() !== "" ? { previewText: body.previewText.trim() } : {},
|
|
856
|
-
...
|
|
857
|
-
...
|
|
1077
|
+
...num(body.speed) !== void 0 ? { speed: num(body.speed) } : {},
|
|
1078
|
+
...num(body.duration) !== void 0 ? { duration: num(body.duration) } : {},
|
|
1079
|
+
...typeof body.lyrics === "string" && body.lyrics.trim() !== "" ? { lyrics: body.lyrics.trim() } : {},
|
|
1080
|
+
...typeof body.isInstrumental === "boolean" ? { isInstrumental: body.isInstrumental } : {},
|
|
858
1081
|
...typeof body.format === "string" && body.format.trim() !== "" ? { format: body.format.trim() } : {},
|
|
859
|
-
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {}
|
|
1082
|
+
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
1083
|
+
...str(body.emotion) !== void 0 ? { emotion: str(body.emotion) } : {},
|
|
1084
|
+
...num(body.vol) !== void 0 ? { vol: num(body.vol) } : {},
|
|
1085
|
+
...num(body.pitch) !== void 0 ? { pitch: num(body.pitch) } : {},
|
|
1086
|
+
...flag(body.textNormalization) !== void 0 ? { textNormalization: flag(body.textNormalization) } : {},
|
|
1087
|
+
...flag(body.latexRead) !== void 0 ? { latexRead: flag(body.latexRead) } : {},
|
|
1088
|
+
...tone !== void 0 && tone.length > 0 ? { pronunciationTone: tone } : {},
|
|
1089
|
+
...num(body.sampleRate) !== void 0 ? { sampleRate: num(body.sampleRate) } : {},
|
|
1090
|
+
...num(body.bitrate) !== void 0 ? { bitrate: num(body.bitrate) } : {},
|
|
1091
|
+
...num(body.audioChannel) !== void 0 ? { audioChannel: num(body.audioChannel) } : {},
|
|
1092
|
+
...flag(body.forceCbr) !== void 0 ? { forceCbr: flag(body.forceCbr) } : {},
|
|
1093
|
+
...flag(body.subtitleEnable) !== void 0 ? { subtitleEnable: flag(body.subtitleEnable) } : {},
|
|
1094
|
+
...flag(body.aigcWatermark) !== void 0 ? { aigcWatermark: flag(body.aigcWatermark) } : {},
|
|
1095
|
+
...str(body.languageBoost) !== void 0 ? { languageBoost: str(body.languageBoost) } : {},
|
|
1096
|
+
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
1097
|
+
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
860
1098
|
};
|
|
861
1099
|
}
|
|
862
1100
|
function toView(descriptor) {
|
|
@@ -997,7 +1235,7 @@ function makeRoutes(deps) {
|
|
|
997
1235
|
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];
|
|
998
1236
|
const channel = {
|
|
999
1237
|
id: stored?.id ?? "preview",
|
|
1000
|
-
preset: stored?.preset ?? "",
|
|
1238
|
+
preset: typeof body?.preset === "string" ? body.preset.trim() : stored?.preset ?? "",
|
|
1001
1239
|
name: stored?.name ?? "",
|
|
1002
1240
|
apiUrl: typeof body?.apiUrl === "string" && body.apiUrl.trim() !== "" ? body.apiUrl.trim() : stored?.apiUrl ?? "",
|
|
1003
1241
|
apiKey: typeof body?.apiKey === "string" && body.apiKey.trim() !== "" ? body.apiKey.trim() : stored?.apiKey ?? "",
|
|
@@ -1364,7 +1602,7 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1364
1602
|
},
|
|
1365
1603
|
voice: {
|
|
1366
1604
|
type: "string",
|
|
1367
|
-
description: "Optional voice id/name for TTS providers."
|
|
1605
|
+
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."
|
|
1368
1606
|
},
|
|
1369
1607
|
preview_text: {
|
|
1370
1608
|
type: "string",
|
|
@@ -1372,15 +1610,112 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1372
1610
|
},
|
|
1373
1611
|
speed: {
|
|
1374
1612
|
type: "number",
|
|
1375
|
-
description: "Optional speaking rate / speed multiplier where supported."
|
|
1613
|
+
description: "Optional speaking rate / speed multiplier where supported. MiniMax range 0.5-2.0 (default 1)."
|
|
1376
1614
|
},
|
|
1377
1615
|
duration: {
|
|
1378
1616
|
type: "number",
|
|
1379
1617
|
description: "Requested duration in seconds for music/sfx."
|
|
1380
1618
|
},
|
|
1619
|
+
lyrics: {
|
|
1620
|
+
type: "string",
|
|
1621
|
+
description: "Lyrics for music generation (MiniMax music-3.0/music-cover). Required unless is_instrumental is true. Split verses with an empty line."
|
|
1622
|
+
},
|
|
1623
|
+
is_instrumental: {
|
|
1624
|
+
type: "boolean",
|
|
1625
|
+
description: "Generate purely instrumental music without vocals/lyrics (MiniMax is_instrumental). When true, lyrics may be omitted."
|
|
1626
|
+
},
|
|
1381
1627
|
format: {
|
|
1382
1628
|
type: "string",
|
|
1383
|
-
description: "Output format such as mp3 or wav."
|
|
1629
|
+
description: "Output format such as mp3 or wav. MiniMax music supports mp3/wav/pcm."
|
|
1630
|
+
},
|
|
1631
|
+
emotion: {
|
|
1632
|
+
type: "string",
|
|
1633
|
+
description: "MiniMax TTS emotion, e.g. happy/sad/angry/nervous/fearful/bored (voice_setting.emotion)."
|
|
1634
|
+
},
|
|
1635
|
+
vol: {
|
|
1636
|
+
type: "number",
|
|
1637
|
+
description: "MiniMax TTS volume 0-10, default 1 (voice_setting.vol)."
|
|
1638
|
+
},
|
|
1639
|
+
pitch: {
|
|
1640
|
+
type: "integer",
|
|
1641
|
+
description: "MiniMax TTS pitch shift -12..12 semitones, default 0 (voice_setting.pitch)."
|
|
1642
|
+
},
|
|
1643
|
+
text_normalization: {
|
|
1644
|
+
type: "boolean",
|
|
1645
|
+
description: "MiniMax TTS text normalization switch (voice_setting.text_normalization)."
|
|
1646
|
+
},
|
|
1647
|
+
latex_read: {
|
|
1648
|
+
type: "boolean",
|
|
1649
|
+
description: "MiniMax TTS math formula reading switch (voice_setting.latex_read)."
|
|
1650
|
+
},
|
|
1651
|
+
pronunciation_tone: {
|
|
1652
|
+
type: "array",
|
|
1653
|
+
items: { type: "string" },
|
|
1654
|
+
description: "MiniMax TTS pronunciation dictionary tone entries, each \"word/pronunciation\", e.g. [\"处理/(chu3)(li3)\", \"危险/dangerous\"] (pronunciation_dict.tone)."
|
|
1655
|
+
},
|
|
1656
|
+
sample_rate: {
|
|
1657
|
+
type: "integer",
|
|
1658
|
+
description: "MiniMax TTS sample rate: 16000/24000/32000/44100/48000, default 32000 (audio_setting.sample_rate)."
|
|
1659
|
+
},
|
|
1660
|
+
bitrate: {
|
|
1661
|
+
type: "integer",
|
|
1662
|
+
description: "MiniMax TTS bitrate in bps: 64000-320000, default 128000 (audio_setting.bitrate)."
|
|
1663
|
+
},
|
|
1664
|
+
channel: {
|
|
1665
|
+
type: "integer",
|
|
1666
|
+
description: "MiniMax TTS audio channels: 1 or 2, default 1 (audio_setting.channel)."
|
|
1667
|
+
},
|
|
1668
|
+
force_cbr: {
|
|
1669
|
+
type: "boolean",
|
|
1670
|
+
description: "MiniMax TTS force CBR encoding (audio_setting.force_cbr)."
|
|
1671
|
+
},
|
|
1672
|
+
subtitle_enable: {
|
|
1673
|
+
type: "boolean",
|
|
1674
|
+
description: "MiniMax TTS subtitle output switch (subtitle_enable)."
|
|
1675
|
+
},
|
|
1676
|
+
aigc_watermark: {
|
|
1677
|
+
type: "boolean",
|
|
1678
|
+
description: "MiniMax TTS AIGC watermark switch (aigc_watermark)."
|
|
1679
|
+
},
|
|
1680
|
+
language_boost: {
|
|
1681
|
+
type: "string",
|
|
1682
|
+
description: "MiniMax TTS language boost, e.g. 中英混读 (language_boost, model-dependent)."
|
|
1683
|
+
},
|
|
1684
|
+
voice_modify: {
|
|
1685
|
+
type: "object",
|
|
1686
|
+
additionalProperties: false,
|
|
1687
|
+
properties: {
|
|
1688
|
+
pitch: {
|
|
1689
|
+
type: "integer",
|
|
1690
|
+
description: "Pitch shift for voice modification."
|
|
1691
|
+
},
|
|
1692
|
+
intensity: {
|
|
1693
|
+
type: "integer",
|
|
1694
|
+
description: "Intensity for voice modification."
|
|
1695
|
+
},
|
|
1696
|
+
timbre: {
|
|
1697
|
+
type: "integer",
|
|
1698
|
+
description: "Timbre shift for voice modification."
|
|
1699
|
+
},
|
|
1700
|
+
sound_effects: {
|
|
1701
|
+
type: "string",
|
|
1702
|
+
description: "Sound effect for voice modification, e.g. 耳语."
|
|
1703
|
+
}
|
|
1704
|
+
},
|
|
1705
|
+
description: "MiniMax TTS voice modification (voice_modify, supported by speech-2.8+)."
|
|
1706
|
+
},
|
|
1707
|
+
timbre_weights: {
|
|
1708
|
+
type: "array",
|
|
1709
|
+
items: {
|
|
1710
|
+
type: "object",
|
|
1711
|
+
additionalProperties: false,
|
|
1712
|
+
properties: {
|
|
1713
|
+
voice_id: { type: "string" },
|
|
1714
|
+
weight: { type: "integer" }
|
|
1715
|
+
},
|
|
1716
|
+
required: ["voice_id", "weight"]
|
|
1717
|
+
},
|
|
1718
|
+
description: "MiniMax TTS dual-voice blend weights (timbre_weights)."
|
|
1384
1719
|
}
|
|
1385
1720
|
},
|
|
1386
1721
|
output: {
|
|
@@ -1403,6 +1738,19 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1403
1738
|
upstream: ""
|
|
1404
1739
|
};
|
|
1405
1740
|
})() : resolveModel(config, args.model);
|
|
1741
|
+
const voiceModify = typeof args.voice_modify === "object" && args.voice_modify !== null ? (() => {
|
|
1742
|
+
const raw = args.voice_modify;
|
|
1743
|
+
const out = {};
|
|
1744
|
+
if (typeof raw.pitch === "number") out.pitch = raw.pitch;
|
|
1745
|
+
if (typeof raw.intensity === "number") out.intensity = raw.intensity;
|
|
1746
|
+
if (typeof raw.timbre === "number") out.timbre = raw.timbre;
|
|
1747
|
+
if (typeof raw.sound_effects === "string" && raw.sound_effects.trim() !== "") out.soundEffects = raw.sound_effects.trim();
|
|
1748
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1749
|
+
})() : void 0;
|
|
1750
|
+
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) => ({
|
|
1751
|
+
voiceId: item.voice_id.trim(),
|
|
1752
|
+
weight: item.weight
|
|
1753
|
+
})).filter((item) => item.voiceId !== "") : void 0;
|
|
1406
1754
|
const request = {
|
|
1407
1755
|
mode,
|
|
1408
1756
|
model: picked.alias,
|
|
@@ -1414,7 +1762,24 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1414
1762
|
...typeof args.preview_text === "string" && args.preview_text.trim() !== "" ? { previewText: args.preview_text.trim() } : {},
|
|
1415
1763
|
...typeof args.speed === "number" ? { speed: args.speed } : {},
|
|
1416
1764
|
...typeof args.duration === "number" ? { duration: args.duration } : {},
|
|
1417
|
-
...typeof args.
|
|
1765
|
+
...typeof args.lyrics === "string" && args.lyrics.trim() !== "" ? { lyrics: args.lyrics.trim() } : {},
|
|
1766
|
+
...typeof args.is_instrumental === "boolean" ? { isInstrumental: args.is_instrumental } : {},
|
|
1767
|
+
...typeof args.format === "string" && args.format.trim() !== "" ? { format: args.format.trim() } : {},
|
|
1768
|
+
...typeof args.emotion === "string" && args.emotion.trim() !== "" ? { emotion: args.emotion.trim() } : {},
|
|
1769
|
+
...typeof args.vol === "number" && Number.isFinite(args.vol) ? { vol: args.vol } : {},
|
|
1770
|
+
...typeof args.pitch === "number" && Number.isFinite(args.pitch) ? { pitch: args.pitch } : {},
|
|
1771
|
+
...typeof args.text_normalization === "boolean" ? { textNormalization: args.text_normalization } : {},
|
|
1772
|
+
...typeof args.latex_read === "boolean" ? { latexRead: args.latex_read } : {},
|
|
1773
|
+
...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()) } : {},
|
|
1774
|
+
...typeof args.sample_rate === "number" && Number.isFinite(args.sample_rate) ? { sampleRate: args.sample_rate } : {},
|
|
1775
|
+
...typeof args.bitrate === "number" && Number.isFinite(args.bitrate) ? { bitrate: args.bitrate } : {},
|
|
1776
|
+
...typeof args.channel === "number" && Number.isFinite(args.channel) ? { audioChannel: args.channel } : {},
|
|
1777
|
+
...typeof args.force_cbr === "boolean" ? { forceCbr: args.force_cbr } : {},
|
|
1778
|
+
...typeof args.subtitle_enable === "boolean" ? { subtitleEnable: args.subtitle_enable } : {},
|
|
1779
|
+
...typeof args.aigc_watermark === "boolean" ? { aigcWatermark: args.aigc_watermark } : {},
|
|
1780
|
+
...typeof args.language_boost === "string" && args.language_boost.trim() !== "" ? { languageBoost: args.language_boost.trim() } : {},
|
|
1781
|
+
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
1782
|
+
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
1418
1783
|
};
|
|
1419
1784
|
try {
|
|
1420
1785
|
const outputs = await generateAudio(picked.channel, request, exec.signal);
|