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.
@@ -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
- channel: channel.id,
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
- <input className={css.input} value={language} onChange={event => setLanguage(event.target.value)} placeholder="en / zh / ja…" />
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),再用选定音色的 voice_id 调用 `generate_audio` 生成。限制:生成消耗上游 API 额度;音频内容由上游模型生成;模型只能使用用户在各渠道配置目录中的模型。用户提到「音频 / 语音 / TTS / 配乐 / 音效 / AI 音频」时即指本插件,请据此协作。'
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.19'
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',