dsh-audiogen 0.4.1 → 0.4.2

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.
@@ -425,6 +425,116 @@
425
425
  background: color-mix(in srgb, var(--dsw-alias-brand-primary, #2563eb) 8%, transparent);
426
426
  }
427
427
 
428
+ /* ---------------------------------------------------------- model compare */
429
+
430
+ .compareBox {
431
+ display: flex;
432
+ flex-direction: column;
433
+ gap: 7px;
434
+ padding: 9px 11px;
435
+ border: 1px dashed var(--dsw-alias-brand-primary, #2563eb);
436
+ border-radius: 10px;
437
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #2563eb) 4%, var(--dsw-alias-bg-layer-1, #fff));
438
+ }
439
+
440
+ .compareChips {
441
+ display: flex;
442
+ flex-wrap: wrap;
443
+ gap: 6px;
444
+ }
445
+
446
+ .compareChip {
447
+ display: inline-flex;
448
+ align-items: center;
449
+ min-height: 27px;
450
+ padding: 0 11px;
451
+ border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
452
+ border-radius: 999px;
453
+ background: var(--dsw-alias-bg-layer-1, #fff);
454
+ color: var(--dsw-alias-label-secondary, #6b7280);
455
+ font-family: inherit;
456
+ font-size: 12px;
457
+ cursor: pointer;
458
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
459
+ }
460
+
461
+ .compareChip:hover {
462
+ color: var(--dsw-alias-label-primary, #1f2328);
463
+ border-color: var(--dsw-alias-label-dimmed, #9ca3af);
464
+ }
465
+
466
+ .compareChip[data-active='true'] {
467
+ color: var(--dsw-alias-brand-primary, #2563eb);
468
+ border-color: var(--dsw-alias-brand-primary, #2563eb);
469
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #2563eb) 9%, transparent);
470
+ font-weight: 600;
471
+ }
472
+
473
+ .compareBoard {
474
+ display: flex;
475
+ flex-direction: column;
476
+ gap: 12px;
477
+ }
478
+
479
+ .compareGroup {
480
+ display: flex;
481
+ flex-direction: column;
482
+ gap: 8px;
483
+ padding: 11px;
484
+ border: 1px solid var(--dsw-alias-border-l1, #e5e7eb);
485
+ border-radius: 12px;
486
+ background: var(--dsw-alias-bg-layer-2, #f7f7f8);
487
+ }
488
+
489
+ .compareGroup[data-state='running'] {
490
+ border-color: color-mix(in srgb, var(--dsw-alias-brand-primary, #2563eb) 45%, var(--dsw-alias-border-l1, #e5e7eb));
491
+ }
492
+
493
+ .compareGroup[data-state='done'] {
494
+ border-color: color-mix(in srgb, #10b981 45%, var(--dsw-alias-border-l1, #e5e7eb));
495
+ }
496
+
497
+ .compareGroup[data-state='error'] {
498
+ border-color: color-mix(in srgb, var(--dsw-alias-label-error, #b91c1c) 45%, var(--dsw-alias-border-l1, #e5e7eb));
499
+ }
500
+
501
+ .compareGroupHead {
502
+ display: flex;
503
+ align-items: center;
504
+ justify-content: space-between;
505
+ gap: 10px;
506
+ }
507
+
508
+ .compareModelName {
509
+ overflow: hidden;
510
+ padding: 3px 10px;
511
+ border-radius: 999px;
512
+ background: var(--dsw-alias-bg-layer-1, #fff);
513
+ border: 1px solid var(--dsw-alias-border-l1, #e5e7eb);
514
+ color: var(--dsw-alias-label-primary, #1f2328);
515
+ font-size: 12px;
516
+ font-weight: 600;
517
+ text-overflow: ellipsis;
518
+ white-space: nowrap;
519
+ }
520
+
521
+ .compareState {
522
+ display: inline-flex;
523
+ align-items: center;
524
+ gap: 5px;
525
+ font-size: 11px;
526
+ color: var(--dsw-alias-label-tertiary, #9ca3af);
527
+ white-space: nowrap;
528
+ }
529
+
530
+ .compareGroup[data-state='running'] .compareState { color: var(--dsw-alias-brand-primary, #2563eb); }
531
+ .compareGroup[data-state='done'] .compareState { color: #059669; }
532
+ .compareGroup[data-state='error'] .compareState { color: var(--dsw-alias-label-error, #b91c1c); }
533
+
534
+ .hint[data-error] {
535
+ color: var(--dsw-alias-label-error, #b91c1c);
536
+ }
537
+
428
538
  /* --------------------------------------------------------------- history */
429
539
 
430
540
  .historyCol {
@@ -892,3 +1002,134 @@
892
1002
  color: var(--dsw-alias-label-primary, #1f2328);
893
1003
  white-space: normal;
894
1004
  }
1005
+
1006
+ /* 任务列表 */
1007
+ .taskList {
1008
+ display: flex;
1009
+ flex-direction: column;
1010
+ gap: 14px;
1011
+ }
1012
+
1013
+ .taskCard {
1014
+ border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
1015
+ border-radius: 10px;
1016
+ padding: 10px 12px;
1017
+ display: flex;
1018
+ flex-direction: column;
1019
+ gap: 8px;
1020
+ background: var(--dsw-alias-bg-layer-2, #fafafa);
1021
+ }
1022
+
1023
+ .taskCard[data-state='failed'] {
1024
+ border-color: #dc2626;
1025
+ }
1026
+
1027
+ .taskCard[data-state='cancelled'] {
1028
+ opacity: 0.75;
1029
+ }
1030
+
1031
+ .taskHead {
1032
+ display: flex;
1033
+ align-items: center;
1034
+ gap: 8px;
1035
+ flex-wrap: wrap;
1036
+ }
1037
+
1038
+ .taskLabel {
1039
+ font-size: 13px;
1040
+ font-weight: 600;
1041
+ color: var(--dsw-alias-label-primary, #1f2328);
1042
+ overflow: hidden;
1043
+ text-overflow: ellipsis;
1044
+ white-space: nowrap;
1045
+ max-width: 200px;
1046
+ }
1047
+
1048
+ .taskStatus {
1049
+ font-size: 12px;
1050
+ color: var(--dsw-alias-label-secondary, #6b7280);
1051
+ }
1052
+
1053
+ .taskStatus[data-state='done'] {
1054
+ color: #16a34a;
1055
+ }
1056
+
1057
+ .taskStatus[data-state='failed'] {
1058
+ color: #dc2626;
1059
+ }
1060
+
1061
+ .taskActions {
1062
+ margin-left: auto;
1063
+ display: flex;
1064
+ gap: 6px;
1065
+ }
1066
+
1067
+ /* 历史记录分类 */
1068
+ .historyTabs {
1069
+ display: flex;
1070
+ flex-wrap: wrap;
1071
+ gap: 6px;
1072
+ margin-bottom: 10px;
1073
+ }
1074
+
1075
+ .historyTab {
1076
+ border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
1077
+ border-radius: 999px;
1078
+ padding: 3px 10px;
1079
+ font-size: 12px;
1080
+ cursor: pointer;
1081
+ background: var(--dsw-alias-bg-layer-3, #fff);
1082
+ color: var(--dsw-alias-label-secondary, #6b7280);
1083
+ }
1084
+
1085
+ .historyTab[data-active='true'] {
1086
+ background: var(--dsw-alias-bg-hover, #f3f4f6);
1087
+ color: var(--dsw-alias-label-primary, #1f2328);
1088
+ font-weight: 600;
1089
+ }
1090
+
1091
+ .historyTabCount {
1092
+ margin-left: 4px;
1093
+ font-size: 11px;
1094
+ color: var(--dsw-alias-label-tertiary, #9ca3af);
1095
+ }
1096
+
1097
+ /* 对比任务卡 */
1098
+ .historyCompareSummary {
1099
+ display: flex;
1100
+ align-items: center;
1101
+ gap: 8px;
1102
+ cursor: pointer;
1103
+ flex-wrap: wrap;
1104
+ }
1105
+
1106
+ .historyCompareBadge {
1107
+ font-size: 11px;
1108
+ font-weight: 600;
1109
+ color: #7c3aed;
1110
+ border: 1px solid #ddd6fe;
1111
+ border-radius: 999px;
1112
+ padding: 1px 8px;
1113
+ background: #f5f3ff;
1114
+ white-space: nowrap;
1115
+ }
1116
+
1117
+ .historyModelRow {
1118
+ margin-top: 8px;
1119
+ padding-top: 8px;
1120
+ border-top: 1px dashed var(--dsw-alias-border-l1, #e5e7eb);
1121
+ display: flex;
1122
+ flex-direction: column;
1123
+ gap: 4px;
1124
+ }
1125
+
1126
+ .overrideOnly {
1127
+ font-size: 10px;
1128
+ color: var(--dsw-alias-label-tertiary, #9ca3af);
1129
+ font-weight: 500;
1130
+ }
1131
+
1132
+ .overrideDash {
1133
+ color: var(--dsw-alias-label-tertiary, #9ca3af);
1134
+ padding: 0 6px;
1135
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * 厂商感知的字段规格表:按(渠道 preset × 模式)声明面板该显示哪些参数,
3
+ * 以及每个参数适用于哪些渠道。引擎不变——引擎本来就按渠道消费字段;
4
+ * 这里只负责「该显示什么、标签/枚举/校验按渠道」。
5
+ */
6
+
7
+ import type { AudioMode } from '../protocol.ts'
8
+
9
+ /** 与面板表单 state 对应的一组字段键。 */
10
+ export type FieldKey =
11
+ | 'duration'
12
+ | 'format'
13
+ | 'lyrics'
14
+ | 'instrumental'
15
+ | 'sampleRate'
16
+ | 'bitrate'
17
+ | 'audioChannel'
18
+ | 'voice'
19
+ | 'speed'
20
+ | 'emotion'
21
+ | 'vol'
22
+ | 'pitch'
23
+ | 'toneText'
24
+ | 'subtitle'
25
+ | 'loop'
26
+ | 'promptInfluence'
27
+ | 'seed'
28
+ | 'steps'
29
+ | 'cfgScale'
30
+
31
+ export interface FieldSpec {
32
+ key: FieldKey
33
+ label: string
34
+ type: 'text' | 'number' | 'select' | 'checkbox'
35
+ options?: string[]
36
+ placeholder?: string
37
+ min?: number
38
+ max?: number
39
+ step?: number
40
+ /** 官方字段名/说明(tooltip)。 */
41
+ hint?: string
42
+ /** 放在折叠「高级参数」里。 */
43
+ advanced?: boolean
44
+ /** 适用的渠道 preset;undefined 表示通用。 */
45
+ presets?: string[]
46
+ }
47
+
48
+ const PRESET_MINIMAX = 'minimax'
49
+ const PRESET_ELEVENLABS = 'elevenlabs'
50
+ const PRESET_STABILITY = 'stability-audio'
51
+
52
+ /** 各 preset 支持的音乐输出格式(交集用于全局字段)。 */
53
+ const MUSIC_FORMATS: Record<string, string[]> = {
54
+ [PRESET_MINIMAX]: ['mp3', 'wav', 'pcm'],
55
+ [PRESET_ELEVENLABS]: ['mp3', 'wav'],
56
+ [PRESET_STABILITY]: ['mp3', 'wav'],
57
+ }
58
+
59
+ const MUSIC_KEYS: FieldKey[] = ['duration', 'format', 'lyrics', 'instrumental', 'sampleRate', 'bitrate']
60
+ const TTS_KEYS: FieldKey[] = ['voice', 'speed', 'format', 'emotion', 'vol', 'pitch', 'toneText', 'sampleRate', 'bitrate', 'audioChannel', 'subtitle']
61
+ const SFX_KEYS: FieldKey[] = ['duration', 'format', 'loop', 'promptInfluence', 'seed', 'steps', 'cfgScale']
62
+
63
+ export function presetSupports(preset: string, key: FieldKey, mode: AudioMode): boolean {
64
+ const p = preset.toLowerCase()
65
+ const K = MUSIC_KEYS.includes(key) ? MUSIC_KEYS : TTS_KEYS.includes(key) ? TTS_KEYS : SFX_KEYS
66
+ if (!K.includes(key)) return true
67
+ switch (key) {
68
+ case 'lyrics':
69
+ case 'instrumental':
70
+ return p === PRESET_MINIMAX || p === PRESET_ELEVENLABS
71
+ case 'sampleRate':
72
+ case 'bitrate':
73
+ case 'audioChannel':
74
+ return p === PRESET_MINIMAX
75
+ case 'emotion':
76
+ case 'vol':
77
+ case 'pitch':
78
+ case 'toneText':
79
+ case 'subtitle':
80
+ return p === PRESET_MINIMAX
81
+ case 'loop':
82
+ case 'promptInfluence':
83
+ return p === PRESET_ELEVENLABS
84
+ case 'seed':
85
+ case 'steps':
86
+ case 'cfgScale':
87
+ return p === PRESET_STABILITY
88
+ default:
89
+ return true
90
+ }
91
+ }
92
+
93
+ const SPECS: Record<FieldKey, Omit<FieldSpec, 'key' | 'presets'>> = {
94
+ duration: { label: '时长(秒)', type: 'number', min: 1, max: 120, placeholder: '30', hint: 'duration;MiniMax 音乐 ≤190、ElevenLabs 音乐 3-600(转 ms)、Stability 按模型 190/380' },
95
+ format: { label: '输出格式', type: 'select', options: ['mp3', 'wav', 'pcm', 'flac', 'ogg'], hint: 'format / output_format / response_format' },
96
+ lyrics: { label: '歌词(纯音乐模式可留空;多段用空行分隔)', type: 'text', placeholder: '第一段歌词…\n\n第二段歌词…', hint: 'MiniMax lyrics / ElevenLabs lyrics_text' },
97
+ instrumental: { label: '纯音乐(无歌词/人声)', type: 'checkbox', hint: 'MiniMax is_instrumental / ElevenLabs force_instrumental' },
98
+ sampleRate: { label: '采样率', type: 'select', options: ['16000', '24000', '32000', '44100'], placeholder: '默认(44100)', hint: 'audio_setting.sample_rate:16000-44100' },
99
+ bitrate: { label: '码率 bps', type: 'select', options: ['32000', '64000', '128000', '256000'], placeholder: '默认(256000)', hint: 'audio_setting.bitrate:32000-256000' },
100
+ audioChannel: { label: '声道', type: 'select', options: ['1', '2'], placeholder: '默认(1)', hint: 'audio_setting.channel(TTS)' },
101
+ voice: { label: '音色', type: 'text', placeholder: '自定义音色', hint: 'voice_id(MiniMax 必填)/ voice(ElevenLabs)' },
102
+ speed: { label: '语速', type: 'number', min: 0.5, max: 2, step: 0.1, placeholder: '1.0', hint: 'speed(MiniMax 0.5-2)' },
103
+ emotion: { label: '情绪 emotion', type: 'text', placeholder: 'happy / sad / angry / nervous…', hint: 'voice_setting.emotion', advanced: true },
104
+ vol: { label: '音量 vol (0-10)', type: 'number', min: 0, max: 10, step: 0.5, placeholder: '1', hint: 'voice_setting.vol', advanced: true },
105
+ pitch: { label: '音调 pitch (-12~12)', type: 'number', min: -12, max: 12, placeholder: '0', hint: 'voice_setting.pitch', advanced: true },
106
+ toneText: { label: '发音词典(每行一条:"文字/读音")', type: 'text', placeholder: '处理/(chu3)(li3)\n危险/dangerous', hint: 'pronunciation_dict.tone', advanced: true },
107
+ subtitle: { label: '生成字幕 subtitle_enable', type: 'checkbox', hint: 'subtitle_enable', advanced: true },
108
+ loop: { label: '循环音效 loop(无缝循环)', type: 'checkbox', hint: 'ElevenLabs loop(仅 eleven_text_to_sound_v2)' },
109
+ promptInfluence: { label: '提示词影响度 prompt_influence (0-1)', type: 'number', min: 0, max: 1, step: 0.1, placeholder: '0.3', hint: 'prompt_influence:越高越贴提示词' },
110
+ seed: { label: 'seed', type: 'number', min: 0, max: 4294967294, placeholder: '默认(随机)', hint: 'Stable Audio seed(同参数可复现)' },
111
+ steps: { label: 'steps', type: 'number', min: 4, max: 100, placeholder: '默认', hint: 'Stable Audio 采样步数(2: 30-100;2.5/3: 4-8)' },
112
+ cfgScale: { label: 'cfg_scale', type: 'number', min: 1, max: 25, placeholder: '默认', hint: 'Stable Audio 提示词遵循度(2 默认 7,2.5/3 默认 1)' },
113
+ }
114
+
115
+ function specOf(key: FieldKey, presets?: string[]): FieldSpec {
116
+ return { key, ...SPECS[key], ...(presets === undefined ? {} : { presets }) }
117
+ }
118
+
119
+ /** 当前模式 + 所选模型集合(渠道集合)对应的「全局字段」清单。 */
120
+ export function globalFieldSpecs(mode: AudioMode, presets: string[]): FieldSpec[] {
121
+ const list: FieldSpec[] = []
122
+ const all = (key: FieldKey, keys: FieldKey[]): boolean => keys.includes(key) && presets.every(preset => presetSupports(preset, key, mode))
123
+ if (mode === 'tts') {
124
+ list.push(specOf('voice'), specOf('speed'))
125
+ list.push({ ...specOf('format'), options: ['mp3', 'wav', 'flac', 'ogg', 'pcm'] })
126
+ const advanced = (['emotion', 'vol', 'pitch', 'toneText', 'sampleRate', 'bitrate', 'audioChannel', 'subtitle'] as FieldKey[]).filter(key => all(key, TTS_KEYS) && presets.length > 0)
127
+ for (const key of advanced) list.push(specOf(key))
128
+ } else if (mode === 'music') {
129
+ list.push(specOf('duration'))
130
+ // 格式选项取所有选中渠道支持的交集(无交集则回退 mp3/wav)
131
+ const supported: string[][] = presets.length === 0 ? [['mp3', 'wav', 'pcm']] : presets.map(preset => MUSIC_FORMATS[preset.toLowerCase()] ?? ['mp3', 'wav'])
132
+ const intersect: string[] = supported.reduce<string[]>((acc, cur) => acc.filter(item => cur.includes(item)), supported[0] ?? ['mp3', 'wav'])
133
+ list.push({ ...specOf('format'), options: intersect.length > 0 ? intersect : ['mp3', 'wav'] })
134
+ if (all('lyrics', MUSIC_KEYS) && presets.length > 0) list.push(specOf('lyrics'))
135
+ if (all('instrumental', MUSIC_KEYS) && presets.length > 0) list.push(specOf('instrumental'))
136
+ if (all('sampleRate', MUSIC_KEYS) && presets.length > 0) list.push(specOf('sampleRate'))
137
+ if (all('bitrate', MUSIC_KEYS) && presets.length > 0) list.push(specOf('bitrate'))
138
+ } else if (mode === 'sfx') {
139
+ list.push(specOf('duration'))
140
+ list.push({ ...specOf('format'), options: ['mp3', 'wav', 'pcm'] })
141
+ if (all('loop', SFX_KEYS) && presets.length > 0) list.push(specOf('loop'))
142
+ if (all('promptInfluence', SFX_KEYS) && presets.length > 0) list.push(specOf('promptInfluence'))
143
+ if (all('seed', SFX_KEYS) && presets.length > 0) list.push(specOf('seed'), specOf('steps'), specOf('cfgScale'))
144
+ }
145
+ return list
146
+ }
147
+
148
+ /** 「每模型参数覆盖」矩阵的字段全集(含适用渠道标注)。 */
149
+ export function overrideRowSpecs(mode: AudioMode): Array<FieldSpec & { presets: string[] }> {
150
+ const rows: Array<FieldSpec & { presets: string[] }> = []
151
+ const presets: Array<[FieldKey, string[]]> = [
152
+ ['format', [PRESET_MINIMAX, PRESET_ELEVENLABS, PRESET_STABILITY]],
153
+ ['duration', [PRESET_MINIMAX, PRESET_ELEVENLABS, PRESET_STABILITY]],
154
+ ['voice', [PRESET_MINIMAX, PRESET_ELEVENLABS]],
155
+ ['speed', [PRESET_MINIMAX, PRESET_ELEVENLABS]],
156
+ ['lyrics', [PRESET_MINIMAX, PRESET_ELEVENLABS]],
157
+ ['instrumental', [PRESET_MINIMAX, PRESET_ELEVENLABS]],
158
+ ['sampleRate', [PRESET_MINIMAX]],
159
+ ['bitrate', [PRESET_MINIMAX]],
160
+ ['audioChannel', [PRESET_MINIMAX]],
161
+ ['emotion', [PRESET_MINIMAX]],
162
+ ['vol', [PRESET_MINIMAX]],
163
+ ['pitch', [PRESET_MINIMAX]],
164
+ ['toneText', [PRESET_MINIMAX]],
165
+ ['subtitle', [PRESET_MINIMAX]],
166
+ ['loop', [PRESET_ELEVENLABS]],
167
+ ['promptInfluence', [PRESET_ELEVENLABS]],
168
+ ['seed', [PRESET_STABILITY]],
169
+ ['steps', [PRESET_STABILITY]],
170
+ ['cfgScale', [PRESET_STABILITY]],
171
+ ]
172
+ for (const [key, applicable] of presets) {
173
+ const spec = specOf(key, applicable)
174
+ rows.push({ ...spec, presets: applicable })
175
+ }
176
+ return rows
177
+ }
178
+
179
+ /** 渠道 preset 的展示名。 */
180
+ export function presetLabel(preset: string): string {
181
+ if (preset === PRESET_MINIMAX) return 'MiniMax'
182
+ if (preset === PRESET_ELEVENLABS) return 'ElevenLabs'
183
+ if (preset === PRESET_STABILITY) return 'Stability'
184
+ if (preset === 'openai-tts') return 'OpenAI'
185
+ return '自定义'
186
+ }
@@ -319,7 +319,12 @@ export function LibraryView(props: {
319
319
  </div>
320
320
 
321
321
  {loading ? <p className={css.stateNote}>加载中…</p> : null}
322
- {error !== null ? <p className={css.stateNote} data-error>{error}</p> : null}
322
+ {error !== null ? (
323
+ <div className={css.stateNote} data-error>
324
+ <p>{error}</p>
325
+ <p>若提示 not found / 404:插件宿主尚未加载新代码,请重启 `dsh web` 后在浏览器强制刷新(Cmd+Shift+R)。</p>
326
+ </div>
327
+ ) : null}
323
328
  {!loading && error === null && filtered.length === 0 ? (
324
329
  <div className={css.empty}>
325
330
  <span className={css.emptyIcon}>🎧</span>
@@ -36,6 +36,7 @@ export const zh = {
36
36
  'settings.announceToAgent': '向 Agent 播报本插件',
37
37
  'settings.allowAgentAudio': '允许 Agent 调用音频生成',
38
38
  'settings.autoSaveLibrary': '生成后自动保存到资源库',
39
+ 'settings.maxConcurrent': '最大并发生成数(同时打到上游的请求数,默认 5)',
39
40
  'settings.save': '保存',
40
41
  'settings.saving': '保存中…',
41
42
  'settings.discard': '放弃修改',
@@ -140,6 +141,7 @@ export const en: Record<AudioGenKey, string> = {
140
141
  'settings.announceToAgent': 'Announce this plugin to agents',
141
142
  'settings.allowAgentAudio': 'Allow agents to generate audio',
142
143
  'settings.autoSaveLibrary': 'Auto-save generated audio to the library',
144
+ 'settings.maxConcurrent': 'Max concurrent generations (in-flight upstream calls, default 5)',
143
145
  'settings.save': 'Save',
144
146
  'settings.saving': 'Saving…',
145
147
  'settings.discard': 'Discard',
@@ -271,8 +271,17 @@ export function bindAudiogenScope(fetchFn: typeof fetch = fetch): AudiogenScope
271
271
  * Falls back to the legacy flat allow-list while no channels exist (upgrade
272
272
  * path). Pure projection — no host calls.
273
273
  */
274
+ export interface ModelOption {
275
+ alias: string
276
+ category?: AudioModelCategory
277
+ /** 所属渠道 id/名称/preset(面板按渠道渲染参数与分组下拉)。 */
278
+ channelId: string
279
+ channelName: string
280
+ preset: string
281
+ }
282
+
274
283
  export function audioModelOptions(config: AudiogenConfig | undefined): {
275
- models: Array<{ alias: string; category?: AudioModelCategory }>
284
+ models: ModelOption[]
276
285
  defaultChannelId?: string
277
286
  } {
278
287
  const channels = config?.channels ?? []
@@ -283,14 +292,20 @@ export function audioModelOptions(config: AudiogenConfig | undefined): {
283
292
  ? config.defaultChannelId
284
293
  : channels[0]!.id
285
294
  const ordered = [defaultId, ...channels.filter(channel => channel.id !== defaultId).map(channel => channel.id)]
286
- const models: Array<{ alias: string; category?: AudioModelCategory }> = []
295
+ const models: ModelOption[] = []
287
296
  const seen = new Set<string>()
288
297
  for (const id of ordered) {
289
298
  const channel = channels.find(candidate => candidate.id === id)!
290
299
  for (const model of channel.models) {
291
300
  if (model.alias === '' || seen.has(model.alias)) continue
292
301
  seen.add(model.alias)
293
- models.push({ alias: model.alias, ...(model.category === undefined ? {} : { category: model.category }) })
302
+ models.push({
303
+ alias: model.alias,
304
+ ...(model.category === undefined ? {} : { category: model.category }),
305
+ channelId: channel.id,
306
+ channelName: channel.name,
307
+ preset: channel.preset,
308
+ })
294
309
  }
295
310
  }
296
311
  return models.length > 0 ? { models, defaultChannelId: defaultId } : { models: [], defaultChannelId: defaultId }