dsh-audiogen 0.3.3 → 0.3.5
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 +378 -253
- package/lib/client.js.map +1 -1
- package/lib/index.js +141 -25
- package/package.json +1 -1
- package/skills/design/SKILL.md +7 -1
- package/skills/music/SKILL.md +13 -0
- package/skills/sfx/SKILL.md +17 -3
- package/src/agent-audio-tools.ts +7 -3
- package/src/audio-engine.ts +111 -7
- package/src/audio-presets.ts +6 -1
- package/src/client/AudioGenPanel.tsx +69 -5
- package/src/client/locales.ts +2 -2
- package/src/protocol.ts +5 -1
- package/src/routes.ts +10 -4
package/lib/index.js
CHANGED
|
@@ -250,6 +250,97 @@ async function openAITTS(channel, request, signal) {
|
|
|
250
250
|
async function elevenLabs(channel, request, signal) {
|
|
251
251
|
const base = endpointBase(channel.apiUrl);
|
|
252
252
|
const model = (request.upstream ?? request.model) || "eleven_multilingual_v2";
|
|
253
|
+
const headers = {
|
|
254
|
+
"xi-api-key": channel.apiKey.trim(),
|
|
255
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
256
|
+
"content-type": "application/json",
|
|
257
|
+
accept: "audio/mpeg, application/json"
|
|
258
|
+
};
|
|
259
|
+
if (request.mode === "voice_design") {
|
|
260
|
+
const endpoint = `${base}/text-to-voice/design`;
|
|
261
|
+
const previewText = request.previewText?.trim() ?? "";
|
|
262
|
+
const body = {
|
|
263
|
+
voice_description: request.prompt,
|
|
264
|
+
...previewText.length >= 100 ? { text: previewText } : { auto_generate_text: true }
|
|
265
|
+
};
|
|
266
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
redirect: "error",
|
|
269
|
+
headers,
|
|
270
|
+
body: JSON.stringify(body),
|
|
271
|
+
signal
|
|
272
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
const detail = await response.text().catch(() => "");
|
|
275
|
+
throw new AudioGenError(`ElevenLabs voice design API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
276
|
+
}
|
|
277
|
+
const previews = (await response.json()).previews ?? [];
|
|
278
|
+
if (previews.length === 0) throw new AudioGenError("ElevenLabs voice design returned no previews", "audio-empty-result");
|
|
279
|
+
const outputs = [];
|
|
280
|
+
for (const preview of previews) {
|
|
281
|
+
const encoded = preview.audio_base_64?.trim() ?? "";
|
|
282
|
+
if (encoded === "") continue;
|
|
283
|
+
const data = new Uint8Array(Buffer.from(encoded, "base64"));
|
|
284
|
+
outputs.push({
|
|
285
|
+
data,
|
|
286
|
+
mime: preview.media_type ?? "audio/mpeg",
|
|
287
|
+
...preview.generated_voice_id === void 0 || preview.generated_voice_id === "" ? {} : { voiceId: preview.generated_voice_id }
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (outputs.length === 0) throw new AudioGenError("ElevenLabs voice design returned no audio", "audio-empty-result");
|
|
291
|
+
return outputs;
|
|
292
|
+
}
|
|
293
|
+
if (request.mode === "music") {
|
|
294
|
+
const endpoint = `${base}/music`;
|
|
295
|
+
const body = {
|
|
296
|
+
model_id: (request.upstream ?? request.model) || "music_v1",
|
|
297
|
+
prompt: request.prompt,
|
|
298
|
+
...request.duration !== void 0 && Number.isFinite(request.duration) ? { music_length_ms: Math.round(Math.min(6e5, Math.max(3e3, request.duration * 1e3))) } : {},
|
|
299
|
+
...request.lyrics !== void 0 && request.lyrics.trim() !== "" ? { lyrics_text: request.lyrics.trim() } : {},
|
|
300
|
+
...request.isInstrumental !== void 0 ? { force_instrumental: request.isInstrumental } : {}
|
|
301
|
+
};
|
|
302
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
303
|
+
method: "POST",
|
|
304
|
+
redirect: "follow",
|
|
305
|
+
headers,
|
|
306
|
+
body: JSON.stringify(body),
|
|
307
|
+
signal
|
|
308
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
const detail = await response.text().catch(() => "");
|
|
311
|
+
throw new AudioGenError(`ElevenLabs music API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
312
|
+
}
|
|
313
|
+
return normalizeAudioResponse(response, {
|
|
314
|
+
apiKey: channel.apiKey,
|
|
315
|
+
fallbackMime: "audio/mpeg"
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (request.mode === "sfx") {
|
|
319
|
+
const endpoint = `${base}/sound-generation`;
|
|
320
|
+
const sfxModel = (request.upstream ?? request.model) || "eleven_text_to_sound_v2";
|
|
321
|
+
const body = {
|
|
322
|
+
text: request.prompt,
|
|
323
|
+
model_id: sfxModel,
|
|
324
|
+
...request.duration !== void 0 && Number.isFinite(request.duration) ? { duration_seconds: Math.min(30, Math.max(.5, request.duration)) } : {},
|
|
325
|
+
...request.loop !== void 0 ? { loop: request.loop } : {},
|
|
326
|
+
...request.promptInfluence !== void 0 && Number.isFinite(request.promptInfluence) ? { prompt_influence: Math.min(1, Math.max(0, request.promptInfluence)) } : {}
|
|
327
|
+
};
|
|
328
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
redirect: "follow",
|
|
331
|
+
headers,
|
|
332
|
+
body: JSON.stringify(body),
|
|
333
|
+
signal
|
|
334
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
const detail = await response.text().catch(() => "");
|
|
337
|
+
throw new AudioGenError(`ElevenLabs sound effects API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
338
|
+
}
|
|
339
|
+
return normalizeAudioResponse(response, {
|
|
340
|
+
apiKey: channel.apiKey,
|
|
341
|
+
fallbackMime: "audio/mpeg"
|
|
342
|
+
});
|
|
343
|
+
}
|
|
253
344
|
const voiceId = (request.voice ?? request.model ?? model).trim();
|
|
254
345
|
const endpoint = `${base}/text-to-speech/${encodeURIComponent(voiceId)}`;
|
|
255
346
|
const body = {
|
|
@@ -266,11 +357,7 @@ async function elevenLabs(channel, request, signal) {
|
|
|
266
357
|
return normalizeAudioResponse(await fetchWithTimeout(endpoint, {
|
|
267
358
|
method: "POST",
|
|
268
359
|
redirect: "error",
|
|
269
|
-
headers
|
|
270
|
-
"xi-api-key": channel.apiKey.trim(),
|
|
271
|
-
"content-type": "application/json",
|
|
272
|
-
accept: "audio/mpeg, application/json"
|
|
273
|
-
},
|
|
360
|
+
headers,
|
|
274
361
|
body: JSON.stringify(body),
|
|
275
362
|
signal
|
|
276
363
|
}, UPSTREAM_TIMEOUT_MS), {
|
|
@@ -563,7 +650,7 @@ async function generateAudio(channel, request, signal) {
|
|
|
563
650
|
if (channel.apiUrl.trim() === "") throw new AudioGenError("channel API URL is not configured", "audio-no-endpoint");
|
|
564
651
|
if (channel.apiKey.trim() === "") throw new AudioGenError("channel API key is not configured", "audio-no-key");
|
|
565
652
|
if (request.prompt.trim() === "") throw new AudioGenError("audio prompt/text is required", "audio-empty-prompt");
|
|
566
|
-
if (request.mode === "voice_design" && !isMiniMax$1(channel)) throw new AudioGenError("音色设计当前仅支持 MiniMax
|
|
653
|
+
if (request.mode === "voice_design" && !isMiniMax$1(channel) && !isElevenLabs$1(channel)) throw new AudioGenError("音色设计当前仅支持 MiniMax(/v1/voice_design)与 ElevenLabs(/v1/text-to-voice/design)渠道", "voice-design-unsupported");
|
|
567
654
|
if (isElevenLabs$1(channel)) return elevenLabs(channel, request, signal);
|
|
568
655
|
if (isMiniMax$1(channel)) return minimax(channel, request, signal);
|
|
569
656
|
if (isStability$1(channel)) return stabilityAudio(channel, request, signal);
|
|
@@ -642,7 +729,7 @@ const AUDIO_PRESETS = [
|
|
|
642
729
|
name: "ElevenLabs",
|
|
643
730
|
apiUrl: "https://api.elevenlabs.io/v1",
|
|
644
731
|
site: "https://elevenlabsai.cn",
|
|
645
|
-
hint: "ElevenLabs 语音合成(TTS
|
|
732
|
+
hint: "ElevenLabs 语音合成(TTS)与音乐生成(POST /v1/music,music_v2);可点「获取可用模型」拉取音色与模型",
|
|
646
733
|
models: [
|
|
647
734
|
{
|
|
648
735
|
alias: "Rachel",
|
|
@@ -678,6 +765,21 @@ const AUDIO_PRESETS = [
|
|
|
678
765
|
alias: "eleven_flash_v2_5",
|
|
679
766
|
id: "eleven_flash_v2_5",
|
|
680
767
|
category: "tts"
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
alias: "music_v2",
|
|
771
|
+
id: "music_v2",
|
|
772
|
+
category: "music"
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
alias: "music_v1",
|
|
776
|
+
id: "music_v1",
|
|
777
|
+
category: "music"
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
alias: "eleven_text_to_sound_v2",
|
|
781
|
+
id: "eleven_text_to_sound_v2",
|
|
782
|
+
category: "sfx"
|
|
681
783
|
}
|
|
682
784
|
]
|
|
683
785
|
},
|
|
@@ -1078,6 +1180,8 @@ function parseGenerateRequest(body) {
|
|
|
1078
1180
|
...num(body.duration) !== void 0 ? { duration: num(body.duration) } : {},
|
|
1079
1181
|
...typeof body.lyrics === "string" && body.lyrics.trim() !== "" ? { lyrics: body.lyrics.trim() } : {},
|
|
1080
1182
|
...typeof body.isInstrumental === "boolean" ? { isInstrumental: body.isInstrumental } : {},
|
|
1183
|
+
...typeof body.loop === "boolean" ? { loop: body.loop } : {},
|
|
1184
|
+
...num(body.promptInfluence) !== void 0 ? { promptInfluence: num(body.promptInfluence) } : {},
|
|
1081
1185
|
...typeof body.format === "string" && body.format.trim() !== "" ? { format: body.format.trim() } : {},
|
|
1082
1186
|
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
1083
1187
|
...str(body.emotion) !== void 0 ? { emotion: str(body.emotion) } : {},
|
|
@@ -1136,22 +1240,24 @@ function resolveChannelRequest(request, view) {
|
|
|
1136
1240
|
const defaults = view.channels.find((candidate) => candidate.id === view.defaultChannelId) ?? view.channels[0];
|
|
1137
1241
|
const target = explicit ?? defaults;
|
|
1138
1242
|
const asked = request.model.trim();
|
|
1243
|
+
if (request.mode === "voice_design") {
|
|
1244
|
+
if (target === void 0) return {
|
|
1245
|
+
ok: false,
|
|
1246
|
+
code: "no-channels",
|
|
1247
|
+
message: "尚未配置任何渠道"
|
|
1248
|
+
};
|
|
1249
|
+
return {
|
|
1250
|
+
ok: true,
|
|
1251
|
+
request: {
|
|
1252
|
+
...request,
|
|
1253
|
+
model: "",
|
|
1254
|
+
upstream: void 0,
|
|
1255
|
+
channelId: target.id,
|
|
1256
|
+
channel: target.name
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1139
1260
|
if (asked === "") {
|
|
1140
|
-
if (request.mode === "voice_design") {
|
|
1141
|
-
if (target === void 0) return {
|
|
1142
|
-
ok: false,
|
|
1143
|
-
code: "no-channels",
|
|
1144
|
-
message: "尚未配置任何渠道"
|
|
1145
|
-
};
|
|
1146
|
-
return {
|
|
1147
|
-
ok: true,
|
|
1148
|
-
request: {
|
|
1149
|
-
...request,
|
|
1150
|
-
channelId: target.id,
|
|
1151
|
-
channel: target.name
|
|
1152
|
-
}
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
1261
|
const alias = target?.models[0]?.alias ?? "";
|
|
1156
1262
|
if (alias === "") return {
|
|
1157
1263
|
ok: false,
|
|
@@ -1579,7 +1685,7 @@ function ensureConfigured(config) {
|
|
|
1579
1685
|
function registerAgentAudioTools(ctx, resolve) {
|
|
1580
1686
|
return ctx.tools.register(defineTool({
|
|
1581
1687
|
name: "generate_audio",
|
|
1582
|
-
description: "Generate audio with the configured audio provider. Supports text-to-speech, music generation, sound effects and MiniMax voice
|
|
1688
|
+
description: "Generate audio with the configured audio provider. Supports text-to-speech, music generation, sound effects and voice design (MiniMax /v1/voice_design, ElevenLabs /v1/text-to-voice/design). The tool call waits for the upstream result and returns same-origin audio URLs; pass those URLs to the user for playback or download. If multiple models are configured, first ask the user which one to use or pass model explicitly.",
|
|
1583
1689
|
parameters: {
|
|
1584
1690
|
prompt: {
|
|
1585
1691
|
type: "string",
|
|
@@ -1624,6 +1730,14 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1624
1730
|
type: "boolean",
|
|
1625
1731
|
description: "Generate purely instrumental music without vocals/lyrics (MiniMax is_instrumental). When true, lyrics may be omitted."
|
|
1626
1732
|
},
|
|
1733
|
+
loop: {
|
|
1734
|
+
type: "boolean",
|
|
1735
|
+
description: "Create a seamlessly looping sound effect (ElevenLabs sound generation loop, only for eleven_text_to_sound_v2)."
|
|
1736
|
+
},
|
|
1737
|
+
prompt_influence: {
|
|
1738
|
+
type: "number",
|
|
1739
|
+
description: "Sound effect prompt influence 0-1 (ElevenLabs prompt_influence, default 0.3): higher follows the prompt more closely, lower is more variable."
|
|
1740
|
+
},
|
|
1627
1741
|
format: {
|
|
1628
1742
|
type: "string",
|
|
1629
1743
|
description: "Output format such as mp3 or wav. MiniMax music supports mp3/wav/pcm."
|
|
@@ -1655,11 +1769,11 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1655
1769
|
},
|
|
1656
1770
|
sample_rate: {
|
|
1657
1771
|
type: "integer",
|
|
1658
|
-
description: "MiniMax
|
|
1772
|
+
description: "MiniMax sample rate: music 16000/24000/32000/44100 (default 44100); tts default 32000 (audio_setting.sample_rate)."
|
|
1659
1773
|
},
|
|
1660
1774
|
bitrate: {
|
|
1661
1775
|
type: "integer",
|
|
1662
|
-
description: "MiniMax
|
|
1776
|
+
description: "MiniMax bitrate in bps: 32000/64000/128000/256000 (music default 256000, tts default 128000; audio_setting.bitrate)."
|
|
1663
1777
|
},
|
|
1664
1778
|
channel: {
|
|
1665
1779
|
type: "integer",
|
|
@@ -1764,6 +1878,8 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1764
1878
|
...typeof args.duration === "number" ? { duration: args.duration } : {},
|
|
1765
1879
|
...typeof args.lyrics === "string" && args.lyrics.trim() !== "" ? { lyrics: args.lyrics.trim() } : {},
|
|
1766
1880
|
...typeof args.is_instrumental === "boolean" ? { isInstrumental: args.is_instrumental } : {},
|
|
1881
|
+
...typeof args.loop === "boolean" ? { loop: args.loop } : {},
|
|
1882
|
+
...typeof args.prompt_influence === "number" && Number.isFinite(args.prompt_influence) ? { promptInfluence: args.prompt_influence } : {},
|
|
1767
1883
|
...typeof args.format === "string" && args.format.trim() !== "" ? { format: args.format.trim() } : {},
|
|
1768
1884
|
...typeof args.emotion === "string" && args.emotion.trim() !== "" ? { emotion: args.emotion.trim() } : {},
|
|
1769
1885
|
...typeof args.vol === "number" && Number.isFinite(args.vol) ? { vol: args.vol } : {},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-audiogen",
|
|
3
3
|
"description": "AI audio generation plugin for the dsh web GUI: multi-vendor TTS/music/sound-effect channels (OpenAI-compatible, ElevenLabs, MiniMax, Stability AI and custom), per-channel model/voice catalogs, Agent tool and a sidebar AI 音频 panel.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/skills/design/SKILL.md
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
- 帮助用户把音色或音效需求细化成可复用的描述,供后续 TTS/music/sfx 生成。
|
|
8
8
|
- 输出不建议直接生成音频,而是给出音色参数建议和可用的模型/音色清单。
|
|
9
9
|
|
|
10
|
+
## 面板/工具的音色设计(voice_design)
|
|
11
|
+
- 渠道支持:MiniMax(POST /v1/voice_design,prompt + preview_text)与 ElevenLabs(POST /v1/text-to-voice/design)。
|
|
12
|
+
- MiniMax 参数:`prompt`(音色描述)、`preview_text`(试听文本,任意长度)。
|
|
13
|
+
- ElevenLabs 参数:`voice_description`(音色描述);试听文本 `preview_text` 需 100-1000 字符,过短时自动 `auto_generate_text`;响应含 `previews[].audio_base_64` 与 `generated_voice_id`(可后续在 TTS 中复用该 voice_id)。
|
|
14
|
+
- 面板音色设计模式顶部可切换「厂商 / 渠道」。
|
|
15
|
+
|
|
10
16
|
## 流程
|
|
11
17
|
1. 询问目标风格、音域、情绪、适用场景。
|
|
12
18
|
2. 生成结构化的音色描述(如温暖复古合成器、未来感 UI 提示音)。
|
|
13
|
-
3. 在后续生成中复用该描述,必要时通过 `generate_audio
|
|
19
|
+
3. 在后续生成中复用该描述,必要时通过 `generate_audio`(mode=voice_design)试听。
|
package/skills/music/SKILL.md
CHANGED
|
@@ -37,3 +37,16 @@
|
|
|
37
37
|
## 常见错误
|
|
38
38
|
- `lyrics-required`:MiniMax 音乐生成需要歌词,或开启纯音乐。
|
|
39
39
|
- `HTTP 400` 且含 `2013`:上游参数不合法,检查 lyrics / audio_setting 枚举。
|
|
40
|
+
|
|
41
|
+
## ElevenLabs Music(POST /v1/music,模型 music_v1 / music_v2)
|
|
42
|
+
|
|
43
|
+
| 字段 | 工具/面板参数 | 说明 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| model_id | model(music_v2 等) | 官方枚举:music_v1 / music_v2,默认 music_v1 |
|
|
46
|
+
| prompt | prompt | 音乐/歌词主题描述(不能与 composition_plan 同用;引擎用 prompt) |
|
|
47
|
+
| music_length_ms | duration(秒,自动×1000) | 3000ms - 600000ms(3s-600s),超出自动收敛区间 |
|
|
48
|
+
| lyrics_text | lyrics(歌词) | 选填歌词文本 |
|
|
49
|
+
| force_instrumental | is_instrumental(纯音乐) | true 保证无演唱(无词) |
|
|
50
|
+
| seed / generation_mode / finetune_* | — | 高级字段,暂未透出 |
|
|
51
|
+
|
|
52
|
+
> 响应为音频字节流(audio/*,常为 mp3)。请求同时携带 `xi-api-key` 与 `Authorization: Bearer`,以兼容 New API 类网关(官方站任一头即可)。
|
package/skills/sfx/SKILL.md
CHANGED
|
@@ -2,15 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## 触发
|
|
4
4
|
- `/audio:sfx <描述>`
|
|
5
|
-
- 用户说“生成一个音效 / 提示音 /
|
|
5
|
+
- 用户说“生成一个音效 / 提示音 / 环境音”
|
|
6
6
|
|
|
7
7
|
## 参数
|
|
8
8
|
- prompt: 必填,音效描述
|
|
9
|
-
- model:
|
|
10
|
-
- duration:
|
|
9
|
+
- model: 可选,已配置模型(ElevenLabs:eleven_text_to_sound_v2)
|
|
10
|
+
- duration: 可选,秒数(ElevenLabs 0.5-30,留空则自动)
|
|
11
|
+
- loop: 可选,ElevenLabs 无缝循环音效(仅 eleven_text_to_sound_v2)
|
|
12
|
+
- prompt_influence: 可选,ElevenLabs 提示词影响度 0-1(默认 0.3,越高越贴近提示词、越少随机)
|
|
11
13
|
- format: 可选
|
|
12
14
|
|
|
13
15
|
## 流程
|
|
14
16
|
1. 确认已配置音效生成渠道。
|
|
15
17
|
2. 调用 `generate_audio`,mode=sfx。
|
|
16
18
|
3. 将音频 URL 返回。
|
|
19
|
+
|
|
20
|
+
## ElevenLabs Sound Generation(POST /v1/sound-generation)
|
|
21
|
+
|
|
22
|
+
| 字段 | 工具/面板参数 | 说明 |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| text | prompt | 必填,转换为音效的文本/描述 |
|
|
25
|
+
| model_id | model | 官方枚举:eleven_text_to_sound_v2(默认) |
|
|
26
|
+
| duration_seconds | duration | 0.5-30 秒;留空由提示词推算最优时长 |
|
|
27
|
+
| loop | loop | 是否生成平滑循环音效(仅 eleven_text_to_sound_v2) |
|
|
28
|
+
| prompt_influence | prompt_influence | 0-1,默认 0.3;越高越贴提示词,越低越多样 |
|
|
29
|
+
|
|
30
|
+
> 响应为 audio/mpeg 二进制。请求同时携带 `xi-api-key` 与 `Authorization: Bearer`,兼容 New API 类网关。
|
package/src/agent-audio-tools.ts
CHANGED
|
@@ -100,7 +100,7 @@ function ensureConfigured(config: AgentAudioToolConfig): void {
|
|
|
100
100
|
export function registerAgentAudioTools(ctx: Context, resolve: () => AgentAudioToolConfig): () => void {
|
|
101
101
|
const disposer = ctx.tools.register(defineTool({
|
|
102
102
|
name: 'generate_audio',
|
|
103
|
-
description: 'Generate audio with the configured audio provider. Supports text-to-speech, music generation, sound effects and MiniMax voice
|
|
103
|
+
description: 'Generate audio with the configured audio provider. Supports text-to-speech, music generation, sound effects and voice design (MiniMax /v1/voice_design, ElevenLabs /v1/text-to-voice/design). The tool call waits for the upstream result and returns same-origin audio URLs; pass those URLs to the user for playback or download. If multiple models are configured, first ask the user which one to use or pass model explicitly.',
|
|
104
104
|
parameters: {
|
|
105
105
|
prompt: { type: 'string', required: true, description: 'For tts, the text to speak. For music/sfx, a descriptive prompt.' },
|
|
106
106
|
mode: { type: 'string', enum: ['tts', 'music', 'sfx', 'voice_design'], description: 'Generation mode. Defaults to tts.' },
|
|
@@ -111,6 +111,8 @@ export function registerAgentAudioTools(ctx: Context, resolve: () => AgentAudioT
|
|
|
111
111
|
duration: { type: 'number', description: 'Requested duration in seconds for music/sfx.' },
|
|
112
112
|
lyrics: { type: 'string', description: 'Lyrics for music generation (MiniMax music-3.0/music-cover). Required unless is_instrumental is true. Split verses with an empty line.' },
|
|
113
113
|
is_instrumental: { type: 'boolean', description: 'Generate purely instrumental music without vocals/lyrics (MiniMax is_instrumental). When true, lyrics may be omitted.' },
|
|
114
|
+
loop: { type: 'boolean', description: 'Create a seamlessly looping sound effect (ElevenLabs sound generation loop, only for eleven_text_to_sound_v2).' },
|
|
115
|
+
prompt_influence: { type: 'number', description: 'Sound effect prompt influence 0-1 (ElevenLabs prompt_influence, default 0.3): higher follows the prompt more closely, lower is more variable.' },
|
|
114
116
|
format: { type: 'string', description: 'Output format such as mp3 or wav. MiniMax music supports mp3/wav/pcm.' },
|
|
115
117
|
// ---- MiniMax TTS only (ignored by other providers) ----
|
|
116
118
|
emotion: { type: 'string', description: 'MiniMax TTS emotion, e.g. happy/sad/angry/nervous/fearful/bored (voice_setting.emotion).' },
|
|
@@ -119,8 +121,8 @@ export function registerAgentAudioTools(ctx: Context, resolve: () => AgentAudioT
|
|
|
119
121
|
text_normalization: { type: 'boolean', description: 'MiniMax TTS text normalization switch (voice_setting.text_normalization).' },
|
|
120
122
|
latex_read: { type: 'boolean', description: 'MiniMax TTS math formula reading switch (voice_setting.latex_read).' },
|
|
121
123
|
pronunciation_tone: { type: 'array', items: { type: 'string' }, description: 'MiniMax TTS pronunciation dictionary tone entries, each "word/pronunciation", e.g. ["处理/(chu3)(li3)", "危险/dangerous"] (pronunciation_dict.tone).' },
|
|
122
|
-
sample_rate: { type: 'integer', description: 'MiniMax
|
|
123
|
-
bitrate: { type: 'integer', description: 'MiniMax
|
|
124
|
+
sample_rate: { type: 'integer', description: 'MiniMax sample rate: music 16000/24000/32000/44100 (default 44100); tts default 32000 (audio_setting.sample_rate).' },
|
|
125
|
+
bitrate: { type: 'integer', description: 'MiniMax bitrate in bps: 32000/64000/128000/256000 (music default 256000, tts default 128000; audio_setting.bitrate).' },
|
|
124
126
|
channel: { type: 'integer', description: 'MiniMax TTS audio channels: 1 or 2, default 1 (audio_setting.channel).' },
|
|
125
127
|
force_cbr: { type: 'boolean', description: 'MiniMax TTS force CBR encoding (audio_setting.force_cbr).' },
|
|
126
128
|
subtitle_enable: { type: 'boolean', description: 'MiniMax TTS subtitle output switch (subtitle_enable).' },
|
|
@@ -199,6 +201,8 @@ export function registerAgentAudioTools(ctx: Context, resolve: () => AgentAudioT
|
|
|
199
201
|
...(typeof args.duration === 'number' ? { duration: args.duration } : {}),
|
|
200
202
|
...(typeof args.lyrics === 'string' && args.lyrics.trim() !== '' ? { lyrics: args.lyrics.trim() } : {}),
|
|
201
203
|
...(typeof args.is_instrumental === 'boolean' ? { isInstrumental: args.is_instrumental } : {}),
|
|
204
|
+
...(typeof args.loop === 'boolean' ? { loop: args.loop } : {}),
|
|
205
|
+
...(typeof args.prompt_influence === 'number' && Number.isFinite(args.prompt_influence) ? { promptInfluence: args.prompt_influence } : {}),
|
|
202
206
|
...(typeof args.format === 'string' && args.format.trim() !== '' ? { format: args.format.trim() } : {}),
|
|
203
207
|
// ---- MiniMax TTS 专属字段 ----
|
|
204
208
|
...(typeof args.emotion === 'string' && args.emotion.trim() !== '' ? { emotion: args.emotion.trim() } : {}),
|
package/src/audio-engine.ts
CHANGED
|
@@ -242,6 +242,114 @@ async function openAITTS(channel: AudioChannel, request: GenerateAudioRequest, s
|
|
|
242
242
|
async function elevenLabs(channel: AudioChannel, request: GenerateAudioRequest, signal?: AbortSignal): Promise<Array<{ data: Uint8Array; mime: string; voiceId?: string }>> {
|
|
243
243
|
const base = endpointBase(channel.apiUrl)
|
|
244
244
|
const model = (request.upstream ?? request.model) || 'eleven_multilingual_v2'
|
|
245
|
+
// 官方使用 xi-api-key;额外携带 Authorization Bearer 以兼容 New API 类网关。
|
|
246
|
+
const headers = {
|
|
247
|
+
'xi-api-key': channel.apiKey.trim(),
|
|
248
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
249
|
+
'content-type': 'application/json',
|
|
250
|
+
accept: 'audio/mpeg, application/json',
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ------------- ElevenLabs Voice Design(POST /v1/text-to-voice/design) -------------
|
|
254
|
+
// voice_description 必填;text 100-1000 字符,过短时用 auto_generate_text;
|
|
255
|
+
// 返回 previews[].audio_base_64 与 previews[].generated_voice_id。
|
|
256
|
+
if (request.mode === 'voice_design') {
|
|
257
|
+
const endpoint = `${base}/text-to-voice/design`
|
|
258
|
+
const previewText = request.previewText?.trim() ?? ''
|
|
259
|
+
const body: Record<string, unknown> = {
|
|
260
|
+
voice_description: request.prompt,
|
|
261
|
+
...(previewText.length >= 100 ? { text: previewText } : { auto_generate_text: true }),
|
|
262
|
+
}
|
|
263
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
redirect: 'error',
|
|
266
|
+
headers,
|
|
267
|
+
body: JSON.stringify(body),
|
|
268
|
+
signal,
|
|
269
|
+
}, UPSTREAM_TIMEOUT_MS)
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
const detail = await response.text().catch(() => '')
|
|
272
|
+
throw new AudioGenError(`ElevenLabs voice design API error (HTTP ${response.status})${detail === '' ? '' : `: ${detail.slice(0, 300)}`}`, 'audio-api-error')
|
|
273
|
+
}
|
|
274
|
+
const payload = await response.json() as {
|
|
275
|
+
previews?: Array<{ audio_base_64?: string; generated_voice_id?: string; media_type?: string }>
|
|
276
|
+
}
|
|
277
|
+
const previews = payload.previews ?? []
|
|
278
|
+
if (previews.length === 0) throw new AudioGenError('ElevenLabs voice design returned no previews', 'audio-empty-result')
|
|
279
|
+
const outputs: Array<{ data: Uint8Array; mime: string; voiceId?: string }> = []
|
|
280
|
+
for (const preview of previews) {
|
|
281
|
+
const encoded = preview.audio_base_64?.trim() ?? ''
|
|
282
|
+
if (encoded === '') continue
|
|
283
|
+
const data = new Uint8Array(Buffer.from(encoded, 'base64'))
|
|
284
|
+
outputs.push({
|
|
285
|
+
data,
|
|
286
|
+
mime: preview.media_type ?? 'audio/mpeg',
|
|
287
|
+
...(preview.generated_voice_id === undefined || preview.generated_voice_id === '' ? {} : { voiceId: preview.generated_voice_id }),
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
if (outputs.length === 0) throw new AudioGenError('ElevenLabs voice design returned no audio', 'audio-empty-result')
|
|
291
|
+
return outputs
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ------------- ElevenLabs Music(POST /v1/music) -------------
|
|
295
|
+
// 模型:music_v1 / music_v2;prompt 与 composition_plan 二选一(引擎用 prompt)。
|
|
296
|
+
if (request.mode === 'music') {
|
|
297
|
+
const endpoint = `${base}/music`
|
|
298
|
+
const musicModel = (request.upstream ?? request.model) || 'music_v1'
|
|
299
|
+
const body: Record<string, unknown> = {
|
|
300
|
+
model_id: musicModel,
|
|
301
|
+
prompt: request.prompt,
|
|
302
|
+
...(request.duration !== undefined && Number.isFinite(request.duration)
|
|
303
|
+
? { music_length_ms: Math.round(Math.min(600_000, Math.max(3_000, request.duration * 1000))) }
|
|
304
|
+
: {}),
|
|
305
|
+
...(request.lyrics !== undefined && request.lyrics.trim() !== '' ? { lyrics_text: request.lyrics.trim() } : {}),
|
|
306
|
+
...(request.isInstrumental !== undefined ? { force_instrumental: request.isInstrumental } : {}),
|
|
307
|
+
}
|
|
308
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
redirect: 'follow',
|
|
311
|
+
headers,
|
|
312
|
+
body: JSON.stringify(body),
|
|
313
|
+
signal,
|
|
314
|
+
}, UPSTREAM_TIMEOUT_MS)
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
const detail = await response.text().catch(() => '')
|
|
317
|
+
throw new AudioGenError(`ElevenLabs music API error (HTTP ${response.status})${detail === '' ? '' : `: ${detail.slice(0, 300)}`}`, 'audio-api-error')
|
|
318
|
+
}
|
|
319
|
+
return normalizeAudioResponse(response, { apiKey: channel.apiKey, fallbackMime: 'audio/mpeg' })
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ------------- ElevenLabs Sound Effects(POST /v1/sound-generation) -------------
|
|
323
|
+
// 官方模型:eleven_text_to_sound_v2;text 必填;duration_seconds 0.5-30;
|
|
324
|
+
// loop 仅该模型可用;prompt_influence 0-1(默认 0.3)。
|
|
325
|
+
if (request.mode === 'sfx') {
|
|
326
|
+
const endpoint = `${base}/sound-generation`
|
|
327
|
+
const sfxModel = (request.upstream ?? request.model) || 'eleven_text_to_sound_v2'
|
|
328
|
+
const body: Record<string, unknown> = {
|
|
329
|
+
text: request.prompt,
|
|
330
|
+
model_id: sfxModel,
|
|
331
|
+
...(request.duration !== undefined && Number.isFinite(request.duration)
|
|
332
|
+
? { duration_seconds: Math.min(30, Math.max(0.5, request.duration)) }
|
|
333
|
+
: {}),
|
|
334
|
+
...(request.loop !== undefined ? { loop: request.loop } : {}),
|
|
335
|
+
...(request.promptInfluence !== undefined && Number.isFinite(request.promptInfluence)
|
|
336
|
+
? { prompt_influence: Math.min(1, Math.max(0, request.promptInfluence)) }
|
|
337
|
+
: {}),
|
|
338
|
+
}
|
|
339
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
340
|
+
method: 'POST',
|
|
341
|
+
redirect: 'follow',
|
|
342
|
+
headers,
|
|
343
|
+
body: JSON.stringify(body),
|
|
344
|
+
signal,
|
|
345
|
+
}, UPSTREAM_TIMEOUT_MS)
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
const detail = await response.text().catch(() => '')
|
|
348
|
+
throw new AudioGenError(`ElevenLabs sound effects API error (HTTP ${response.status})${detail === '' ? '' : `: ${detail.slice(0, 300)}`}`, 'audio-api-error')
|
|
349
|
+
}
|
|
350
|
+
return normalizeAudioResponse(response, { apiKey: channel.apiKey, fallbackMime: 'audio/mpeg' })
|
|
351
|
+
}
|
|
352
|
+
|
|
245
353
|
const voiceId = (request.voice ?? request.model ?? model).trim()
|
|
246
354
|
const endpoint = `${base}/text-to-speech/${encodeURIComponent(voiceId)}`
|
|
247
355
|
const body: Record<string, unknown> = {
|
|
@@ -258,11 +366,7 @@ async function elevenLabs(channel: AudioChannel, request: GenerateAudioRequest,
|
|
|
258
366
|
const response = await fetchWithTimeout(endpoint, {
|
|
259
367
|
method: 'POST',
|
|
260
368
|
redirect: 'error',
|
|
261
|
-
headers
|
|
262
|
-
'xi-api-key': channel.apiKey.trim(),
|
|
263
|
-
'content-type': 'application/json',
|
|
264
|
-
accept: 'audio/mpeg, application/json',
|
|
265
|
-
},
|
|
369
|
+
headers,
|
|
266
370
|
body: JSON.stringify(body),
|
|
267
371
|
signal,
|
|
268
372
|
}, UPSTREAM_TIMEOUT_MS)
|
|
@@ -591,8 +695,8 @@ export async function generateAudio(
|
|
|
591
695
|
if (channel.apiUrl.trim() === '') throw new AudioGenError('channel API URL is not configured', 'audio-no-endpoint')
|
|
592
696
|
if (channel.apiKey.trim() === '') throw new AudioGenError('channel API key is not configured', 'audio-no-key')
|
|
593
697
|
if (request.prompt.trim() === '') throw new AudioGenError('audio prompt/text is required', 'audio-empty-prompt')
|
|
594
|
-
if (request.mode === 'voice_design' && !isMiniMax(channel)) {
|
|
595
|
-
throw new AudioGenError('音色设计当前仅支持 MiniMax
|
|
698
|
+
if (request.mode === 'voice_design' && !isMiniMax(channel) && !isElevenLabs(channel)) {
|
|
699
|
+
throw new AudioGenError('音色设计当前仅支持 MiniMax(/v1/voice_design)与 ElevenLabs(/v1/text-to-voice/design)渠道', 'voice-design-unsupported')
|
|
596
700
|
}
|
|
597
701
|
|
|
598
702
|
if (isElevenLabs(channel)) return elevenLabs(channel, request, signal)
|
package/src/audio-presets.ts
CHANGED
|
@@ -53,7 +53,7 @@ export const AUDIO_PRESETS: AudioPresetProvider[] = [
|
|
|
53
53
|
name: 'ElevenLabs',
|
|
54
54
|
apiUrl: 'https://api.elevenlabs.io/v1',
|
|
55
55
|
site: 'https://elevenlabsai.cn',
|
|
56
|
-
hint: 'ElevenLabs 语音合成(TTS
|
|
56
|
+
hint: 'ElevenLabs 语音合成(TTS)与音乐生成(POST /v1/music,music_v2);可点「获取可用模型」拉取音色与模型',
|
|
57
57
|
models: [
|
|
58
58
|
{ alias: 'Rachel', id: '21m00Tcm4TlvDq8ikWAM', category: 'tts' },
|
|
59
59
|
{ alias: 'Adam', id: 'pNInz6obpgDQGcFmaJgB', category: 'tts' },
|
|
@@ -62,6 +62,11 @@ export const AUDIO_PRESETS: AudioPresetProvider[] = [
|
|
|
62
62
|
{ alias: 'eleven_multilingual_v2', id: 'eleven_multilingual_v2', category: 'tts' },
|
|
63
63
|
{ alias: 'eleven_turbo_v2_5', id: 'eleven_turbo_v2_5', category: 'tts' },
|
|
64
64
|
{ alias: 'eleven_flash_v2_5', id: 'eleven_flash_v2_5', category: 'tts' },
|
|
65
|
+
// ElevenLabs Music(POST /v1/music)
|
|
66
|
+
{ alias: 'music_v2', id: 'music_v2', category: 'music' },
|
|
67
|
+
{ alias: 'music_v1', id: 'music_v1', category: 'music' },
|
|
68
|
+
// ElevenLabs Sound Effects(POST /v1/sound-generation)
|
|
69
|
+
{ alias: 'eleven_text_to_sound_v2', id: 'eleven_text_to_sound_v2', category: 'sfx' },
|
|
65
70
|
],
|
|
66
71
|
},
|
|
67
72
|
{
|
|
@@ -62,6 +62,9 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
62
62
|
const [duration, setDuration] = useState('')
|
|
63
63
|
const [lyrics, setLyrics] = useState('')
|
|
64
64
|
const [instrumental, setInstrumental] = useState(false)
|
|
65
|
+
// ElevenLabs 音效参数
|
|
66
|
+
const [loop, setLoop] = useState(false)
|
|
67
|
+
const [promptInfluence, setPromptInfluence] = useState('')
|
|
65
68
|
const [format, setFormat] = useState('mp3')
|
|
66
69
|
// MiniMax TTS 高级参数(其他厂商忽略)
|
|
67
70
|
const [emotion, setEmotion] = useState('')
|
|
@@ -76,12 +79,21 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
76
79
|
const [error, setError] = useState<string | null>(null)
|
|
77
80
|
const [outputs, setOutputs] = useState<GeneratedAudio[]>([])
|
|
78
81
|
const { entries, reload, clear } = useHistory()
|
|
82
|
+
// 音色设计模式的厂商/渠道选择(默认渠道)
|
|
83
|
+
const [designChannelId, setDesignChannelId] = useState('')
|
|
79
84
|
|
|
80
85
|
const isMiniMaxChannel = useMemo(() => {
|
|
81
86
|
const target = channels.find(candidate => candidate.id === modelOptions.defaultChannelId) ?? channels[0]
|
|
82
87
|
return target !== undefined && (target.preset === 'minimax' || /minimax/i.test(target.apiUrl))
|
|
83
88
|
}, [channels, modelOptions.defaultChannelId])
|
|
84
89
|
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (channels.length === 0) return
|
|
92
|
+
if (designChannelId === '' || !channels.some(candidate => candidate.id === designChannelId)) {
|
|
93
|
+
setDesignChannelId(modelOptions.defaultChannelId ?? channels[0]!.id)
|
|
94
|
+
}
|
|
95
|
+
}, [channels, modelOptions.defaultChannelId, designChannelId])
|
|
96
|
+
|
|
85
97
|
const visibleModels = useMemo(() => {
|
|
86
98
|
if (mode === 'voice_design') return []
|
|
87
99
|
return modelOptions.models
|
|
@@ -105,14 +117,17 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
105
117
|
try {
|
|
106
118
|
const response = await api.generate({
|
|
107
119
|
mode,
|
|
108
|
-
model: (model || visibleModels[0]) ?? '',
|
|
120
|
+
model: mode === 'voice_design' ? '' : (model || visibleModels[0]) ?? '',
|
|
109
121
|
prompt: prompt.trim(),
|
|
122
|
+
...(mode === 'voice_design' && designChannelId !== '' ? { channelId: designChannelId } : {}),
|
|
110
123
|
...(previewText.trim() !== '' ? { previewText: previewText.trim() } : {}),
|
|
111
124
|
...(voice.trim() !== '' ? { voice: voice.trim() } : {}),
|
|
112
125
|
...(speed.trim() !== '' ? { speed: Number(speed) } : {}),
|
|
113
126
|
...(duration.trim() !== '' ? { duration: Number(duration) } : {}),
|
|
114
127
|
...(lyrics.trim() !== '' ? { lyrics: lyrics.trim() } : {}),
|
|
115
128
|
...(instrumental ? { isInstrumental: true } : {}),
|
|
129
|
+
...(loop ? { loop: true } : {}),
|
|
130
|
+
...(promptInfluence.trim() !== '' ? { promptInfluence: Number(promptInfluence) } : {}),
|
|
116
131
|
...(format.trim() !== '' ? { format: format.trim() } : {}),
|
|
117
132
|
...(emotion.trim() !== '' ? { emotion: emotion.trim() } : {}),
|
|
118
133
|
...(vol.trim() !== '' ? { vol: Number(vol) } : {}),
|
|
@@ -173,10 +188,24 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
173
188
|
</label>
|
|
174
189
|
|
|
175
190
|
{mode === 'voice_design' ? (
|
|
176
|
-
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
<>
|
|
192
|
+
<label className={css.label}>
|
|
193
|
+
<span>厂商 / 渠道</span>
|
|
194
|
+
<select className={css.select} value={designChannelId} onChange={event => setDesignChannelId(event.target.value)}>
|
|
195
|
+
{channels.length === 0 ? <option value="">(尚未配置渠道)</option> : null}
|
|
196
|
+
{channels.map(candidate => (
|
|
197
|
+
<option key={candidate.id} value={candidate.id}>
|
|
198
|
+
{candidate.name}({candidate.preset === 'minimax' ? 'MiniMax' : candidate.preset === 'elevenlabs' ? 'ElevenLabs' : candidate.preset || '自定义'})
|
|
199
|
+
</option>
|
|
200
|
+
))}
|
|
201
|
+
</select>
|
|
202
|
+
</label>
|
|
203
|
+
<p className={css.hint}>MiniMax:/v1/voice_design;ElevenLabs:/v1/text-to-voice/design(试听文本 100-1000 字符,过短将自动生成)</p>
|
|
204
|
+
<label className={css.label}>
|
|
205
|
+
<span>试听文本</span>
|
|
206
|
+
<input className={css.input} value={previewText} onChange={event => setPreviewText(event.target.value)} placeholder="你好,这是新设计的音色试听。" />
|
|
207
|
+
</label>
|
|
208
|
+
</>
|
|
180
209
|
) : null}
|
|
181
210
|
|
|
182
211
|
{needModel ? (
|
|
@@ -256,6 +285,19 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
256
285
|
</label>
|
|
257
286
|
) : null}
|
|
258
287
|
|
|
288
|
+
{mode === 'sfx' ? (
|
|
289
|
+
<>
|
|
290
|
+
<label className={css.checkbox}>
|
|
291
|
+
<input type="checkbox" checked={loop} onChange={event => setLoop(event.target.checked)} />
|
|
292
|
+
<span>循环音效 loop(无缝循环,需 eleven_text_to_sound_v2)</span>
|
|
293
|
+
</label>
|
|
294
|
+
<label className={css.label}>
|
|
295
|
+
<span>提示词影响度 prompt_influence (0-1)</span>
|
|
296
|
+
<input className={css.input} type="number" step="0.1" min="0" max="1" value={promptInfluence} onChange={event => setPromptInfluence(event.target.value)} placeholder="0.3" />
|
|
297
|
+
</label>
|
|
298
|
+
</>
|
|
299
|
+
) : null}
|
|
300
|
+
|
|
259
301
|
{mode === 'music' ? (
|
|
260
302
|
<>
|
|
261
303
|
<label className={css.label}>
|
|
@@ -266,6 +308,28 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
266
308
|
<input type="checkbox" checked={instrumental} onChange={event => setInstrumental(event.target.checked)} />
|
|
267
309
|
<span>纯音乐(无歌词/人声)is_instrumental</span>
|
|
268
310
|
</label>
|
|
311
|
+
<div className={css.row}>
|
|
312
|
+
<label className={css.label}>
|
|
313
|
+
<span>采样率</span>
|
|
314
|
+
<select className={css.select} value={sampleRate} onChange={event => setSampleRate(event.target.value)}>
|
|
315
|
+
<option value="">默认(44100)</option>
|
|
316
|
+
<option value="16000">16000</option>
|
|
317
|
+
<option value="24000">24000</option>
|
|
318
|
+
<option value="32000">32000</option>
|
|
319
|
+
<option value="44100">44100</option>
|
|
320
|
+
</select>
|
|
321
|
+
</label>
|
|
322
|
+
<label className={css.label}>
|
|
323
|
+
<span>码率 bps</span>
|
|
324
|
+
<select className={css.select} value={bitrate} onChange={event => setBitrate(event.target.value)}>
|
|
325
|
+
<option value="">默认(256000)</option>
|
|
326
|
+
<option value="32000">32000</option>
|
|
327
|
+
<option value="64000">64000</option>
|
|
328
|
+
<option value="128000">128000</option>
|
|
329
|
+
<option value="256000">256000</option>
|
|
330
|
+
</select>
|
|
331
|
+
</label>
|
|
332
|
+
</div>
|
|
269
333
|
</>
|
|
270
334
|
) : null}
|
|
271
335
|
|