dsh-audiogen 0.4.19 → 0.4.22
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 +4 -3
- package/README.zh-CN.md +4 -3
- package/lib/client.js +1003 -407
- package/lib/client.js.map +1 -1
- package/lib/index.js +1327 -50
- package/package.json +1 -1
- package/skills/voice-cast/SKILL.md +151 -0
- package/skills/voice-management/SKILL.md +37 -2
- package/src/agent-audio-tools.ts +261 -13
- package/src/client/AudioGenPanel.tsx +1 -1
- package/src/client/api.ts +55 -0
- package/src/client/audio-panel.module.css +67 -0
- package/src/client/field-specs.ts +4 -2
- package/src/client/icons.tsx +13 -0
- package/src/client/studio-view.tsx +275 -34
- package/src/client/voices-view.tsx +129 -17
- package/src/client/voices.module.css +165 -0
- package/src/index.ts +16 -1
- package/src/protocol.ts +62 -3
- package/src/routes.ts +157 -27
- package/src/voice-cast.ts +712 -0
- package/src/voice-manager.ts +82 -7
- package/src/voice-recommend.ts +350 -0
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
import { useEffect, useMemo, useState } from 'react'
|
|
13
13
|
import type { AudiogenApi } from './api.ts'
|
|
14
14
|
import type { AudiogenScope } from './settings-scope.ts'
|
|
15
|
-
import type { VoiceEntry } from '../protocol.ts'
|
|
15
|
+
import type { VoiceEntry, VoiceRecommendRecord } from '../protocol.ts'
|
|
16
16
|
import { tt } from './helpers.ts'
|
|
17
17
|
import { AudioPlayer } from './audio-player.tsx'
|
|
18
|
-
import { MicIcon, SearchIcon, TrashIcon, CheckIcon } from './icons.tsx'
|
|
18
|
+
import { MicIcon, SearchIcon, TrashIcon, CheckIcon, ListIcon } from './icons.tsx'
|
|
19
19
|
import css from './voices.module.css'
|
|
20
20
|
|
|
21
21
|
export interface VoicesReusePayload {
|
|
@@ -29,8 +29,26 @@ const SOURCE_LABELS: Record<VoiceEntry['source'], string> = {
|
|
|
29
29
|
custom: '自建设计',
|
|
30
30
|
owned: '账户自有',
|
|
31
31
|
shared: '社区共享',
|
|
32
|
+
configured: '渠道模型',
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/** 语言下拉:ISO 639-1 值;后端按两套厂商词汇匹配(ElevenLabs ISO / MiniMax 前缀)。 */
|
|
36
|
+
const LANGUAGE_OPTIONS: Array<{ value: string; label: string }> = [
|
|
37
|
+
{ value: '', label: '全部' },
|
|
38
|
+
{ value: 'zh', label: '中文(普通话 / 粤语)' },
|
|
39
|
+
{ value: 'en', label: '英语 English' },
|
|
40
|
+
{ value: 'ja', label: '日语 Japanese' },
|
|
41
|
+
{ value: 'ko', label: '韩语 Korean' },
|
|
42
|
+
{ value: 'es', label: '西班牙语 Spanish' },
|
|
43
|
+
{ value: 'fr', label: '法语 French' },
|
|
44
|
+
{ value: 'de', label: '德语 German' },
|
|
45
|
+
{ value: 'ru', label: '俄语 Russian' },
|
|
46
|
+
{ value: 'it', label: '意大利语 Italian' },
|
|
47
|
+
{ value: 'pt', label: '葡萄牙语 Portuguese' },
|
|
48
|
+
{ value: 'ar', label: '阿拉伯语 Arabic' },
|
|
49
|
+
{ value: 'hi', label: '印地语 Hindi' },
|
|
50
|
+
]
|
|
51
|
+
|
|
34
52
|
function useConfig(scope: AudiogenScope) {
|
|
35
53
|
const [value, setValue] = useState(scope.getSnapshot().value)
|
|
36
54
|
useEffect(() => scope.subscribe(() => { setValue(scope.getSnapshot().value) }), [scope])
|
|
@@ -82,6 +100,44 @@ export function VoicesView(props: {
|
|
|
82
100
|
const [note, setNote] = useState<string | null>(null)
|
|
83
101
|
const [deletingId, setDeletingId] = useState<string | null>(null)
|
|
84
102
|
|
|
103
|
+
// AI 推荐记录(每次推荐自动落盘,最多保留 50 条;关闭面板后仍可回看)
|
|
104
|
+
const [records, setRecords] = useState<VoiceRecommendRecord[]>([])
|
|
105
|
+
const [recordsOpen, setRecordsOpen] = useState(false)
|
|
106
|
+
|
|
107
|
+
const loadRecords = async (): Promise<void> => {
|
|
108
|
+
try {
|
|
109
|
+
const response = await api.voiceRecommendHistory(30)
|
|
110
|
+
setRecords(response.entries ?? [])
|
|
111
|
+
} catch {
|
|
112
|
+
// best-effort:记录加载失败不打扰主流程
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const removeRecord = async (id: string): Promise<void> => {
|
|
117
|
+
await api.voiceRecommendHistoryRemove(id).catch(() => { /* best-effort */ })
|
|
118
|
+
void loadRecords()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
useEffect(() => { void loadRecords() }, [api])
|
|
122
|
+
|
|
123
|
+
/** 当前的筛选载荷(查询音色列表用)。 */
|
|
124
|
+
const currentFilterPayload = (): Record<string, unknown> => ({
|
|
125
|
+
channel: channelId,
|
|
126
|
+
...(language.trim() === '' ? {} : { language: language.trim() }),
|
|
127
|
+
...(keyword.trim() === '' ? {} : { keyword: keyword.trim() }),
|
|
128
|
+
...(source === '' ? {} : { source }),
|
|
129
|
+
...(search.trim() === '' ? {} : { search: search.trim() }),
|
|
130
|
+
...(useCase.trim() === '' ? {} : { use_case: useCase.trim() }),
|
|
131
|
+
...(accent.trim() === '' ? {} : { accent: accent.trim() }),
|
|
132
|
+
...(gender.trim() === '' ? {} : { gender: gender.trim() }),
|
|
133
|
+
...(age.trim() === '' ? {} : { age: age.trim() }),
|
|
134
|
+
...(locale.trim() === '' ? {} : { locale: locale.trim() }),
|
|
135
|
+
...(category.trim() === '' ? {} : { category: category.trim() }),
|
|
136
|
+
...(sort === '' ? {} : { sort }),
|
|
137
|
+
...(featured ? { featured: true } : {}),
|
|
138
|
+
...(freeUsersAllowed ? { free_users_allowed: true } : {}),
|
|
139
|
+
})
|
|
140
|
+
|
|
85
141
|
const load = async (): Promise<void> => {
|
|
86
142
|
const channel = channels.find(item => item.id === channelId)
|
|
87
143
|
if (channel === undefined) return
|
|
@@ -90,20 +146,7 @@ export function VoicesView(props: {
|
|
|
90
146
|
setNote(null)
|
|
91
147
|
try {
|
|
92
148
|
const response = await api.voiceList({
|
|
93
|
-
|
|
94
|
-
...(language.trim() === '' ? {} : { language: language.trim() }),
|
|
95
|
-
...(keyword.trim() === '' ? {} : { keyword: keyword.trim() }),
|
|
96
|
-
...(source === '' ? {} : { source }),
|
|
97
|
-
...(search.trim() === '' ? {} : { search: search.trim() }),
|
|
98
|
-
...(useCase.trim() === '' ? {} : { use_case: useCase.trim() }),
|
|
99
|
-
...(accent.trim() === '' ? {} : { accent: accent.trim() }),
|
|
100
|
-
...(gender.trim() === '' ? {} : { gender: gender.trim() }),
|
|
101
|
-
...(age.trim() === '' ? {} : { age: age.trim() }),
|
|
102
|
-
...(locale.trim() === '' ? {} : { locale: locale.trim() }),
|
|
103
|
-
...(category.trim() === '' ? {} : { category: category.trim() }),
|
|
104
|
-
...(sort === '' ? {} : { sort }),
|
|
105
|
-
...(featured ? { featured: true } : {}),
|
|
106
|
-
...(freeUsersAllowed ? { free_users_allowed: true } : {}),
|
|
149
|
+
...currentFilterPayload(),
|
|
107
150
|
limit: 500,
|
|
108
151
|
})
|
|
109
152
|
if (response.ok) {
|
|
@@ -158,6 +201,20 @@ export function VoicesView(props: {
|
|
|
158
201
|
})
|
|
159
202
|
}
|
|
160
203
|
|
|
204
|
+
/** 从推荐记录里复用音色:切到记录对应的渠道,再回填 TTS 表单。 */
|
|
205
|
+
const reuseFromRecord = (record: VoiceRecommendRecord, voice: { voice_id: string }): void => {
|
|
206
|
+
if (record.channel_id !== undefined && record.channel_id !== '' && channels.some(item => item.id === record.channel_id)) {
|
|
207
|
+
setChannelId(record.channel_id)
|
|
208
|
+
}
|
|
209
|
+
const channel = channels.find(item => item.id === (record.channel_id ?? channelId))
|
|
210
|
+
const ttsModel = channel?.models.find(model => model.category === 'tts' || /tts|speech|voice|t2a/i.test(model.alias))
|
|
211
|
+
onReuseVoice({
|
|
212
|
+
mode: 'tts',
|
|
213
|
+
voiceId: voice.voice_id,
|
|
214
|
+
...(ttsModel === undefined || ttsModel.alias === '' ? {} : { model: ttsModel.alias }),
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
161
218
|
return (
|
|
162
219
|
<div className={css.view}>
|
|
163
220
|
<div className={css.toolbar}>
|
|
@@ -171,7 +228,11 @@ export function VoicesView(props: {
|
|
|
171
228
|
</label>
|
|
172
229
|
<label className={css.field}>
|
|
173
230
|
<span className={css.fieldLabel}>语言</span>
|
|
174
|
-
<
|
|
231
|
+
<select className={css.select} value={language} onChange={event => setLanguage(event.target.value)}>
|
|
232
|
+
{LANGUAGE_OPTIONS.map(option => (
|
|
233
|
+
<option key={option.value} value={option.value}>{option.label}</option>
|
|
234
|
+
))}
|
|
235
|
+
</select>
|
|
175
236
|
</label>
|
|
176
237
|
<label className={css.field}>
|
|
177
238
|
<span className={css.fieldLabel}>关键词</span>
|
|
@@ -192,6 +253,57 @@ export function VoicesView(props: {
|
|
|
192
253
|
</button>
|
|
193
254
|
</div>
|
|
194
255
|
|
|
256
|
+
<p className={css.stateNote}>
|
|
257
|
+
按需求描述(prompt)推荐音色请使用生成页左侧「🎤 音色推荐」模式;这里仅浏览/筛选/删除音色,下方为历次 AI 推荐记录。
|
|
258
|
+
</p>
|
|
259
|
+
|
|
260
|
+
{records.length > 0 ? (
|
|
261
|
+
<details
|
|
262
|
+
className={css.recordBox}
|
|
263
|
+
open={recordsOpen}
|
|
264
|
+
onToggle={(event) => setRecordsOpen((event.target as HTMLDetailsElement).open)}
|
|
265
|
+
>
|
|
266
|
+
<summary className={css.recordSummary}>
|
|
267
|
+
<ListIcon /> 最近 AI 推荐({records.length})
|
|
268
|
+
<span className={css.recordHint}>每次推荐自动记录,点「用此音色生成」可直接回填 TTS 表单</span>
|
|
269
|
+
</summary>
|
|
270
|
+
<div className={css.recordList}>
|
|
271
|
+
{records.map(record => (
|
|
272
|
+
<div className={`${css.card} ${css.recordCard}`} key={record.id}>
|
|
273
|
+
<div className={css.cardHead}>
|
|
274
|
+
<span className={css.voiceName} title={record.requirement}>{record.requirement}</span>
|
|
275
|
+
<span className={css.badge} data-source={record.vendor}>{record.channel}</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div className={css.meta}>
|
|
278
|
+
<span>{new Date(record.createdAt).toLocaleString()}</span>
|
|
279
|
+
<span>候选池 {record.candidate_count}</span>
|
|
280
|
+
<span>推荐 {record.recommendations.length} 条</span>
|
|
281
|
+
</div>
|
|
282
|
+
<div className={css.recordVoices}>
|
|
283
|
+
{record.recommendations.map((voice, index) => (
|
|
284
|
+
<div className={css.recordVoiceRow} key={`${record.id}:${voice.voice_id}`}>
|
|
285
|
+
<span className={css.rank}>{index + 1}</span>
|
|
286
|
+
<span className={css.recordVoiceName} title={voice.name}>{voice.name}</span>
|
|
287
|
+
<span className={css.voiceId} title={voice.voice_id}>ID: {voice.voice_id}</span>
|
|
288
|
+
{voice.descriptive === undefined ? null : <span className={css.reasonInline}>descriptive: {voice.descriptive}</span>}
|
|
289
|
+
{voice.reason === '' ? null : <span className={css.reasonInline}>{voice.reason}</span>}
|
|
290
|
+
<button type="button" className={css.useBtn} onClick={() => reuseFromRecord(record, voice)}>
|
|
291
|
+
<MicIcon /> 用此音色生成
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
<div className={css.actions}>
|
|
297
|
+
<button type="button" className={css.deleteBtn} onClick={() => void removeRecord(record.id)}>
|
|
298
|
+
<TrashIcon /> 删除记录
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
</details>
|
|
305
|
+
) : null}
|
|
306
|
+
|
|
195
307
|
<details className={css.advanced}>
|
|
196
308
|
<summary className={css.advancedSummary}>官方共享库筛选(ElevenLabs /v1/shared-voices,其它渠道无效)</summary>
|
|
197
309
|
<div className={css.advancedGrid}>
|
|
@@ -249,3 +249,168 @@
|
|
|
249
249
|
font-size: 12.5px;
|
|
250
250
|
color: var(--dsw-alias-fg-l3, #6b7280);
|
|
251
251
|
}
|
|
252
|
+
|
|
253
|
+
/* AI 推荐(按需求描述选音色) */
|
|
254
|
+
|
|
255
|
+
.recommend {
|
|
256
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
257
|
+
border-radius: 10px;
|
|
258
|
+
padding: 10px 12px;
|
|
259
|
+
background: var(--dsw-alias-bg-layer-2, #f9fafb);
|
|
260
|
+
display: flex;
|
|
261
|
+
flex-direction: column;
|
|
262
|
+
gap: 8px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.recommendHead {
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 6px;
|
|
269
|
+
flex-wrap: wrap;
|
|
270
|
+
color: var(--dsw-accent, #2563eb);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.recommendTitle {
|
|
274
|
+
font-size: 13px;
|
|
275
|
+
font-weight: 600;
|
|
276
|
+
color: var(--dsw-alias-fg, #111827);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.recommendHint {
|
|
280
|
+
font-size: 11.5px;
|
|
281
|
+
color: var(--dsw-alias-fg-l3, #6b7280);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.recommendBody {
|
|
285
|
+
display: flex;
|
|
286
|
+
flex-direction: column;
|
|
287
|
+
gap: 8px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.recommendInput {
|
|
291
|
+
width: 100%;
|
|
292
|
+
min-height: 48px;
|
|
293
|
+
padding: 7px 9px;
|
|
294
|
+
border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
|
|
295
|
+
border-radius: 8px;
|
|
296
|
+
background: var(--dsw-alias-bg, #fff);
|
|
297
|
+
color: var(--dsw-alias-fg, #111827);
|
|
298
|
+
font-size: 13px;
|
|
299
|
+
font-family: inherit;
|
|
300
|
+
resize: vertical;
|
|
301
|
+
outline: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.recommendInput:focus {
|
|
305
|
+
border-color: var(--dsw-accent, #2563eb);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.recommendControls {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: end;
|
|
311
|
+
gap: 8px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.recommendResults {
|
|
315
|
+
display: flex;
|
|
316
|
+
flex-direction: column;
|
|
317
|
+
gap: 8px;
|
|
318
|
+
margin-top: 2px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.recommendCard {
|
|
322
|
+
border-color: var(--dsw-accent, #2563eb);
|
|
323
|
+
background: color-mix(in srgb, var(--dsw-accent, #2563eb) 4%, var(--dsw-alias-bg, #fff));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.rank {
|
|
327
|
+
display: inline-flex;
|
|
328
|
+
align-items: center;
|
|
329
|
+
justify-content: center;
|
|
330
|
+
width: 20px;
|
|
331
|
+
height: 20px;
|
|
332
|
+
border-radius: 999px;
|
|
333
|
+
background: var(--dsw-accent, #2563eb);
|
|
334
|
+
color: #fff;
|
|
335
|
+
font-size: 11px;
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
flex: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.reason {
|
|
341
|
+
margin: 0;
|
|
342
|
+
font-size: 12.5px;
|
|
343
|
+
line-height: 1.5;
|
|
344
|
+
color: var(--dsw-accent, #2563eb);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ------------------------------------------------------ AI 推荐记录 */
|
|
348
|
+
.recordBox {
|
|
349
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
350
|
+
border-radius: 10px;
|
|
351
|
+
background: var(--dsw-alias-bg-layer-2, #f9fafb);
|
|
352
|
+
padding: 8px 12px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.recordSummary {
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
gap: 6px;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
font-size: 13px;
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
color: var(--dsw-alias-fg, #111827);
|
|
363
|
+
user-select: none;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.recordHint {
|
|
367
|
+
font-size: 11.5px;
|
|
368
|
+
font-weight: 400;
|
|
369
|
+
color: var(--dsw-alias-fg-l3, #6b7280);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.recordList {
|
|
373
|
+
display: flex;
|
|
374
|
+
flex-direction: column;
|
|
375
|
+
gap: 8px;
|
|
376
|
+
margin-top: 8px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.recordCard {
|
|
380
|
+
border-left: 3px solid var(--dsw-accent, #2563eb);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.recordVoices {
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
gap: 4px;
|
|
387
|
+
margin-top: 6px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.recordVoiceRow {
|
|
391
|
+
display: flex;
|
|
392
|
+
align-items: center;
|
|
393
|
+
gap: 8px;
|
|
394
|
+
flex-wrap: wrap;
|
|
395
|
+
padding: 4px 2px;
|
|
396
|
+
border-radius: 6px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.recordVoiceRow:hover {
|
|
400
|
+
background: color-mix(in srgb, var(--dsw-accent, #2563eb) 5%, transparent);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.recordVoiceName {
|
|
404
|
+
font-size: 13px;
|
|
405
|
+
font-weight: 500;
|
|
406
|
+
color: var(--dsw-alias-fg, #111827);
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
text-overflow: ellipsis;
|
|
409
|
+
white-space: nowrap;
|
|
410
|
+
max-width: 220px;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.reasonInline {
|
|
414
|
+
font-size: 12px;
|
|
415
|
+
color: var(--dsw-accent, #2563eb);
|
|
416
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { enhancePromptText } from './prompt-enhance.ts'
|
|
|
22
22
|
import { AudioGenError } from './audio-engine.ts'
|
|
23
23
|
import { makeRoutes, type ChannelsView, type SettingsSeam } from './routes.ts'
|
|
24
24
|
import type { AudioChannel } from './audio-engine.ts'
|
|
25
|
+
import { recommendVoices, type VoiceRecommendation } from './voice-recommend.ts'
|
|
26
|
+
import type { VendorVoiceEntry } from './voice-manager.ts'
|
|
25
27
|
import { registerAgentAudioTools, type AgentAudioToolConfig } from './agent-audio-tools.ts'
|
|
26
28
|
import { audioPresetById } from './audio-presets.ts'
|
|
27
29
|
|
|
@@ -82,7 +84,7 @@ const DEFAULT_ALLOW_AGENT_AUDIO = true
|
|
|
82
84
|
|
|
83
85
|
const SECTION_ORDER = 160
|
|
84
86
|
|
|
85
|
-
export 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
|
|
87
|
+
export 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),也能用该工具的 action=recommend 按自然语言需求描述(如「清亮甜美的少女音」)让默认模型推荐 top-k 音色,再用选定音色的 voice_id 调用 `generate_audio` 生成。角色音色选角(小说/游戏配音 casting):传角色画像(JSON 数组/对象,字段 character_id/character_name/gender/age_stage/voice_traits/personality_traits/appearance/sample_lines/dialogue_count/language;文本描述需先整理成该结构)调用 `manage_audio_voices` 的 action=cast,工具按性别/年龄/用途做确定性硬过滤(accent 为偏好、候选为空才放松)并返回每个角色的候选池,你在上下文中全局权衡主音色(lead/major 不复用)+ 备用音色后,用 action=save_cast 校验落盘(~/.dsh/dsh-audiogen/cast-selections.json),随后按选定 voice_id 生成 TTS;MiniMax 也可先用 `generate_audio(mode=voice_design)` 为角色创作专属音色再选角。限制:生成消耗上游 API 额度;音频内容由上游模型生成;模型只能使用用户在各渠道配置目录中的模型;音色管理仅 MiniMax 与 ElevenLabs 渠道支持。用户提到「音频 / 语音 / TTS / 配乐 / 音效 / AI 音频」时即指本插件,请据此协作。'
|
|
86
88
|
|
|
87
89
|
function guidanceFor(channels: AudioChannel[], defaultChannelId: string): string {
|
|
88
90
|
if (channels.length === 0) {
|
|
@@ -225,6 +227,17 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
225
227
|
return provider !== '' && model !== '' ? { provider, model } : undefined
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
// 音色推荐:与提示词增强同机制,复用 Agent 默认模型(agent-default-model)。
|
|
231
|
+
const recommend = async (
|
|
232
|
+
requirement: string,
|
|
233
|
+
candidates: VendorVoiceEntry[],
|
|
234
|
+
topK: number,
|
|
235
|
+
): Promise<VoiceRecommendation[]> => {
|
|
236
|
+
const seam = ctx.get('settings') as unknown as SettingsSeam
|
|
237
|
+
if (seam?.describe === undefined) throw new AudioGenError('设置服务不可用,无法推荐音色', 'settings-unavailable')
|
|
238
|
+
return recommendVoices({ settings: seam, llm: () => ctx.get('llm') }, requirement, candidates, topK)
|
|
239
|
+
}
|
|
240
|
+
|
|
228
241
|
/** 读取「设置 → 模型」目录:各提供方 + 可广播的模型列表(增强模型下拉候选)。 */
|
|
229
242
|
const llmModelOptions = async (): Promise<LlmModelOption[]> => {
|
|
230
243
|
const llm = ctx.get('llm') as {
|
|
@@ -271,6 +284,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
271
284
|
autoSave: () => resolve().autoSaveToLibrary,
|
|
272
285
|
budget,
|
|
273
286
|
enhance,
|
|
287
|
+
recommend,
|
|
274
288
|
llmModelOptions,
|
|
275
289
|
})
|
|
276
290
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
@@ -289,6 +303,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
289
303
|
autoSaveToLibrary: value.autoSaveToLibrary,
|
|
290
304
|
budget,
|
|
291
305
|
enhance,
|
|
306
|
+
recommend,
|
|
292
307
|
}
|
|
293
308
|
}), 'dsh-audiogen: agent audio tools')
|
|
294
309
|
})
|
package/src/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const AUDIOGEN_SETTINGS_NAMESPACE = 'dsh-audiogen'
|
|
9
9
|
|
|
10
10
|
/** Published package version shared by the host updater and the client UI. */
|
|
11
|
-
export const PLUGIN_VERSION = '0.4.
|
|
11
|
+
export const PLUGIN_VERSION = '0.4.22'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
|
|
14
14
|
export const SETTINGS_API = {
|
|
@@ -42,15 +42,57 @@ export const MODEL_API = {
|
|
|
42
42
|
export const VOICES_API = {
|
|
43
43
|
list: '/api/dsh-audiogen/voices/list',
|
|
44
44
|
delete: '/api/dsh-audiogen/voices/delete',
|
|
45
|
+
recommend: '/api/dsh-audiogen/voices/recommend',
|
|
46
|
+
recommendHistory: {
|
|
47
|
+
list: '/api/dsh-audiogen/voices/recommend-history/list',
|
|
48
|
+
remove: '/api/dsh-audiogen/voices/recommend-history/remove',
|
|
49
|
+
},
|
|
45
50
|
} as const
|
|
46
51
|
|
|
52
|
+
/** 一条已记录的 AI 音色推荐结果(面板与 Agent 共用同一存储)。 */
|
|
53
|
+
export interface VoiceRecommendRecord {
|
|
54
|
+
id: string
|
|
55
|
+
createdAt: number
|
|
56
|
+
/** 渠道展示名(如 MiniMax / ElevenLabs)。 */
|
|
57
|
+
channel: string
|
|
58
|
+
/** 渠道 id(「用此音色生成」时切换目标渠道用)。 */
|
|
59
|
+
channel_id?: string
|
|
60
|
+
vendor: string
|
|
61
|
+
requirement: string
|
|
62
|
+
/** 候选池大小(推荐时)。 */
|
|
63
|
+
candidate_count: number
|
|
64
|
+
top_k: number
|
|
65
|
+
/** 推荐时使用的筛选条件(language/keyword/use_case/accent… 快照)。 */
|
|
66
|
+
filters?: Record<string, string | boolean>
|
|
67
|
+
recommendations: Array<{
|
|
68
|
+
voice_id: string
|
|
69
|
+
name: string
|
|
70
|
+
source: string
|
|
71
|
+
deletable: boolean
|
|
72
|
+
language?: string
|
|
73
|
+
locale?: string
|
|
74
|
+
accent?: string
|
|
75
|
+
gender?: string
|
|
76
|
+
age?: string
|
|
77
|
+
use_case?: string
|
|
78
|
+
category?: string
|
|
79
|
+
/** 厂商返回的完整标签(ElevenLabs shared-voices labels 原样保留)。 */
|
|
80
|
+
labels?: Record<string, string>
|
|
81
|
+
/** ElevenLabs descriptive 标签(如 serious / calm / whispery),选角排序参考。 */
|
|
82
|
+
descriptive?: string
|
|
83
|
+
description?: string
|
|
84
|
+
preview_url?: string
|
|
85
|
+
reason: string
|
|
86
|
+
}>
|
|
87
|
+
}
|
|
88
|
+
|
|
47
89
|
/** One normalized vendor voice entry (see voice-manager). */
|
|
48
90
|
export interface VoiceEntry {
|
|
49
91
|
provider: string
|
|
50
92
|
voice_id: string
|
|
51
93
|
name: string
|
|
52
|
-
/** system | custom | owned | shared — determines deletability. */
|
|
53
|
-
source: 'system' | 'custom' | 'owned' | 'shared'
|
|
94
|
+
/** system | custom | owned | shared | configured — determines deletability. */
|
|
95
|
+
source: 'system' | 'custom' | 'owned' | 'shared' | 'configured'
|
|
54
96
|
deletable: boolean
|
|
55
97
|
language?: string
|
|
56
98
|
locale?: string
|
|
@@ -59,6 +101,10 @@ export interface VoiceEntry {
|
|
|
59
101
|
age?: string
|
|
60
102
|
use_case?: string
|
|
61
103
|
category?: string
|
|
104
|
+
/** 厂商返回的完整标签(ElevenLabs labels 原样保留)。 */
|
|
105
|
+
labels?: Record<string, string>
|
|
106
|
+
/** ElevenLabs descriptive 标签(如 serious / calm / whispery)。 */
|
|
107
|
+
descriptive?: string
|
|
62
108
|
description?: string
|
|
63
109
|
preview_url?: string
|
|
64
110
|
}
|
|
@@ -93,6 +139,19 @@ export interface VoicesDeleteRequest {
|
|
|
93
139
|
confirm: boolean
|
|
94
140
|
}
|
|
95
141
|
|
|
142
|
+
/** Prompt-based recommendation request: 需求描述 + 可选的候选池缩小条件。 */
|
|
143
|
+
export interface VoicesRecommendRequest extends VoicesListRequest {
|
|
144
|
+
/** 自然语言需求描述(如「17岁清亮甜美的少女音,适合活泼女主角」)。 */
|
|
145
|
+
requirement: string
|
|
146
|
+
/** 推荐条数(1-10,默认 5)。 */
|
|
147
|
+
top_k?: number
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 单条推荐:音色条目 + LLM 理由(复用 VoiceEntry 形状以便前端直接渲染/复用)。 */
|
|
151
|
+
export interface VoiceRecommendation extends VoiceEntry {
|
|
152
|
+
reason: string
|
|
153
|
+
}
|
|
154
|
+
|
|
96
155
|
/** Loopback-only audio file reader for panel/tool-result previews. */
|
|
97
156
|
export const AUDIO_API = {
|
|
98
157
|
file: '/api/dsh-audiogen/audio',
|