dsh-audiogen 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +1069 -550
- package/lib/client.js.map +1 -1
- package/lib/index.js +483 -118
- package/package.json +1 -1
- package/skills/music/SKILL.md +30 -7
- package/skills/tts/SKILL.md +42 -4
- package/src/agent-audio-tools.ts +80 -3
- package/src/audio-engine.ts +185 -26
- package/src/audio-models.ts +132 -24
- package/src/audio-presets.ts +27 -33
- package/src/client/AudioGenPanel.tsx +92 -3
- package/src/client/SettingsCard.tsx +520 -176
- package/src/client/audio-panel.module.css +36 -0
- package/src/client/locales.ts +84 -16
- package/src/client/settings-card.module.css +424 -607
- package/src/protocol.ts +39 -1
- package/src/routes.ts +44 -3
package/src/audio-models.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host-side model/voice discovery.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Each vendor answers differently:
|
|
5
|
+
* - MiniMax exposes a voice-management API (/v1/get_voice) that returns all
|
|
6
|
+
* system and user-generated voice ids; the known MiniMax music models are
|
|
7
|
+
* appended so the settings card can offer a full categorized catalog.
|
|
8
|
+
* - ElevenLabs exposes /v1/models and /v1/voices; only models that can
|
|
9
|
+
* actually speak (text_to_speech capability) and account voices are kept.
|
|
10
|
+
* - Stability AI has no listing endpoint; the built-in stable-audio catalog
|
|
11
|
+
* is returned.
|
|
12
|
+
* - Generic OpenAI-compatible endpoints answer /models; the reply is filtered
|
|
13
|
+
* to audio-related model ids only (tts / music / sfx), never the whole
|
|
14
|
+
* model list of a gateway.
|
|
7
15
|
*/
|
|
8
16
|
|
|
9
17
|
import type { AudioChannel } from './audio-engine.ts'
|
|
@@ -14,20 +22,38 @@ function isMiniMax(channel: AudioChannel): boolean {
|
|
|
14
22
|
return channel.preset === 'minimax' || /minimax/i.test(channel.apiUrl)
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
function isElevenLabs(channel: AudioChannel): boolean {
|
|
26
|
+
return channel.preset === 'elevenlabs' || /elevenlabs/i.test(channel.apiUrl)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isStability(channel: AudioChannel): boolean {
|
|
30
|
+
return channel.preset === 'stability' || /stability\.ai/i.test(channel.apiUrl)
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
function baseUrl(url: string): string {
|
|
18
34
|
return url.trim().replace(/\/+$/, '')
|
|
19
35
|
}
|
|
20
36
|
|
|
37
|
+
/** Whether an upstream model id is audio-related at all. */
|
|
21
38
|
function categoryFor(id: string): AudioModelCategory | undefined {
|
|
22
39
|
const value = id.toLowerCase()
|
|
23
|
-
if (/(tts|speech|voice|t2a)/i.test(value)) return 'tts'
|
|
24
|
-
if (/(music|song|cover|lyrics)/i.test(value)) return 'music'
|
|
40
|
+
if (/(tts|speech|voice|t2a|talk|narration)/i.test(value)) return 'tts'
|
|
41
|
+
if (/(music|song|cover|lyrics|audio|melody|beat)/i.test(value)) return 'music'
|
|
25
42
|
if (/(sfx|sound.?effect|effect|foley)/i.test(value)) return 'sfx'
|
|
26
43
|
return undefined
|
|
27
44
|
}
|
|
28
45
|
|
|
46
|
+
async function fetchJson(url: string, init: RequestInit): Promise<unknown> {
|
|
47
|
+
const response = await fetch(url, init)
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const text = await response.text().catch(() => '')
|
|
50
|
+
throw new Error(`HTTP ${response.status}${text === '' ? '' : `: ${text.slice(0, 300)}`}`)
|
|
51
|
+
}
|
|
52
|
+
return response.json()
|
|
53
|
+
}
|
|
54
|
+
|
|
29
55
|
async function postJson(url: string, apiKey: string, body: unknown): Promise<unknown> {
|
|
30
|
-
|
|
56
|
+
return fetchJson(url, {
|
|
31
57
|
method: 'POST',
|
|
32
58
|
headers: {
|
|
33
59
|
authorization: `Bearer ${apiKey.trim()}`,
|
|
@@ -35,11 +61,6 @@ async function postJson(url: string, apiKey: string, body: unknown): Promise<unk
|
|
|
35
61
|
},
|
|
36
62
|
body: JSON.stringify(body),
|
|
37
63
|
})
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
const text = await response.text().catch(() => '')
|
|
40
|
-
throw new Error(`HTTP ${response.status}${text === '' ? '' : `: ${text.slice(0, 300)}`}`)
|
|
41
|
-
}
|
|
42
|
-
return response.json()
|
|
43
64
|
}
|
|
44
65
|
|
|
45
66
|
/** Discover available models/voices for a channel. */
|
|
@@ -47,9 +68,18 @@ export async function discoverAudioModels(channel: AudioChannel): Promise<{ mode
|
|
|
47
68
|
if (channel.apiUrl.trim() === '') throw new Error('API URL is not configured')
|
|
48
69
|
if (channel.apiKey.trim() === '') throw new Error('API key is not configured')
|
|
49
70
|
|
|
50
|
-
if (isMiniMax(channel))
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
if (isMiniMax(channel)) return discoverMiniMax(channel)
|
|
72
|
+
if (isElevenLabs(channel)) return discoverElevenLabs(channel)
|
|
73
|
+
if (isStability(channel)) return discoverStability(channel)
|
|
74
|
+
return discoverOpenAICompatible(channel)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------- MiniMax
|
|
78
|
+
|
|
79
|
+
async function discoverMiniMax(channel: AudioChannel): Promise<{ models: DiscoveredAudioModel[]; source: string }> {
|
|
80
|
+
const base = baseUrl(channel.apiUrl).replace(/\/v1$/i, '')
|
|
81
|
+
const url = `${base}/v1/get_voice`
|
|
82
|
+
try {
|
|
53
83
|
const payload = await postJson(url, channel.apiKey, { voice_type: 'all' }) as {
|
|
54
84
|
system_voice?: Array<{ voice_id?: string; voice_name?: string; description?: string[] }>
|
|
55
85
|
voice_cloning?: Array<{ voice_id?: string; description?: string[] }>
|
|
@@ -94,27 +124,105 @@ export async function discoverAudioModels(channel: AudioChannel): Promise<{ mode
|
|
|
94
124
|
const music = (audioPresetById('minimax')?.models ?? []).filter(model => model.category === 'music')
|
|
95
125
|
for (const model of music) models.push({ ...model, category: 'music' as const })
|
|
96
126
|
const deduped = dedupe(models)
|
|
97
|
-
return { models: deduped, source: 'MiniMax get_voice +
|
|
127
|
+
return { models: deduped, source: 'MiniMax get_voice + music 目录' }
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Gateways may not route /v1/get_voice (or lack voice-management access).
|
|
130
|
+
// Fall back to the built-in catalog so generation still works.
|
|
131
|
+
const fallback = (audioPresetById('minimax')?.models ?? []).map(model => ({ ...model }))
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
133
|
+
return {
|
|
134
|
+
models: dedupe(fallback),
|
|
135
|
+
source: `内置 MiniMax 目录(音色发现失败:${message.slice(0, 160)})`,
|
|
136
|
+
}
|
|
98
137
|
}
|
|
138
|
+
}
|
|
99
139
|
|
|
100
|
-
|
|
140
|
+
// ------------------------------------------------------------- ElevenLabs
|
|
141
|
+
|
|
142
|
+
async function discoverElevenLabs(channel: AudioChannel): Promise<{ models: DiscoveredAudioModel[]; source: string }> {
|
|
143
|
+
const base = baseUrl(channel.apiUrl)
|
|
144
|
+
const headers = { 'xi-api-key': channel.apiKey.trim() }
|
|
145
|
+
const failures: string[] = []
|
|
146
|
+
const models: DiscoveredAudioModel[] = []
|
|
147
|
+
|
|
148
|
+
// 1. TTS-capable models from /v1/models (audio-related only).
|
|
149
|
+
try {
|
|
150
|
+
const payload = await fetchJson(`${base}/models`, { headers }) as Array<{
|
|
151
|
+
model_id?: string
|
|
152
|
+
name?: string
|
|
153
|
+
description?: string
|
|
154
|
+
capabilities?: { text_to_speech?: boolean; voice_change?: boolean; speech_to_text?: boolean }
|
|
155
|
+
}>
|
|
156
|
+
for (const item of Array.isArray(payload) ? payload : []) {
|
|
157
|
+
const id = item.model_id?.trim() ?? ''
|
|
158
|
+
if (id === '') continue
|
|
159
|
+
// Only what actually produces speech audio.
|
|
160
|
+
if (item.capabilities?.text_to_speech !== true && item.capabilities?.voice_change !== true) continue
|
|
161
|
+
models.push({
|
|
162
|
+
alias: item.name?.trim() || id,
|
|
163
|
+
id,
|
|
164
|
+
category: 'tts',
|
|
165
|
+
...(item.description !== undefined && item.description.trim() !== '' ? { description: item.description.trim() } : {}),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
failures.push(`模型列表:${error instanceof Error ? error.message : String(error)}`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 2. The account's voices from /v1/voices, grouped as tts entries.
|
|
173
|
+
try {
|
|
174
|
+
const payload = await fetchJson(`${base}/voices`, { headers }) as {
|
|
175
|
+
voices?: Array<{ voice_id?: string; name?: string; description?: string }>
|
|
176
|
+
}
|
|
177
|
+
for (const voice of Array.isArray(payload?.voices) ? payload.voices : []) {
|
|
178
|
+
const id = voice.voice_id?.trim() ?? ''
|
|
179
|
+
if (id === '') continue
|
|
180
|
+
models.push({
|
|
181
|
+
alias: voice.name?.trim() || id,
|
|
182
|
+
id,
|
|
183
|
+
category: 'tts',
|
|
184
|
+
...(voice.description !== undefined && voice.description.trim() !== '' ? { description: voice.description.trim() } : {}),
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
failures.push(`音色列表:${error instanceof Error ? error.message : String(error)}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (models.length === 0) {
|
|
192
|
+
// Neither endpoint answered — fall back to the built-in catalog.
|
|
193
|
+
const fallback = (audioPresetById('elevenlabs')?.models ?? []).map(model => ({ ...model }))
|
|
194
|
+
const detail = failures.length === 0 ? '' : `(发现失败:${failures.join(';').slice(0, 160)})`
|
|
195
|
+
return { models: dedupe(fallback), source: `内置 ElevenLabs 目录${detail}` }
|
|
196
|
+
}
|
|
197
|
+
return { models: dedupe(models), source: 'ElevenLabs /models + /voices' }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -------------------------------------------------------------- Stability
|
|
201
|
+
|
|
202
|
+
async function discoverStability(channel: AudioChannel): Promise<{ models: DiscoveredAudioModel[]; source: string }> {
|
|
203
|
+
// Stability has no public audio model listing; serve the built-in catalog.
|
|
204
|
+
const fallback = (audioPresetById('stability-audio')?.models ?? []).map(model => ({ ...model }))
|
|
205
|
+
return { models: dedupe(fallback), source: 'Stability stable-audio 内置目录' }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ------------------------------------------------------ OpenAI-compatible
|
|
209
|
+
|
|
210
|
+
async function discoverOpenAICompatible(channel: AudioChannel): Promise<{ models: DiscoveredAudioModel[]; source: string }> {
|
|
101
211
|
const base = baseUrl(channel.apiUrl)
|
|
102
212
|
const url = `${base}/models`
|
|
103
|
-
const
|
|
213
|
+
const payload = await fetchJson(url, {
|
|
104
214
|
headers: { authorization: `Bearer ${channel.apiKey.trim()}` },
|
|
105
|
-
})
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
throw new Error(`model list request failed (HTTP ${response.status}); please add models manually`)
|
|
108
|
-
}
|
|
109
|
-
const payload = await response.json() as { data?: Array<{ id?: string }> }
|
|
215
|
+
}) as { data?: Array<{ id?: string }> }
|
|
110
216
|
const models: DiscoveredAudioModel[] = []
|
|
111
217
|
for (const item of payload.data ?? []) {
|
|
112
218
|
const id = item.id?.trim() ?? ''
|
|
113
219
|
if (id === '') continue
|
|
114
|
-
|
|
220
|
+
// Audio-related models only — never the whole gateway model list.
|
|
221
|
+
const category = categoryFor(id)
|
|
222
|
+
if (category === undefined) continue
|
|
115
223
|
models.push({ alias: id, id, category })
|
|
116
224
|
}
|
|
117
|
-
return { models: dedupe(models), source: 'OpenAI-compatible /models' }
|
|
225
|
+
return { models: dedupe(models), source: 'OpenAI-compatible /models(仅音频相关)' }
|
|
118
226
|
}
|
|
119
227
|
|
|
120
228
|
function dedupe(models: DiscoveredAudioModel[]): DiscoveredAudioModel[] {
|
package/src/audio-presets.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in audio provider catalog (presets).
|
|
3
3
|
* Framework-free pure data; served to the settings card through a host route.
|
|
4
|
+
*
|
|
5
|
+
* Only the officially supported audio vendors are offered here — MiniMax,
|
|
6
|
+
* ElevenLabs and Stability AI. Any other endpoint (including OpenAI-compatible
|
|
7
|
+
* TTS gateways) is added through the "自定义渠道" flow instead.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import type { ModelMapping } from './protocol.ts'
|
|
@@ -15,39 +19,19 @@ export interface AudioPresetProvider {
|
|
|
15
19
|
apiUrl: string
|
|
16
20
|
/** One-line description shown in the picker. */
|
|
17
21
|
hint: string
|
|
22
|
+
/** Official vendor website, shown as a link in the channel editor. */
|
|
23
|
+
site?: string
|
|
18
24
|
/** Known model/voice catalog prefilled into the channel. */
|
|
19
25
|
models: ModelMapping[]
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export const AUDIO_PRESETS: AudioPresetProvider[] = [
|
|
23
|
-
{
|
|
24
|
-
id: 'openai-tts',
|
|
25
|
-
name: 'OpenAI · TTS',
|
|
26
|
-
apiUrl: 'https://api.openai.com/v1',
|
|
27
|
-
hint: 'OpenAI 官方语音合成接口(/audio/speech)',
|
|
28
|
-
models: [
|
|
29
|
-
{ alias: 'tts-1', id: 'tts-1', category: 'tts' },
|
|
30
|
-
{ alias: 'tts-1-hd', id: 'tts-1-hd', category: 'tts' },
|
|
31
|
-
{ alias: 'gpt-4o-mini-tts', id: 'gpt-4o-mini-tts', category: 'tts' },
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: 'elevenlabs',
|
|
36
|
-
name: 'ElevenLabs',
|
|
37
|
-
apiUrl: 'https://api.elevenlabs.io/v1',
|
|
38
|
-
hint: 'ElevenLabs TTS;模型列表请填写你的 Voice ID(如 Rachel / Adam 等别名)',
|
|
39
|
-
models: [
|
|
40
|
-
{ alias: 'Rachel', id: '21m00Tcm4TlvDq8ikWAM', category: 'tts' },
|
|
41
|
-
{ alias: 'Adam', id: 'pNInz6obpgDQGcFmaJgB', category: 'tts' },
|
|
42
|
-
{ alias: 'Antoni', id: 'ErXwobaYiN019PkySvjV', category: 'tts' },
|
|
43
|
-
{ alias: 'Bella', id: 'EXAVITQu4vr4xnSDxMaL', category: 'tts' },
|
|
44
|
-
],
|
|
45
|
-
},
|
|
46
29
|
{
|
|
47
30
|
id: 'minimax',
|
|
48
31
|
name: 'MiniMax',
|
|
49
32
|
apiUrl: 'https://api.minimaxi.com',
|
|
50
|
-
|
|
33
|
+
site: 'https://www.minimaxi.com',
|
|
34
|
+
hint: 'MiniMax 官方音频:音色设计 / TTS / 音乐生成;建议点击「获取可用模型」拉取账号音色与模型',
|
|
51
35
|
models: [
|
|
52
36
|
// TTS models
|
|
53
37
|
{ alias: 'speech-2.8-hd', id: 'speech-2.8-hd', category: 'tts' },
|
|
@@ -64,23 +48,33 @@ export const AUDIO_PRESETS: AudioPresetProvider[] = [
|
|
|
64
48
|
{ alias: 'music-cover', id: 'music-cover', category: 'music' },
|
|
65
49
|
],
|
|
66
50
|
},
|
|
51
|
+
{
|
|
52
|
+
id: 'elevenlabs',
|
|
53
|
+
name: 'ElevenLabs',
|
|
54
|
+
apiUrl: 'https://api.elevenlabs.io/v1',
|
|
55
|
+
site: 'https://elevenlabsai.cn',
|
|
56
|
+
hint: 'ElevenLabs 语音合成(TTS);建议点击「获取可用模型」拉取音色与模型',
|
|
57
|
+
models: [
|
|
58
|
+
{ alias: 'Rachel', id: '21m00Tcm4TlvDq8ikWAM', category: 'tts' },
|
|
59
|
+
{ alias: 'Adam', id: 'pNInz6obpgDQGcFmaJgB', category: 'tts' },
|
|
60
|
+
{ alias: 'Antoni', id: 'ErXwobaYiN019PkySvjV', category: 'tts' },
|
|
61
|
+
{ alias: 'Bella', id: 'EXAVITQu4vr4xnSDxMaL', category: 'tts' },
|
|
62
|
+
{ alias: 'eleven_multilingual_v2', id: 'eleven_multilingual_v2', category: 'tts' },
|
|
63
|
+
{ alias: 'eleven_turbo_v2_5', id: 'eleven_turbo_v2_5', category: 'tts' },
|
|
64
|
+
{ alias: 'eleven_flash_v2_5', id: 'eleven_flash_v2_5', category: 'tts' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
67
|
{
|
|
68
68
|
id: 'stability-audio',
|
|
69
|
-
name: 'Stability AI
|
|
69
|
+
name: 'Stability AI(stable-audio)',
|
|
70
70
|
apiUrl: 'https://api.stability.ai/v2beta/audio',
|
|
71
|
-
|
|
71
|
+
site: 'https://stability.ai/stable-audio',
|
|
72
|
+
hint: 'Stability AI 音乐 / 音效生成(stable-audio 系列)',
|
|
72
73
|
models: [
|
|
73
74
|
{ alias: 'stable-audio-2.0', id: 'stable-audio-2.0', category: 'music' },
|
|
74
75
|
{ alias: 'stable-audio-1.0', id: 'stable-audio-1.0', category: 'music' },
|
|
75
76
|
],
|
|
76
77
|
},
|
|
77
|
-
{
|
|
78
|
-
id: 'custom',
|
|
79
|
-
name: '自定义渠道',
|
|
80
|
-
apiUrl: '',
|
|
81
|
-
hint: '任意兼容接口;支持 OpenAI 兼容 TTS,或返回音频字节 / JSON 的通用 POST',
|
|
82
|
-
models: [],
|
|
83
|
-
},
|
|
84
78
|
]
|
|
85
79
|
|
|
86
80
|
/** Look up one built-in provider by id. */
|
|
@@ -60,12 +60,28 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
60
60
|
const [voice, setVoice] = useState('')
|
|
61
61
|
const [speed, setSpeed] = useState('')
|
|
62
62
|
const [duration, setDuration] = useState('')
|
|
63
|
+
const [lyrics, setLyrics] = useState('')
|
|
64
|
+
const [instrumental, setInstrumental] = useState(false)
|
|
63
65
|
const [format, setFormat] = useState('mp3')
|
|
66
|
+
// MiniMax TTS 高级参数(其他厂商忽略)
|
|
67
|
+
const [emotion, setEmotion] = useState('')
|
|
68
|
+
const [vol, setVol] = useState('')
|
|
69
|
+
const [pitch, setPitch] = useState('')
|
|
70
|
+
const [toneText, setToneText] = useState('')
|
|
71
|
+
const [sampleRate, setSampleRate] = useState('')
|
|
72
|
+
const [bitrate, setBitrate] = useState('')
|
|
73
|
+
const [audioChannel, setAudioChannel] = useState('')
|
|
74
|
+
const [subtitle, setSubtitle] = useState(false)
|
|
64
75
|
const [loading, setLoading] = useState(false)
|
|
65
76
|
const [error, setError] = useState<string | null>(null)
|
|
66
77
|
const [outputs, setOutputs] = useState<GeneratedAudio[]>([])
|
|
67
78
|
const { entries, reload, clear } = useHistory()
|
|
68
79
|
|
|
80
|
+
const isMiniMaxChannel = useMemo(() => {
|
|
81
|
+
const target = channels.find(candidate => candidate.id === modelOptions.defaultChannelId) ?? channels[0]
|
|
82
|
+
return target !== undefined && (target.preset === 'minimax' || /minimax/i.test(target.apiUrl))
|
|
83
|
+
}, [channels, modelOptions.defaultChannelId])
|
|
84
|
+
|
|
69
85
|
const visibleModels = useMemo(() => {
|
|
70
86
|
if (mode === 'voice_design') return []
|
|
71
87
|
return modelOptions.models
|
|
@@ -95,7 +111,17 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
95
111
|
...(voice.trim() !== '' ? { voice: voice.trim() } : {}),
|
|
96
112
|
...(speed.trim() !== '' ? { speed: Number(speed) } : {}),
|
|
97
113
|
...(duration.trim() !== '' ? { duration: Number(duration) } : {}),
|
|
114
|
+
...(lyrics.trim() !== '' ? { lyrics: lyrics.trim() } : {}),
|
|
115
|
+
...(instrumental ? { isInstrumental: true } : {}),
|
|
98
116
|
...(format.trim() !== '' ? { format: format.trim() } : {}),
|
|
117
|
+
...(emotion.trim() !== '' ? { emotion: emotion.trim() } : {}),
|
|
118
|
+
...(vol.trim() !== '' ? { vol: Number(vol) } : {}),
|
|
119
|
+
...(pitch.trim() !== '' ? { pitch: Number(pitch) } : {}),
|
|
120
|
+
...(toneText.trim() !== '' ? { pronunciationTone: toneText.split('\n').map(item => item.trim()).filter(item => item !== '') } : {}),
|
|
121
|
+
...(sampleRate.trim() !== '' ? { sampleRate: Number(sampleRate) } : {}),
|
|
122
|
+
...(bitrate.trim() !== '' ? { bitrate: Number(bitrate) } : {}),
|
|
123
|
+
...(audioChannel.trim() !== '' ? { audioChannel: Number(audioChannel) } : {}),
|
|
124
|
+
...(subtitle ? { subtitleEnable: true } : {}),
|
|
99
125
|
})
|
|
100
126
|
if (!response.ok) {
|
|
101
127
|
setError(response.message ?? '生成失败')
|
|
@@ -166,7 +192,7 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
166
192
|
{mode === 'tts' ? (
|
|
167
193
|
<label className={css.label}>
|
|
168
194
|
<span>{tt('voice.label')}</span>
|
|
169
|
-
<input className={css.input} value={voice} onChange={event => setVoice(event.target.value)} placeholder=
|
|
195
|
+
<input className={css.input} value={voice} onChange={event => setVoice(event.target.value)} placeholder={isMiniMaxChannel ? 'male-qn-qingse / female-shaonv' : 'alloy / 自定义音色'} />
|
|
170
196
|
</label>
|
|
171
197
|
) : null}
|
|
172
198
|
|
|
@@ -177,6 +203,52 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
177
203
|
</label>
|
|
178
204
|
) : null}
|
|
179
205
|
|
|
206
|
+
{mode === 'tts' && isMiniMaxChannel ? (
|
|
207
|
+
<details className={css.advanced}>
|
|
208
|
+
<summary>MiniMax 高级参数</summary>
|
|
209
|
+
<label className={css.label}>
|
|
210
|
+
<span>情绪 emotion</span>
|
|
211
|
+
<input className={css.input} value={emotion} onChange={event => setEmotion(event.target.value)} placeholder="happy / sad / angry / nervous…" />
|
|
212
|
+
</label>
|
|
213
|
+
<div className={css.row}>
|
|
214
|
+
<label className={css.label}>
|
|
215
|
+
<span>音量 vol (0-10)</span>
|
|
216
|
+
<input className={css.input} type="number" min="0" max="10" step="0.5" value={vol} onChange={event => setVol(event.target.value)} placeholder="1" />
|
|
217
|
+
</label>
|
|
218
|
+
<label className={css.label}>
|
|
219
|
+
<span>音调 pitch (-12~12)</span>
|
|
220
|
+
<input className={css.input} type="number" min="-12" max="12" value={pitch} onChange={event => setPitch(event.target.value)} placeholder="0" />
|
|
221
|
+
</label>
|
|
222
|
+
</div>
|
|
223
|
+
<div className={css.row}>
|
|
224
|
+
<label className={css.label}>
|
|
225
|
+
<span>采样率</span>
|
|
226
|
+
<input className={css.input} type="number" min="16000" max="48000" step="8000" value={sampleRate} onChange={event => setSampleRate(event.target.value)} placeholder="32000" />
|
|
227
|
+
</label>
|
|
228
|
+
<label className={css.label}>
|
|
229
|
+
<span>码率 bps</span>
|
|
230
|
+
<input className={css.input} type="number" min="64000" max="320000" step="8000" value={bitrate} onChange={event => setBitrate(event.target.value)} placeholder="128000" />
|
|
231
|
+
</label>
|
|
232
|
+
<label className={css.label}>
|
|
233
|
+
<span>声道</span>
|
|
234
|
+
<select className={css.select} value={audioChannel} onChange={event => setAudioChannel(event.target.value)}>
|
|
235
|
+
<option value="">默认(1)</option>
|
|
236
|
+
<option value="1">1</option>
|
|
237
|
+
<option value="2">2</option>
|
|
238
|
+
</select>
|
|
239
|
+
</label>
|
|
240
|
+
</div>
|
|
241
|
+
<label className={css.label}>
|
|
242
|
+
<span>发音词典(每行一条:"文字/读音")</span>
|
|
243
|
+
<textarea className={css.textarea} value={toneText} onChange={event => setToneText(event.target.value)} placeholder={'处理/(chu3)(li3)\n危险/dangerous'} />
|
|
244
|
+
</label>
|
|
245
|
+
<label className={css.checkbox}>
|
|
246
|
+
<input type="checkbox" checked={subtitle} onChange={event => setSubtitle(event.target.checked)} />
|
|
247
|
+
<span>生成字幕 subtitle_enable</span>
|
|
248
|
+
</label>
|
|
249
|
+
</details>
|
|
250
|
+
) : null}
|
|
251
|
+
|
|
180
252
|
{mode === 'music' || mode === 'sfx' ? (
|
|
181
253
|
<label className={css.label}>
|
|
182
254
|
<span>{tt('duration.label')}</span>
|
|
@@ -184,14 +256,31 @@ export function AudioGenPanel(props: { api: AudiogenApi; scope: AudiogenScope })
|
|
|
184
256
|
</label>
|
|
185
257
|
) : null}
|
|
186
258
|
|
|
259
|
+
{mode === 'music' ? (
|
|
260
|
+
<>
|
|
261
|
+
<label className={css.label}>
|
|
262
|
+
<span>歌词(纯音乐模式可留空;多段用空行分隔)</span>
|
|
263
|
+
<textarea className={css.textarea} value={lyrics} onChange={event => setLyrics(event.target.value)} placeholder={'第一段歌词…\n\n第二段歌词…'} />
|
|
264
|
+
</label>
|
|
265
|
+
<label className={css.checkbox}>
|
|
266
|
+
<input type="checkbox" checked={instrumental} onChange={event => setInstrumental(event.target.checked)} />
|
|
267
|
+
<span>纯音乐(无歌词/人声)is_instrumental</span>
|
|
268
|
+
</label>
|
|
269
|
+
</>
|
|
270
|
+
) : null}
|
|
271
|
+
|
|
187
272
|
{needModel ? (
|
|
188
273
|
<label className={css.label}>
|
|
189
274
|
<span>{tt('format.label')}</span>
|
|
190
275
|
<select className={css.select} value={format} onChange={event => setFormat(event.target.value)}>
|
|
191
276
|
<option value="mp3">mp3</option>
|
|
192
277
|
<option value="wav">wav</option>
|
|
193
|
-
|
|
194
|
-
|
|
278
|
+
{mode === 'tts' ? (
|
|
279
|
+
<>
|
|
280
|
+
<option value="flac">flac</option>
|
|
281
|
+
<option value="ogg">ogg</option>
|
|
282
|
+
</>
|
|
283
|
+
) : null}
|
|
195
284
|
<option value="pcm">pcm</option>
|
|
196
285
|
</select>
|
|
197
286
|
</label>
|