dsh-audiogen 0.4.13 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/lib/client.js +350 -348
- package/lib/client.js.map +1 -1
- package/lib/index.js +588 -27
- package/package.json +1 -1
- package/skills/sfx/SKILL.md +6 -0
- package/skills/voice-management/SKILL.md +48 -0
- package/src/agent-audio-tools.ts +123 -0
- package/src/audio-engine.ts +170 -10
- package/src/client/api.ts +2 -0
- package/src/client/audio-panel.module.css +7 -0
- package/src/client/field-specs.ts +1 -1
- package/src/client/studio-view.tsx +9 -1
- package/src/index.ts +1 -1
- package/src/protocol.ts +1 -1
- package/src/routes.ts +6 -1
- package/src/voice-manager.ts +378 -0
package/lib/index.js
CHANGED
|
@@ -175,10 +175,10 @@ function isPreset(channel, id) {
|
|
|
175
175
|
function isOpenAICompatible(channel, mode) {
|
|
176
176
|
return isPreset(channel, "openai") || /(^|\/)(v\d+\/)?audio\/speech$/i.test(channel.apiUrl.trim()) || channel.preset === "custom" && mode === "tts";
|
|
177
177
|
}
|
|
178
|
-
function isElevenLabs$
|
|
178
|
+
function isElevenLabs$2(channel) {
|
|
179
179
|
return isPreset(channel, "elevenlabs") || /elevenlabs/i.test(channel.apiUrl);
|
|
180
180
|
}
|
|
181
|
-
function isMiniMax$
|
|
181
|
+
function isMiniMax$2(channel) {
|
|
182
182
|
return isPreset(channel, "minimax") || /minimax/i.test(channel.apiUrl);
|
|
183
183
|
}
|
|
184
184
|
function isStability$1(channel) {
|
|
@@ -332,7 +332,7 @@ async function openAITTS(channel, request, signal) {
|
|
|
332
332
|
fallbackMime: "audio/mpeg"
|
|
333
333
|
});
|
|
334
334
|
}
|
|
335
|
-
async function
|
|
335
|
+
async function elevenLabsOfficial(channel, request, signal) {
|
|
336
336
|
const base = endpointBase(channel.apiUrl);
|
|
337
337
|
const model = (request.upstream ?? request.model) || "eleven_multilingual_v2";
|
|
338
338
|
const headers = {
|
|
@@ -377,12 +377,15 @@ async function elevenLabs(channel, request, signal) {
|
|
|
377
377
|
}
|
|
378
378
|
if (request.mode === "music") {
|
|
379
379
|
const endpoint = `${base}/music`;
|
|
380
|
+
const musicModel = (request.upstream ?? request.model) || "music_v1";
|
|
381
|
+
const lyrics = request.lyrics?.trim() ?? "";
|
|
382
|
+
const instrumental = request.isInstrumental === true || lyrics === "";
|
|
380
383
|
const body = {
|
|
381
|
-
model_id:
|
|
384
|
+
model_id: musicModel,
|
|
382
385
|
prompt: request.prompt,
|
|
383
386
|
...request.duration !== void 0 && Number.isFinite(request.duration) ? { music_length_ms: Math.round(Math.min(6e5, Math.max(3e3, request.duration * 1e3))) } : {},
|
|
384
|
-
...
|
|
385
|
-
...
|
|
387
|
+
...lyrics === "" ? {} : { lyrics_text: lyrics },
|
|
388
|
+
...instrumental ? { force_instrumental: true } : {}
|
|
386
389
|
};
|
|
387
390
|
const response = await fetchWithTimeout(endpoint, {
|
|
388
391
|
method: "POST",
|
|
@@ -450,6 +453,120 @@ async function elevenLabs(channel, request, signal) {
|
|
|
450
453
|
fallbackMime: "audio/mpeg"
|
|
451
454
|
});
|
|
452
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* 官方 ElevenLabs 请求被网关拒绝的信号:官方路径未映射(404 Invalid URL)或
|
|
458
|
+
* 网关要求 Bearer 认证而非 xi-api-key(401/403 Invalid token / Invalid API key)。
|
|
459
|
+
* New API 类中转(如 ai.farmmx.com)对 ElevenLabs 官方协议通常返回这类错误。
|
|
460
|
+
*/
|
|
461
|
+
function isGatewayRouteMiss(error) {
|
|
462
|
+
return error instanceof AudioGenError && error.code === "audio-api-error" && /\bHTTP (404|401|403)\b/.test(error.message) && /\bInvalid URL\b|\bInvalid token\b|\bInvalid API key\b/i.test(error.message);
|
|
463
|
+
}
|
|
464
|
+
/** 网关兼容形态的请求头:仅 Bearer(携带 xi-api-key 会被网关按官方协议校验而 401)。 */
|
|
465
|
+
function gatewayHeaders(apiKey) {
|
|
466
|
+
return {
|
|
467
|
+
authorization: `Bearer ${apiKey.trim()}`,
|
|
468
|
+
"content-type": "application/json",
|
|
469
|
+
accept: "audio/mpeg, application/json"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
/** /audio/speech 兼容端点:base 已以此结尾时直接复用,否则拼接。 */
|
|
473
|
+
function speechGatewayEndpoint(base) {
|
|
474
|
+
return /\/audio\/speech(\?|$)/i.test(base) ? base : `${base}/audio/speech`;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* ElevenLabs 渠道的网关兼容形态(OpenAI 风格):路径用 /audio/speech(音效/TTS)
|
|
478
|
+
* 或 /music(音乐),认证用 Bearer、模型用 `model` 字段。
|
|
479
|
+
*
|
|
480
|
+
* 适配未映射 ElevenLabs 官方端点(404 Invalid URL)或要求 Bearer 认证
|
|
481
|
+
* (401 Invalid token)的 New API 类中转,如 ai.farmmx.com。
|
|
482
|
+
*/
|
|
483
|
+
async function elevenLabsGatewayCompat(channel, request, signal) {
|
|
484
|
+
const base = endpointBase(channel.apiUrl);
|
|
485
|
+
const headers = gatewayHeaders(channel.apiKey);
|
|
486
|
+
if (request.mode === "music") {
|
|
487
|
+
const endpoint = /\/music(\?|$)/i.test(base) ? base : `${base}/music`;
|
|
488
|
+
const musicModel = (request.upstream ?? request.model) || "music_v1";
|
|
489
|
+
const lyrics = request.lyrics?.trim() ?? "";
|
|
490
|
+
const instrumental = request.isInstrumental === true || lyrics === "";
|
|
491
|
+
const body = {
|
|
492
|
+
model: musicModel,
|
|
493
|
+
prompt: request.prompt,
|
|
494
|
+
...request.duration !== void 0 && Number.isFinite(request.duration) ? { music_length_ms: Math.round(Math.min(6e5, Math.max(3e3, request.duration * 1e3))) } : {},
|
|
495
|
+
...lyrics === "" ? {} : { lyrics_text: lyrics },
|
|
496
|
+
...instrumental ? { force_instrumental: true } : {}
|
|
497
|
+
};
|
|
498
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
499
|
+
method: "POST",
|
|
500
|
+
redirect: "follow",
|
|
501
|
+
headers,
|
|
502
|
+
body: JSON.stringify(body),
|
|
503
|
+
signal
|
|
504
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
const detail = await response.text().catch(() => "");
|
|
507
|
+
throw new AudioGenError(`ElevenLabs music gateway-compatible API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
508
|
+
}
|
|
509
|
+
return normalizeAudioResponse(response, {
|
|
510
|
+
apiKey: channel.apiKey,
|
|
511
|
+
fallbackMime: "audio/mpeg"
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (request.mode === "voice_design") throw new AudioGenError("当前网关不支持 ElevenLabs 音色设计端点(POST /v1/text-to-voice/design);该模式请改用 MiniMax 渠道或 ElevenLabs 官方 API。", "voice-design-unsupported");
|
|
515
|
+
const endpoint = speechGatewayEndpoint(base);
|
|
516
|
+
const isSfx = request.mode === "sfx";
|
|
517
|
+
const model = (request.upstream ?? request.model) || (isSfx ? "eleven_text_to_sound_v2" : "eleven_multilingual_v2");
|
|
518
|
+
if (!isSfx) {
|
|
519
|
+
if ((request.voice?.trim() ?? "") === "") {
|
|
520
|
+
const suggestions = (channel.models ?? []).filter((entry) => {
|
|
521
|
+
const candidate = entry;
|
|
522
|
+
return candidate.category === "tts" && candidate.id.trim() !== "" && candidate.id !== candidate.alias;
|
|
523
|
+
}).map((entry) => `${entry.alias}(${entry.id})`);
|
|
524
|
+
throw new AudioGenError(`ElevenLabs 网关渠道的 TTS 必须携带音色 voice_id(网关强制校验,缺失会返回 400 voice or voice_id is required):请在「音色」字段填入官方音色 ID${suggestions.length === 0 ? "" : `,可选用以下音色:${suggestions.slice(0, 4).join("、")}`}。`, "voice-required");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const body = isSfx ? {
|
|
528
|
+
model,
|
|
529
|
+
input: request.prompt,
|
|
530
|
+
...request.duration !== void 0 && Number.isFinite(request.duration) ? { duration_seconds: Math.min(30, Math.max(.5, request.duration)) } : {},
|
|
531
|
+
...request.loop !== void 0 ? { loop: request.loop } : {},
|
|
532
|
+
...request.promptInfluence !== void 0 && Number.isFinite(request.promptInfluence) ? { prompt_influence: Math.min(1, Math.max(0, request.promptInfluence)) } : {}
|
|
533
|
+
} : {
|
|
534
|
+
model,
|
|
535
|
+
input: request.prompt,
|
|
536
|
+
voice: request.voice.trim(),
|
|
537
|
+
response_format: request.format ?? "mp3",
|
|
538
|
+
...request.speed !== void 0 ? { speed: request.speed } : {}
|
|
539
|
+
};
|
|
540
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
redirect: "follow",
|
|
543
|
+
headers,
|
|
544
|
+
body: JSON.stringify(body),
|
|
545
|
+
signal
|
|
546
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
547
|
+
if (!response.ok) {
|
|
548
|
+
const detail = await response.text().catch(() => "");
|
|
549
|
+
throw new AudioGenError(`ElevenLabs ${isSfx ? "sound effects" : "TTS"} gateway-compatible API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
550
|
+
}
|
|
551
|
+
return normalizeAudioResponse(response, {
|
|
552
|
+
apiKey: channel.apiKey,
|
|
553
|
+
fallbackMime: "audio/mpeg"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* ElevenLabs 渠道入口:官方端点优先;官方协议被网关(New API 类中转)拒绝时,
|
|
558
|
+
* 自动改用 OpenAI 兼容形态重试,使同一渠道同时兼容 ElevenLabs 官方 API 与
|
|
559
|
+
* ai.farmmx.com 类中转。官方地址(api.elevenlabs.io)直连不触发回退。
|
|
560
|
+
*/
|
|
561
|
+
async function elevenLabs(channel, request, signal) {
|
|
562
|
+
if (/elevenlabs\.io/i.test(channel.apiUrl)) return elevenLabsOfficial(channel, request, signal);
|
|
563
|
+
try {
|
|
564
|
+
return await elevenLabsOfficial(channel, request, signal);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (!isGatewayRouteMiss(error)) throw error;
|
|
567
|
+
}
|
|
568
|
+
return elevenLabsGatewayCompat(channel, request, signal);
|
|
569
|
+
}
|
|
453
570
|
function minimaxApiBase(base) {
|
|
454
571
|
const trimmed = endpointBase(base);
|
|
455
572
|
return /\/v1$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
@@ -614,13 +731,13 @@ async function minimax(channel, request, signal) {
|
|
|
614
731
|
256e3
|
|
615
732
|
]);
|
|
616
733
|
const lyrics = request.lyrics?.trim() ?? "";
|
|
617
|
-
|
|
734
|
+
const instrumental = request.isInstrumental === true || lyrics === "";
|
|
618
735
|
const endpoint = `${base}/music_generation`;
|
|
619
736
|
const body = {
|
|
620
737
|
model,
|
|
621
738
|
prompt: request.prompt,
|
|
622
739
|
...lyrics === "" ? {} : { lyrics },
|
|
623
|
-
...
|
|
740
|
+
...instrumental ? { is_instrumental: true } : {},
|
|
624
741
|
...request.duration !== void 0 ? { duration: request.duration } : {},
|
|
625
742
|
audio_setting: {
|
|
626
743
|
format: MUSIC_FORMATS.has(request.format ?? "mp3") ? request.format ?? "mp3" : "mp3",
|
|
@@ -860,9 +977,9 @@ async function generateAudio(channel, request, signal) {
|
|
|
860
977
|
if (channel.apiUrl.trim() === "") throw new AudioGenError("channel API URL is not configured", "audio-no-endpoint");
|
|
861
978
|
if (channel.apiKey.trim() === "") throw new AudioGenError("channel API key is not configured", "audio-no-key");
|
|
862
979
|
if (request.prompt.trim() === "") throw new AudioGenError("audio prompt/text is required", "audio-empty-prompt");
|
|
863
|
-
if (request.mode === "voice_design" && !isMiniMax$
|
|
864
|
-
if (isElevenLabs$
|
|
865
|
-
if (isMiniMax$
|
|
980
|
+
if (request.mode === "voice_design" && !isMiniMax$2(channel) && !isElevenLabs$2(channel)) throw new AudioGenError("音色设计当前仅支持 MiniMax(/v1/voice_design)与 ElevenLabs(/v1/text-to-voice/design)渠道", "voice-design-unsupported");
|
|
981
|
+
if (isElevenLabs$2(channel)) return elevenLabs(channel, request, signal);
|
|
982
|
+
if (isMiniMax$2(channel)) return minimax(channel, request, signal);
|
|
866
983
|
if (isStability$1(channel) || /^stable-audio-/i.test(((request.upstream ?? request.model) || "").trim())) return stabilityAudio(channel, request, signal);
|
|
867
984
|
if (isOpenAICompatible(channel, request.mode)) return openAITTS(channel, request, signal);
|
|
868
985
|
return genericAudio(channel, request, signal);
|
|
@@ -1101,16 +1218,16 @@ function audioPresetById(id) {
|
|
|
1101
1218
|
}
|
|
1102
1219
|
//#endregion
|
|
1103
1220
|
//#region src/audio-models.ts
|
|
1104
|
-
function isMiniMax(channel) {
|
|
1221
|
+
function isMiniMax$1(channel) {
|
|
1105
1222
|
return channel.preset === "minimax" || /minimax/i.test(channel.apiUrl);
|
|
1106
1223
|
}
|
|
1107
|
-
function isElevenLabs(channel) {
|
|
1224
|
+
function isElevenLabs$1(channel) {
|
|
1108
1225
|
return channel.preset === "elevenlabs" || /elevenlabs/i.test(channel.apiUrl);
|
|
1109
1226
|
}
|
|
1110
1227
|
function isStability(channel) {
|
|
1111
1228
|
return channel.preset === "stability" || /stability\.ai/i.test(channel.apiUrl);
|
|
1112
1229
|
}
|
|
1113
|
-
function baseUrl(url) {
|
|
1230
|
+
function baseUrl$1(url) {
|
|
1114
1231
|
return url.trim().replace(/\/+$/, "");
|
|
1115
1232
|
}
|
|
1116
1233
|
/** Whether an upstream model id is audio-related at all. */
|
|
@@ -1120,7 +1237,7 @@ function categoryFor(id) {
|
|
|
1120
1237
|
if (/(music|song|cover|lyrics|audio|melody|beat)/i.test(value)) return "music";
|
|
1121
1238
|
if (/(sfx|sound.?effect|effect|foley)/i.test(value)) return "sfx";
|
|
1122
1239
|
}
|
|
1123
|
-
async function fetchJson(url, init) {
|
|
1240
|
+
async function fetchJson$1(url, init) {
|
|
1124
1241
|
const response = await fetch(url, init);
|
|
1125
1242
|
if (!response.ok) {
|
|
1126
1243
|
const text = await response.text().catch(() => "");
|
|
@@ -1128,8 +1245,8 @@ async function fetchJson(url, init) {
|
|
|
1128
1245
|
}
|
|
1129
1246
|
return response.json();
|
|
1130
1247
|
}
|
|
1131
|
-
async function postJson(url, apiKey, body) {
|
|
1132
|
-
return fetchJson(url, {
|
|
1248
|
+
async function postJson$1(url, apiKey, body) {
|
|
1249
|
+
return fetchJson$1(url, {
|
|
1133
1250
|
method: "POST",
|
|
1134
1251
|
headers: {
|
|
1135
1252
|
authorization: `Bearer ${apiKey.trim()}`,
|
|
@@ -1142,15 +1259,15 @@ async function postJson(url, apiKey, body) {
|
|
|
1142
1259
|
async function discoverAudioModels(channel) {
|
|
1143
1260
|
if (channel.apiUrl.trim() === "") throw new Error("API URL is not configured");
|
|
1144
1261
|
if (channel.apiKey.trim() === "") throw new Error("API key is not configured");
|
|
1145
|
-
if (isMiniMax(channel)) return discoverMiniMax(channel);
|
|
1146
|
-
if (isElevenLabs(channel)) return discoverElevenLabs(channel);
|
|
1262
|
+
if (isMiniMax$1(channel)) return discoverMiniMax(channel);
|
|
1263
|
+
if (isElevenLabs$1(channel)) return discoverElevenLabs(channel);
|
|
1147
1264
|
if (isStability(channel)) return discoverStability(channel);
|
|
1148
1265
|
return discoverOpenAICompatible(channel);
|
|
1149
1266
|
}
|
|
1150
1267
|
async function discoverMiniMax(channel) {
|
|
1151
|
-
const url = `${baseUrl(channel.apiUrl).replace(/\/v1$/i, "")}/v1/get_voice`;
|
|
1268
|
+
const url = `${baseUrl$1(channel.apiUrl).replace(/\/v1$/i, "")}/v1/get_voice`;
|
|
1152
1269
|
try {
|
|
1153
|
-
const payload = await postJson(url, channel.apiKey, { voice_type: "all" });
|
|
1270
|
+
const payload = await postJson$1(url, channel.apiKey, { voice_type: "all" });
|
|
1154
1271
|
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}`);
|
|
1155
1272
|
const models = [];
|
|
1156
1273
|
for (const voice of payload.system_voice ?? []) {
|
|
@@ -1202,12 +1319,12 @@ async function discoverMiniMax(channel) {
|
|
|
1202
1319
|
}
|
|
1203
1320
|
}
|
|
1204
1321
|
async function discoverElevenLabs(channel) {
|
|
1205
|
-
const base = baseUrl(channel.apiUrl);
|
|
1322
|
+
const base = baseUrl$1(channel.apiUrl);
|
|
1206
1323
|
const headers = { "xi-api-key": channel.apiKey.trim() };
|
|
1207
1324
|
const failures = [];
|
|
1208
1325
|
const models = [];
|
|
1209
1326
|
try {
|
|
1210
|
-
const payload = await fetchJson(`${base}/models`, { headers });
|
|
1327
|
+
const payload = await fetchJson$1(`${base}/models`, { headers });
|
|
1211
1328
|
for (const item of Array.isArray(payload) ? payload : []) {
|
|
1212
1329
|
const id = item.model_id?.trim() ?? "";
|
|
1213
1330
|
if (id === "") continue;
|
|
@@ -1223,7 +1340,7 @@ async function discoverElevenLabs(channel) {
|
|
|
1223
1340
|
failures.push(`模型列表:${error instanceof Error ? error.message : String(error)}`);
|
|
1224
1341
|
}
|
|
1225
1342
|
try {
|
|
1226
|
-
const payload = await fetchJson(`${base}/voices`, { headers });
|
|
1343
|
+
const payload = await fetchJson$1(`${base}/voices`, { headers });
|
|
1227
1344
|
for (const voice of Array.isArray(payload?.voices) ? payload.voices : []) {
|
|
1228
1345
|
const id = voice.voice_id?.trim() ?? "";
|
|
1229
1346
|
if (id === "") continue;
|
|
@@ -1257,7 +1374,7 @@ async function discoverStability(channel) {
|
|
|
1257
1374
|
};
|
|
1258
1375
|
}
|
|
1259
1376
|
async function discoverOpenAICompatible(channel) {
|
|
1260
|
-
const payload = await fetchJson(`${baseUrl(channel.apiUrl)}/models`, { headers: { authorization: `Bearer ${channel.apiKey.trim()}` } });
|
|
1377
|
+
const payload = await fetchJson$1(`${baseUrl$1(channel.apiUrl)}/models`, { headers: { authorization: `Bearer ${channel.apiKey.trim()}` } });
|
|
1261
1378
|
const models = [];
|
|
1262
1379
|
for (const item of payload.data ?? []) {
|
|
1263
1380
|
const id = item.id?.trim() ?? "";
|
|
@@ -2100,11 +2217,13 @@ function makeRoutes(deps) {
|
|
|
2100
2217
|
type: entry.type
|
|
2101
2218
|
}];
|
|
2102
2219
|
} catch {}
|
|
2220
|
+
const note = request.mode === "music" && request.isInstrumental !== true && (request.lyrics === void 0 || request.lyrics.trim() === "") ? "未提供歌词,已按纯音乐生成" : void 0;
|
|
2103
2221
|
writeJson(res, 200, {
|
|
2104
2222
|
ok: true,
|
|
2105
2223
|
outputs: generated,
|
|
2106
2224
|
history,
|
|
2107
|
-
...resources === void 0 ? {} : { resources }
|
|
2225
|
+
...resources === void 0 ? {} : { resources },
|
|
2226
|
+
...note === void 0 ? {} : { note }
|
|
2108
2227
|
});
|
|
2109
2228
|
} catch (error) {
|
|
2110
2229
|
writeJson(res, 200, {
|
|
@@ -2453,6 +2572,276 @@ function makeRoutes(deps) {
|
|
|
2453
2572
|
];
|
|
2454
2573
|
}
|
|
2455
2574
|
//#endregion
|
|
2575
|
+
//#region src/voice-manager.ts
|
|
2576
|
+
const MINIMAX_LANGUAGE_PREFIXES = [
|
|
2577
|
+
"Chinese (Mandarin)",
|
|
2578
|
+
"Chinese (Cantonese)",
|
|
2579
|
+
"Japanese",
|
|
2580
|
+
"English",
|
|
2581
|
+
"Korean",
|
|
2582
|
+
"Spanish",
|
|
2583
|
+
"French",
|
|
2584
|
+
"German",
|
|
2585
|
+
"Italian",
|
|
2586
|
+
"Russian",
|
|
2587
|
+
"Portuguese",
|
|
2588
|
+
"Arabic",
|
|
2589
|
+
"Hindi"
|
|
2590
|
+
];
|
|
2591
|
+
function isMiniMax(channel) {
|
|
2592
|
+
return channel.preset === "minimax" || /minimax/i.test(channel.apiUrl);
|
|
2593
|
+
}
|
|
2594
|
+
function isElevenLabs(channel) {
|
|
2595
|
+
return channel.preset === "elevenlabs" || /elevenlabs/i.test(channel.apiUrl);
|
|
2596
|
+
}
|
|
2597
|
+
function supportsVoiceManagement(channel) {
|
|
2598
|
+
return isMiniMax(channel) || isElevenLabs(channel);
|
|
2599
|
+
}
|
|
2600
|
+
function baseUrl(url) {
|
|
2601
|
+
return url.trim().replace(/\/+$/, "");
|
|
2602
|
+
}
|
|
2603
|
+
/** MiniMax system voice ids carry a language label prefix. */
|
|
2604
|
+
function languageFromMiniMaxId(voiceId) {
|
|
2605
|
+
for (const prefix of MINIMAX_LANGUAGE_PREFIXES) if (voiceId.startsWith(`${prefix}_`)) return prefix;
|
|
2606
|
+
}
|
|
2607
|
+
function asStringList(value) {
|
|
2608
|
+
if (Array.isArray(value)) return value.map((item) => String(item)).filter((item) => item.trim() !== "");
|
|
2609
|
+
if (typeof value === "string" && value.trim() !== "") return [value.trim()];
|
|
2610
|
+
return [];
|
|
2611
|
+
}
|
|
2612
|
+
async function fetchJson(url, init) {
|
|
2613
|
+
const response = await fetch(url, init);
|
|
2614
|
+
if (!response.ok) {
|
|
2615
|
+
const text = await response.text().catch(() => "");
|
|
2616
|
+
throw new Error(`HTTP ${response.status}${text === "" ? "" : `: ${text.slice(0, 300)}`}`);
|
|
2617
|
+
}
|
|
2618
|
+
return response.json();
|
|
2619
|
+
}
|
|
2620
|
+
async function postJson(url, apiKey, body) {
|
|
2621
|
+
return fetchJson(url, {
|
|
2622
|
+
method: "POST",
|
|
2623
|
+
headers: {
|
|
2624
|
+
authorization: `Bearer ${apiKey.trim()}`,
|
|
2625
|
+
"content-type": "application/json"
|
|
2626
|
+
},
|
|
2627
|
+
body: JSON.stringify(body)
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
function normalizeMiniMax(voice, source) {
|
|
2631
|
+
const voiceId = String(voice.voice_id ?? "").trim();
|
|
2632
|
+
const description = asStringList(voice.description).join(";") || void 0;
|
|
2633
|
+
const language = languageFromMiniMaxId(voiceId);
|
|
2634
|
+
return {
|
|
2635
|
+
provider: "minimax",
|
|
2636
|
+
voice_id: voiceId,
|
|
2637
|
+
name: String(voice.voice_name ?? voiceId).trim() || voiceId,
|
|
2638
|
+
source,
|
|
2639
|
+
...language === void 0 ? {} : { language },
|
|
2640
|
+
...description === void 0 ? {} : { description },
|
|
2641
|
+
deletable: source === "custom"
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
async function listMiniMax(channel) {
|
|
2645
|
+
const payload = await postJson(`${baseUrl(channel.apiUrl).replace(/\/v1$/i, "")}/v1/get_voice`, channel.apiKey, { voice_type: "all" });
|
|
2646
|
+
if (payload.base_resp?.status_code !== void 0 && payload.base_resp.status_code !== 0) throw new Error(`MiniMax get_voice 失败:${payload.base_resp.status_msg ?? `status ${payload.base_resp.status_code}`}`);
|
|
2647
|
+
const entries = [];
|
|
2648
|
+
for (const voice of Array.isArray(payload.system_voice) ? payload.system_voice : []) {
|
|
2649
|
+
if (typeof voice !== "object" || voice === null) continue;
|
|
2650
|
+
const entry = normalizeMiniMax(voice, "system");
|
|
2651
|
+
if (entry.voice_id !== "") entries.push(entry);
|
|
2652
|
+
}
|
|
2653
|
+
for (const bucket of ["voice_cloning", "voice_generation"]) for (const voice of Array.isArray(payload[bucket]) ? payload[bucket] : []) {
|
|
2654
|
+
if (typeof voice !== "object" || voice === null) continue;
|
|
2655
|
+
const entry = normalizeMiniMax(voice, "custom");
|
|
2656
|
+
if (entry.voice_id !== "") entries.push(entry);
|
|
2657
|
+
}
|
|
2658
|
+
return {
|
|
2659
|
+
vendor: "minimax",
|
|
2660
|
+
voices: entries,
|
|
2661
|
+
truncated: false
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
function normalizeElevenLabs(voice, source) {
|
|
2665
|
+
const voiceId = String(voice.voice_id ?? "").trim();
|
|
2666
|
+
const labels = typeof voice.labels === "object" && voice.labels !== null ? voice.labels : void 0;
|
|
2667
|
+
const pick = (key) => {
|
|
2668
|
+
const owned = labels?.[key];
|
|
2669
|
+
const direct = voice[key];
|
|
2670
|
+
return (typeof owned === "string" && owned.trim() !== "" ? owned : typeof direct === "string" && direct.trim() !== "" ? direct : void 0)?.trim() || void 0;
|
|
2671
|
+
};
|
|
2672
|
+
const description = pick("description");
|
|
2673
|
+
return {
|
|
2674
|
+
provider: "elevenlabs",
|
|
2675
|
+
voice_id: voiceId,
|
|
2676
|
+
name: String(voice.name ?? voiceId).trim() || voiceId,
|
|
2677
|
+
source,
|
|
2678
|
+
...pick("language") === void 0 ? {} : { language: pick("language") },
|
|
2679
|
+
...pick("locale") === void 0 ? {} : { locale: pick("locale") },
|
|
2680
|
+
...pick("accent") === void 0 ? {} : { accent: pick("accent") },
|
|
2681
|
+
...pick("gender") === void 0 ? {} : { gender: pick("gender") },
|
|
2682
|
+
...pick("age") === void 0 ? {} : { age: pick("age") },
|
|
2683
|
+
...pick("use_case") === void 0 ? {} : { use_case: pick("use_case") },
|
|
2684
|
+
...pick("category") === void 0 ? {} : { category: pick("category") },
|
|
2685
|
+
...description === void 0 ? {} : { description },
|
|
2686
|
+
...typeof voice.preview_url === "string" && voice.preview_url.trim() !== "" ? { preview_url: voice.preview_url.trim() } : {},
|
|
2687
|
+
deletable: source === "owned"
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
async function listElevenLabs(channel, options) {
|
|
2691
|
+
const base = baseUrl(channel.apiUrl);
|
|
2692
|
+
const headers = {
|
|
2693
|
+
"xi-api-key": channel.apiKey.trim(),
|
|
2694
|
+
accept: "application/json"
|
|
2695
|
+
};
|
|
2696
|
+
const entries = [];
|
|
2697
|
+
const failures = [];
|
|
2698
|
+
try {
|
|
2699
|
+
const payload = await fetchJson(`${base}/voices`, { headers });
|
|
2700
|
+
for (const voice of Array.isArray(payload.voices) ? payload.voices : []) {
|
|
2701
|
+
if (typeof voice !== "object" || voice === null) continue;
|
|
2702
|
+
const entry = normalizeElevenLabs(voice, "owned");
|
|
2703
|
+
if (entry.voice_id !== "") entries.push(entry);
|
|
2704
|
+
}
|
|
2705
|
+
} catch (error) {
|
|
2706
|
+
failures.push(`自有音色:${error instanceof Error ? error.message : String(error)}`);
|
|
2707
|
+
}
|
|
2708
|
+
const pageSize = 100;
|
|
2709
|
+
try {
|
|
2710
|
+
for (let page = 0; page < 3; page += 1) {
|
|
2711
|
+
const query = new URLSearchParams({
|
|
2712
|
+
page_size: String(pageSize),
|
|
2713
|
+
page: String(page)
|
|
2714
|
+
});
|
|
2715
|
+
if (options.language !== void 0 && options.language.trim() !== "") query.set("language", options.language.trim());
|
|
2716
|
+
const payload = await fetchJson(`${base}/shared-voices?${query.toString()}`, { headers });
|
|
2717
|
+
const voices = Array.isArray(payload.voices) ? payload.voices : [];
|
|
2718
|
+
for (const voice of voices) {
|
|
2719
|
+
if (typeof voice !== "object" || voice === null) continue;
|
|
2720
|
+
const entry = normalizeElevenLabs(voice, "shared");
|
|
2721
|
+
if (entry.voice_id !== "") entries.push(entry);
|
|
2722
|
+
}
|
|
2723
|
+
if (payload.has_more !== true || voices.length === 0) break;
|
|
2724
|
+
}
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
failures.push(`共享音色库:${error instanceof Error ? error.message : String(error)}`);
|
|
2727
|
+
}
|
|
2728
|
+
if (entries.length === 0 && failures.length > 0) throw new Error(`ElevenLabs 音色列表拉取失败:${failures.join(";").slice(0, 300)}`);
|
|
2729
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2730
|
+
const deduped = [];
|
|
2731
|
+
for (const entry of entries) {
|
|
2732
|
+
if (seen.has(entry.voice_id)) continue;
|
|
2733
|
+
seen.add(entry.voice_id);
|
|
2734
|
+
deduped.push(entry);
|
|
2735
|
+
}
|
|
2736
|
+
return {
|
|
2737
|
+
vendor: "elevenlabs",
|
|
2738
|
+
voices: deduped,
|
|
2739
|
+
truncated: false,
|
|
2740
|
+
...failures.length === 0 ? {} : { note: `部分端点失败(已忽略):${failures.join(";").slice(0, 300)}` }
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
async function listVendorVoices(channel, options = {}) {
|
|
2744
|
+
if (channel.apiUrl.trim() === "") throw new Error("渠道未配置 API 地址");
|
|
2745
|
+
if (channel.apiKey.trim() === "") throw new Error("渠道未配置 API 密钥");
|
|
2746
|
+
if (!supportsVoiceManagement(channel)) throw new Error(`当前渠道「${channel.name}」不提供厂商音色管理接口:仅 MiniMax 与 ElevenLabs 支持音色浏览/删除`);
|
|
2747
|
+
if (isMiniMax(channel)) {
|
|
2748
|
+
const result = await listMiniMax(channel);
|
|
2749
|
+
return {
|
|
2750
|
+
vendor: result.vendor,
|
|
2751
|
+
...applyFilter(result.voices, options),
|
|
2752
|
+
...result.note === void 0 ? {} : { note: result.note }
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
const result = await listElevenLabs(channel, options);
|
|
2756
|
+
return {
|
|
2757
|
+
vendor: result.vendor,
|
|
2758
|
+
...applyFilter(result.voices, options),
|
|
2759
|
+
...result.note === void 0 ? {} : { note: result.note }
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
async function deleteVendorVoice(channel, voiceId) {
|
|
2763
|
+
if (channel.apiUrl.trim() === "") throw new Error("渠道未配置 API 地址");
|
|
2764
|
+
if (channel.apiKey.trim() === "") throw new Error("渠道未配置 API 密钥");
|
|
2765
|
+
const id = voiceId.trim();
|
|
2766
|
+
if (id === "") throw new Error("voice_id 不能为空");
|
|
2767
|
+
if (isMiniMax(channel)) {
|
|
2768
|
+
const known = (await listMiniMax(channel)).voices.find((entry) => entry.voice_id === id);
|
|
2769
|
+
if (known !== void 0 && known.source === "system") throw new Error(`MiniMax 系统预置音色「${id}」为只读,不能删除(仅自定义音色可删)`);
|
|
2770
|
+
const payload = await postJson(`${baseUrl(channel.apiUrl).replace(/\/v1$/i, "")}/v1/delete_voice`, channel.apiKey, {
|
|
2771
|
+
voice_id: id,
|
|
2772
|
+
voice_type: "voice_cloning"
|
|
2773
|
+
});
|
|
2774
|
+
if (payload.base_resp?.status_code !== void 0 && payload.base_resp.status_code !== 0) throw new Error(`MiniMax delete_voice 失败:${payload.base_resp.status_msg ?? `status ${payload.base_resp.status_code}`}`);
|
|
2775
|
+
return {
|
|
2776
|
+
vendor: "minimax",
|
|
2777
|
+
voice_id: id,
|
|
2778
|
+
deleted: true
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
if (isElevenLabs(channel)) {
|
|
2782
|
+
const base = baseUrl(channel.apiUrl);
|
|
2783
|
+
const headers = {
|
|
2784
|
+
"xi-api-key": channel.apiKey.trim(),
|
|
2785
|
+
accept: "application/json"
|
|
2786
|
+
};
|
|
2787
|
+
let owned = false;
|
|
2788
|
+
let checkFailed = false;
|
|
2789
|
+
try {
|
|
2790
|
+
const payload = await fetchJson(`${base}/voices`, { headers });
|
|
2791
|
+
if (Array.isArray(payload.voices)) owned = payload.voices.some((voice) => String(voice.voice_id ?? "") === id);
|
|
2792
|
+
} catch {
|
|
2793
|
+
checkFailed = true;
|
|
2794
|
+
}
|
|
2795
|
+
if (!checkFailed && !owned) throw new Error(`ElevenLabs 音色「${id}」不是账户自有音色(共享库/官方音色只读),不能删除`);
|
|
2796
|
+
const response = await fetch(`${base}/voices/${encodeURIComponent(id)}`, {
|
|
2797
|
+
method: "DELETE",
|
|
2798
|
+
headers
|
|
2799
|
+
});
|
|
2800
|
+
if (!response.ok) {
|
|
2801
|
+
const text = await response.text().catch(() => "");
|
|
2802
|
+
throw new Error(`ElevenLabs 删除失败(HTTP ${response.status})${text === "" ? "" : `:${text.slice(0, 300)}`}`);
|
|
2803
|
+
}
|
|
2804
|
+
return {
|
|
2805
|
+
vendor: "elevenlabs",
|
|
2806
|
+
voice_id: id,
|
|
2807
|
+
deleted: true
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
throw new Error(`当前渠道「${channel.name}」不提供厂商音色管理接口:仅 MiniMax 与 ElevenLabs 支持音色浏览/删除`);
|
|
2811
|
+
}
|
|
2812
|
+
function cap(options) {
|
|
2813
|
+
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) ? Math.floor(options.limit) : 100;
|
|
2814
|
+
return Math.max(1, Math.min(200, limit));
|
|
2815
|
+
}
|
|
2816
|
+
function applyFilter(entries, options) {
|
|
2817
|
+
const keyword = options.keyword?.trim().toLowerCase() ?? "";
|
|
2818
|
+
const language = options.language?.trim().toLowerCase() ?? "";
|
|
2819
|
+
const source = options.source?.trim().toLowerCase() ?? "";
|
|
2820
|
+
const count = cap(options);
|
|
2821
|
+
const matched = entries.filter((entry) => {
|
|
2822
|
+
if (source !== "" && entry.source !== source) return false;
|
|
2823
|
+
if (language !== "") {
|
|
2824
|
+
if (![entry.language ?? "", entry.locale ?? ""].join(" ").toLowerCase().includes(language)) return false;
|
|
2825
|
+
}
|
|
2826
|
+
if (keyword !== "") {
|
|
2827
|
+
const haystack = [
|
|
2828
|
+
entry.name,
|
|
2829
|
+
entry.description ?? "",
|
|
2830
|
+
entry.accent ?? "",
|
|
2831
|
+
entry.use_case ?? "",
|
|
2832
|
+
entry.gender ?? "",
|
|
2833
|
+
entry.age ?? ""
|
|
2834
|
+
].join(" ").toLowerCase();
|
|
2835
|
+
for (const token of keyword.split(/\s+/)) if (token !== "" && !haystack.includes(token)) return false;
|
|
2836
|
+
}
|
|
2837
|
+
return true;
|
|
2838
|
+
});
|
|
2839
|
+
return {
|
|
2840
|
+
voices: matched.slice(0, count),
|
|
2841
|
+
truncated: matched.length > count
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
//#endregion
|
|
2456
2845
|
//#region src/agent-audio-tools.ts
|
|
2457
2846
|
const audioRefSchema = {
|
|
2458
2847
|
type: "object",
|
|
@@ -3141,11 +3530,183 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
3141
3530
|
};
|
|
3142
3531
|
}
|
|
3143
3532
|
}));
|
|
3533
|
+
/** 厂商音色管理:浏览/筛选 + 删除(仅自建)。删除不可逆,必须 confirm=true。 */
|
|
3534
|
+
const managementDisposer = ctx.tools.register(defineTool({
|
|
3535
|
+
name: "manage_audio_voices",
|
|
3536
|
+
description: "Manage vendor voice libraries (MiniMax / ElevenLabs). action=list: browse available TTS voices of a channel — official/shared voices plus voices designed/cloned by the account, with language/keyword/source filtering; returns voice_id/name/source/description/preview_url and whether each voice is deletable. action=delete: delete one OWNED voice (custom/owned only; official/shared/system voices are read-only and refused) — irreversible, so confirm must be true (pass the exact voice_id from action=list). Use the returned voice_id with generate_audio (mode=tts, voice=<voice_id>) to speak with the selected voice.",
|
|
3537
|
+
parameters: {
|
|
3538
|
+
action: {
|
|
3539
|
+
type: "string",
|
|
3540
|
+
enum: ["list", "delete"],
|
|
3541
|
+
required: true,
|
|
3542
|
+
description: "list = browse/filter voices; delete = remove an owned voice."
|
|
3543
|
+
},
|
|
3544
|
+
channel: {
|
|
3545
|
+
type: "string",
|
|
3546
|
+
description: "Channel name or id (e.g. the channel shown in settings). Defaults to the default channel; required when more than one channel is configured."
|
|
3547
|
+
},
|
|
3548
|
+
language: {
|
|
3549
|
+
type: "string",
|
|
3550
|
+
description: "Filter for list: language substring (ISO code like en/zh/ja, or a label like Chinese (Mandarin))."
|
|
3551
|
+
},
|
|
3552
|
+
keyword: {
|
|
3553
|
+
type: "string",
|
|
3554
|
+
description: "Filter for list: free text over voice name/description/accent/use_case."
|
|
3555
|
+
},
|
|
3556
|
+
source: {
|
|
3557
|
+
type: "string",
|
|
3558
|
+
enum: [
|
|
3559
|
+
"system",
|
|
3560
|
+
"custom",
|
|
3561
|
+
"owned",
|
|
3562
|
+
"shared"
|
|
3563
|
+
],
|
|
3564
|
+
description: "Filter for list: MiniMax system/custom; ElevenLabs owned (account) / shared (community)."
|
|
3565
|
+
},
|
|
3566
|
+
voice_id: {
|
|
3567
|
+
type: "string",
|
|
3568
|
+
description: "Required for delete: the exact voice_id from action=list."
|
|
3569
|
+
},
|
|
3570
|
+
confirm: {
|
|
3571
|
+
type: "boolean",
|
|
3572
|
+
description: "Required for delete: must be true; deletion is irreversible."
|
|
3573
|
+
}
|
|
3574
|
+
},
|
|
3575
|
+
output: {
|
|
3576
|
+
schema: {
|
|
3577
|
+
type: "object",
|
|
3578
|
+
additionalProperties: false,
|
|
3579
|
+
properties: {
|
|
3580
|
+
status: {
|
|
3581
|
+
type: "string",
|
|
3582
|
+
required: true,
|
|
3583
|
+
enum: ["ok"]
|
|
3584
|
+
},
|
|
3585
|
+
kind: {
|
|
3586
|
+
type: "string",
|
|
3587
|
+
required: true,
|
|
3588
|
+
enum: ["list", "delete"]
|
|
3589
|
+
},
|
|
3590
|
+
vendor: {
|
|
3591
|
+
type: "string",
|
|
3592
|
+
required: true
|
|
3593
|
+
},
|
|
3594
|
+
channel: {
|
|
3595
|
+
type: "string",
|
|
3596
|
+
required: true
|
|
3597
|
+
},
|
|
3598
|
+
count: { type: "integer" },
|
|
3599
|
+
truncated: { type: "boolean" },
|
|
3600
|
+
voices: {
|
|
3601
|
+
type: "array",
|
|
3602
|
+
items: {
|
|
3603
|
+
type: "object",
|
|
3604
|
+
additionalProperties: false,
|
|
3605
|
+
properties: {
|
|
3606
|
+
voice_id: {
|
|
3607
|
+
type: "string",
|
|
3608
|
+
required: true
|
|
3609
|
+
},
|
|
3610
|
+
name: {
|
|
3611
|
+
type: "string",
|
|
3612
|
+
required: true
|
|
3613
|
+
},
|
|
3614
|
+
source: {
|
|
3615
|
+
type: "string",
|
|
3616
|
+
required: true
|
|
3617
|
+
},
|
|
3618
|
+
deletable: {
|
|
3619
|
+
type: "boolean",
|
|
3620
|
+
required: true
|
|
3621
|
+
},
|
|
3622
|
+
language: { type: "string" },
|
|
3623
|
+
accent: { type: "string" },
|
|
3624
|
+
gender: { type: "string" },
|
|
3625
|
+
age: { type: "string" },
|
|
3626
|
+
use_case: { type: "string" },
|
|
3627
|
+
description: { type: "string" },
|
|
3628
|
+
preview_url: { type: "string" }
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
},
|
|
3632
|
+
voice_id: { type: "string" },
|
|
3633
|
+
deleted: { type: "boolean" },
|
|
3634
|
+
message: { type: "string" },
|
|
3635
|
+
note: { type: "string" }
|
|
3636
|
+
}
|
|
3637
|
+
},
|
|
3638
|
+
render: (_args, value) => [{
|
|
3639
|
+
type: "text",
|
|
3640
|
+
text: JSON.stringify(value)
|
|
3641
|
+
}]
|
|
3642
|
+
},
|
|
3643
|
+
timeoutMs: 12e4,
|
|
3644
|
+
isConcurrencySafe: () => true,
|
|
3645
|
+
async execute(args) {
|
|
3646
|
+
const config = resolve();
|
|
3647
|
+
if (!config.enabled) throw new AudioGenError("AI audio is disabled. Open Settings > Plugins > AI Audio and enable it.", "plugin-disabled");
|
|
3648
|
+
const channel = resolveVoiceManagementChannel(config, args.channel);
|
|
3649
|
+
if ((args.action === "delete" ? "delete" : "list") === "list") {
|
|
3650
|
+
const source = typeof args.source === "string" && [
|
|
3651
|
+
"system",
|
|
3652
|
+
"custom",
|
|
3653
|
+
"owned",
|
|
3654
|
+
"shared"
|
|
3655
|
+
].includes(args.source) ? args.source : void 0;
|
|
3656
|
+
const result = await listVendorVoices(channel, {
|
|
3657
|
+
...typeof args.language === "string" && args.language.trim() !== "" ? { language: args.language.trim() } : {},
|
|
3658
|
+
...typeof args.keyword === "string" && args.keyword.trim() !== "" ? { keyword: args.keyword.trim() } : {},
|
|
3659
|
+
...source === void 0 ? {} : { source }
|
|
3660
|
+
});
|
|
3661
|
+
return {
|
|
3662
|
+
status: "ok",
|
|
3663
|
+
kind: "list",
|
|
3664
|
+
vendor: result.vendor,
|
|
3665
|
+
channel: channel.name,
|
|
3666
|
+
count: result.voices.length,
|
|
3667
|
+
...result.truncated ? { truncated: true } : {},
|
|
3668
|
+
voices: result.voices,
|
|
3669
|
+
...result.note === void 0 ? {} : { note: result.note }
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
const voiceId = typeof args.voice_id === "string" ? args.voice_id.trim() : "";
|
|
3673
|
+
if (voiceId === "") throw new AudioGenError("delete requires voice_id (the exact voice_id from action=list).", "voice-id-required");
|
|
3674
|
+
if (args.confirm !== true) throw new AudioGenError("Deletion is irreversible: pass confirm=true after verifying the voice_id.", "voice-delete-requires-confirm");
|
|
3675
|
+
const deleted = await deleteVendorVoice(channel, voiceId);
|
|
3676
|
+
return {
|
|
3677
|
+
status: "ok",
|
|
3678
|
+
kind: "delete",
|
|
3679
|
+
vendor: deleted.vendor,
|
|
3680
|
+
channel: channel.name,
|
|
3681
|
+
voice_id: deleted.voice_id,
|
|
3682
|
+
deleted: true,
|
|
3683
|
+
message: `已删除音色 ${deleted.voice_id}(${channel.name})`
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
}));
|
|
3144
3687
|
return () => {
|
|
3145
3688
|
disposer();
|
|
3146
3689
|
searchDisposer();
|
|
3690
|
+
managementDisposer();
|
|
3147
3691
|
};
|
|
3148
3692
|
}
|
|
3693
|
+
/** 解析工具目标渠道:默认渠道 > 唯一可用渠道;多渠道未指定时要求显式选择。 */
|
|
3694
|
+
function resolveVoiceManagementChannel(config, requested) {
|
|
3695
|
+
const usable = config.channels.filter((channel) => channel.apiUrl.trim() !== "" && channel.apiKey.trim() !== "");
|
|
3696
|
+
if (usable.length === 0) throw new AudioGenError("Audio API credentials are not configured. Open Settings > Plugins > AI Audio, add a channel and fill its API URL and API key.", "audio-api-not-configured");
|
|
3697
|
+
const wanted = typeof requested === "string" ? requested.trim() : "";
|
|
3698
|
+
if (wanted === "") {
|
|
3699
|
+
if (usable.length === 1) return usable[0];
|
|
3700
|
+
const target = usable.find((channel) => channel.id === config.defaultChannelId);
|
|
3701
|
+
if (target !== void 0) return target;
|
|
3702
|
+
throw new AudioGenError(`Multiple audio channels are configured — specify the channel (one of: ${usable.map((channel) => `"${channel.name}"`).join(", ")}).`, "channel-choice-required");
|
|
3703
|
+
}
|
|
3704
|
+
const direct = usable.find((channel) => channel.name === wanted || channel.id === wanted);
|
|
3705
|
+
if (direct !== void 0) return direct;
|
|
3706
|
+
const partial = usable.filter((channel) => channel.name.toLowerCase().includes(wanted.toLowerCase()));
|
|
3707
|
+
if (partial.length === 1) return partial[0];
|
|
3708
|
+
throw new AudioGenError(`Audio channel "${wanted}" is not configured. Choose one of: ${usable.map((channel) => channel.name).join(", ")}.`, "channel-not-configured");
|
|
3709
|
+
}
|
|
3149
3710
|
//#endregion
|
|
3150
3711
|
//#region src/index.ts
|
|
3151
3712
|
/** Stable cordis plugin name. */
|
|
@@ -3180,7 +3741,7 @@ const DEFAULT_ENABLED = true;
|
|
|
3180
3741
|
const DEFAULT_ANNOUNCE = true;
|
|
3181
3742
|
const DEFAULT_ALLOW_AGENT_AUDIO = true;
|
|
3182
3743
|
const SECTION_ORDER = 160;
|
|
3183
|
-
const AUDIOGEN_GUIDANCE = "本机已安装 dsh-audiogen 插件(DSH AI 音频):侧边栏「AI 音频」入口。能力:通过「渠道」对接多个音频生成厂商(OpenAI TTS、ElevenLabs、MiniMax、Stability Audio、自定义 OpenAI 兼容接口),支持 TTS 文本转语音、音乐生成和音效生成。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发。Agent 可直接调用 `generate_audio` 提交 TTS/音乐/音效任务,默认等待完成并返回同源音频 URL
|
|
3744
|
+
const AUDIOGEN_GUIDANCE = "本机已安装 dsh-audiogen 插件(DSH AI 音频):侧边栏「AI 音频」入口。能力:通过「渠道」对接多个音频生成厂商(OpenAI TTS、ElevenLabs、MiniMax、Stability Audio、自定义 OpenAI 兼容接口),支持 TTS 文本转语音、音乐生成和音效生成。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发。Agent 可直接调用 `generate_audio` 提交 TTS/音乐/音效任务,默认等待完成并返回同源音频 URL;可用 `manage_audio_voices` 浏览/筛选/删除厂商音色(MiniMax、ElevenLabs),再用选定音色的 voice_id 调用 `generate_audio` 生成。限制:生成消耗上游 API 额度;音频内容由上游模型生成;模型只能使用用户在各渠道配置目录中的模型。用户提到「音频 / 语音 / TTS / 配乐 / 音效 / AI 音频」时即指本插件,请据此协作。";
|
|
3184
3745
|
function guidanceFor(channels, defaultChannelId) {
|
|
3185
3746
|
if (channels.length === 0) return `${AUDIOGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 音频」添加渠道并填写 API 地址与密钥。`;
|
|
3186
3747
|
const table = channels.map((channel) => {
|