dsh-audiogen 0.3.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/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,10 +281,115 @@ 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
- const voice = request.voice ?? request.model ?? "";
288
393
  if (request.mode === "voice_design") {
289
394
  const endpoint = `${base}/voice_design`;
290
395
  const body = {
@@ -317,11 +422,9 @@ async function minimax(channel, request, signal) {
317
422
  ...payload.voice_id === void 0 ? {} : { voiceId: payload.voice_id }
318
423
  }];
319
424
  }
320
- let endpoint;
321
- let body;
322
425
  if (request.mode === "music") {
323
- endpoint = `${base}/music_generation`;
324
- body = {
426
+ const endpoint = `${base}/music_generation`;
427
+ const body = {
325
428
  model,
326
429
  prompt: request.prompt,
327
430
  ...request.duration !== void 0 ? { duration: request.duration } : {},
@@ -331,26 +434,31 @@ async function minimax(channel, request, signal) {
331
434
  bitrate: 256e3
332
435
  }
333
436
  };
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
350
- }
351
- };
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
+ });
352
456
  }
353
- return normalizeAudioResponse(await fetchWithTimeout(endpoint, {
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, {
354
462
  method: "POST",
355
463
  redirect: "error",
356
464
  headers: {
@@ -360,10 +468,19 @@ async function minimax(channel, request, signal) {
360
468
  },
361
469
  body: JSON.stringify(body),
362
470
  signal
363
- }, UPSTREAM_TIMEOUT_MS), {
471
+ }, UPSTREAM_TIMEOUT_MS);
472
+ if (response.ok) return normalizeAudioResponse(response, {
364
473
  apiKey: channel.apiKey,
365
474
  fallbackMime: "audio/mpeg"
366
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
+ }
367
484
  }
368
485
  async function stabilityAudio(channel, request, signal) {
369
486
  const base = endpointBase(channel.apiUrl);
@@ -425,71 +542,21 @@ async function generateAudio(channel, request, signal) {
425
542
  if (channel.apiKey.trim() === "") throw new AudioGenError("channel API key is not configured", "audio-no-key");
426
543
  if (request.prompt.trim() === "") throw new AudioGenError("audio prompt/text is required", "audio-empty-prompt");
427
544
  if (request.mode === "voice_design" && !isMiniMax$1(channel)) throw new AudioGenError("音色设计当前仅支持 MiniMax 渠道", "voice-design-unsupported");
428
- if (isElevenLabs(channel)) return elevenLabs(channel, request, signal);
545
+ if (isElevenLabs$1(channel)) return elevenLabs(channel, request, signal);
429
546
  if (isMiniMax$1(channel)) return minimax(channel, request, signal);
430
- if (isStability(channel)) return stabilityAudio(channel, request, signal);
547
+ if (isStability$1(channel)) return stabilityAudio(channel, request, signal);
431
548
  if (isOpenAICompatible(channel, request.mode)) return openAITTS(channel, request, signal);
432
549
  return genericAudio(channel, request, signal);
433
550
  }
434
551
  //#endregion
435
552
  //#region src/audio-presets.ts
436
553
  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
554
  {
489
555
  id: "minimax",
490
556
  name: "MiniMax",
491
557
  apiUrl: "https://api.minimaxi.com",
492
- hint: "MiniMax 音色设计 / TTS / 音乐生成;可使用“获取可用模型”拉取账号音色",
558
+ site: "https://www.minimaxi.com",
559
+ hint: "MiniMax 官方音频:音色设计 / TTS / 音乐生成;建议点击「获取可用模型」拉取账号音色与模型",
493
560
  models: [
494
561
  {
495
562
  alias: "speech-2.8-hd",
@@ -548,11 +615,56 @@ const AUDIO_PRESETS = [
548
615
  }
549
616
  ]
550
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
+ },
551
662
  {
552
663
  id: "stability-audio",
553
- name: "Stability AI · 音频",
664
+ name: "Stability AI(stable-audio)",
554
665
  apiUrl: "https://api.stability.ai/v2beta/audio",
555
- hint: "Stability AI 音乐/音效生成(stable-audio 系列)",
666
+ site: "https://stability.ai/stable-audio",
667
+ hint: "Stability AI 音乐 / 音效生成(stable-audio 系列)",
556
668
  models: [{
557
669
  alias: "stable-audio-2.0",
558
670
  id: "stable-audio-2.0",
@@ -562,13 +674,6 @@ const AUDIO_PRESETS = [
562
674
  id: "stable-audio-1.0",
563
675
  category: "music"
564
676
  }]
565
- },
566
- {
567
- id: "custom",
568
- name: "自定义渠道",
569
- apiUrl: "",
570
- hint: "任意兼容接口;支持 OpenAI 兼容 TTS,或返回音频字节 / JSON 的通用 POST",
571
- models: []
572
677
  }
573
678
  ];
574
679
  /** Look up one built-in provider by id. */
@@ -580,17 +685,32 @@ function audioPresetById(id) {
580
685
  function isMiniMax(channel) {
581
686
  return channel.preset === "minimax" || /minimax/i.test(channel.apiUrl);
582
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
+ }
583
694
  function baseUrl(url) {
584
695
  return url.trim().replace(/\/+$/, "");
585
696
  }
697
+ /** Whether an upstream model id is audio-related at all. */
586
698
  function categoryFor(id) {
587
699
  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";
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";
590
702
  if (/(sfx|sound.?effect|effect|foley)/i.test(value)) return "sfx";
591
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
+ }
592
712
  async function postJson(url, apiKey, body) {
593
- const response = await fetch(url, {
713
+ return fetchJson(url, {
594
714
  method: "POST",
595
715
  headers: {
596
716
  authorization: `Bearer ${apiKey.trim()}`,
@@ -598,18 +718,20 @@ async function postJson(url, apiKey, body) {
598
718
  },
599
719
  body: JSON.stringify(body)
600
720
  });
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
721
  }
607
722
  /** Discover available models/voices for a channel. */
608
723
  async function discoverAudioModels(channel) {
609
724
  if (channel.apiUrl.trim() === "") throw new Error("API URL is not configured");
610
725
  if (channel.apiKey.trim() === "") throw new Error("API key is not configured");
611
- if (isMiniMax(channel)) {
612
- const payload = await postJson(`${baseUrl(channel.apiUrl).replace(/\/v1$/i, "")}/v1/get_voice`, channel.apiKey, { voice_type: "all" });
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" });
613
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}`);
614
736
  const models = [];
615
737
  for (const voice of payload.system_voice ?? []) {
@@ -649,18 +771,80 @@ async function discoverAudioModels(channel) {
649
771
  });
650
772
  return {
651
773
  models: dedupe(models),
652
- source: "MiniMax get_voice + built-in music catalog"
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)})`
653
782
  };
654
783
  }
655
- const url = `${baseUrl(channel.apiUrl)}/models`;
656
- const response = await fetch(url, { headers: { authorization: `Bearer ${channel.apiKey.trim()}` } });
657
- if (!response.ok) throw new Error(`model list request failed (HTTP ${response.status}); please add models manually`);
658
- const payload = await response.json();
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()}` } });
659
842
  const models = [];
660
843
  for (const item of payload.data ?? []) {
661
844
  const id = item.id?.trim() ?? "";
662
845
  if (id === "") continue;
663
- const category = categoryFor(id) ?? "tts";
846
+ const category = categoryFor(id);
847
+ if (category === void 0) continue;
664
848
  models.push({
665
849
  alias: id,
666
850
  id,
@@ -669,7 +853,7 @@ async function discoverAudioModels(channel) {
669
853
  }
670
854
  return {
671
855
  models: dedupe(models),
672
- source: "OpenAI-compatible /models"
856
+ source: "OpenAI-compatible /models(仅音频相关)"
673
857
  };
674
858
  }
675
859
  function dedupe(models) {
@@ -847,16 +1031,46 @@ function parseGenerateRequest(body) {
847
1031
  const mode = body.mode === "music" ? "music" : body.mode === "sfx" ? "sfx" : body.mode === "voice_design" ? "voice_design" : "tts";
848
1032
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
849
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;
850
1049
  return {
851
1050
  mode,
852
1051
  model: typeof body.model === "string" ? body.model.trim() : "",
853
1052
  prompt,
854
1053
  ...typeof body.voice === "string" && body.voice.trim() !== "" ? { voice: body.voice.trim() } : {},
855
1054
  ...typeof body.previewText === "string" && body.previewText.trim() !== "" ? { previewText: body.previewText.trim() } : {},
856
- ...typeof body.speed === "number" ? { speed: body.speed } : {},
857
- ...typeof body.duration === "number" ? { duration: body.duration } : {},
1055
+ ...num(body.speed) !== void 0 ? { speed: num(body.speed) } : {},
1056
+ ...num(body.duration) !== void 0 ? { duration: num(body.duration) } : {},
858
1057
  ...typeof body.format === "string" && body.format.trim() !== "" ? { format: body.format.trim() } : {},
859
- ...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 } : {}
860
1074
  };
861
1075
  }
862
1076
  function toView(descriptor) {
@@ -997,7 +1211,7 @@ function makeRoutes(deps) {
997
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];
998
1212
  const channel = {
999
1213
  id: stored?.id ?? "preview",
1000
- preset: stored?.preset ?? "",
1214
+ preset: typeof body?.preset === "string" ? body.preset.trim() : stored?.preset ?? "",
1001
1215
  name: stored?.name ?? "",
1002
1216
  apiUrl: typeof body?.apiUrl === "string" && body.apiUrl.trim() !== "" ? body.apiUrl.trim() : stored?.apiUrl ?? "",
1003
1217
  apiKey: typeof body?.apiKey === "string" && body.apiKey.trim() !== "" ? body.apiKey.trim() : stored?.apiKey ?? "",
@@ -1364,7 +1578,7 @@ function registerAgentAudioTools(ctx, resolve) {
1364
1578
  },
1365
1579
  voice: {
1366
1580
  type: "string",
1367
- 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."
1368
1582
  },
1369
1583
  preview_text: {
1370
1584
  type: "string",
@@ -1372,7 +1586,7 @@ function registerAgentAudioTools(ctx, resolve) {
1372
1586
  },
1373
1587
  speed: {
1374
1588
  type: "number",
1375
- 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)."
1376
1590
  },
1377
1591
  duration: {
1378
1592
  type: "number",
@@ -1381,6 +1595,95 @@ function registerAgentAudioTools(ctx, resolve) {
1381
1595
  format: {
1382
1596
  type: "string",
1383
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)."
1384
1687
  }
1385
1688
  },
1386
1689
  output: {
@@ -1403,6 +1706,19 @@ function registerAgentAudioTools(ctx, resolve) {
1403
1706
  upstream: ""
1404
1707
  };
1405
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;
1406
1722
  const request = {
1407
1723
  mode,
1408
1724
  model: picked.alias,
@@ -1414,7 +1730,22 @@ function registerAgentAudioTools(ctx, resolve) {
1414
1730
  ...typeof args.preview_text === "string" && args.preview_text.trim() !== "" ? { previewText: args.preview_text.trim() } : {},
1415
1731
  ...typeof args.speed === "number" ? { speed: args.speed } : {},
1416
1732
  ...typeof args.duration === "number" ? { duration: args.duration } : {},
1417
- ...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 } : {}
1418
1749
  };
1419
1750
  try {
1420
1751
  const outputs = await generateAudio(picked.channel, request, exec.signal);