dsh-audiogen 0.4.0 → 0.4.1
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 +665 -407
- package/lib/client.js.map +1 -1
- package/lib/index.js +289 -157
- package/package.json +1 -1
- package/skills/design/SKILL.md +5 -0
- package/skills/music/SKILL.md +5 -0
- package/skills/sfx/SKILL.md +5 -0
- package/skills/tts/SKILL.md +5 -0
- package/src/agent-audio-tools.ts +255 -149
- package/src/client/audio-panel.module.css +87 -0
- package/src/client/studio-view.tsx +270 -91
- package/src/index.ts +32 -0
- package/src/protocol.ts +1 -1
|
@@ -11,7 +11,7 @@ import { audioModelOptions } from './settings-scope.ts'
|
|
|
11
11
|
import { tt } from './helpers.ts'
|
|
12
12
|
import {
|
|
13
13
|
HISTORY_API,
|
|
14
|
-
type AudioMode, type GeneratedAudio, type HistoryEntry, type LibraryEntry,
|
|
14
|
+
type AudioMode, type GenerateAudioRequest, type GeneratedAudio, type HistoryEntry, type LibraryEntry,
|
|
15
15
|
} from '../protocol.ts'
|
|
16
16
|
import { AudioPlayer } from './audio-player.tsx'
|
|
17
17
|
import { LibrarySaveDialog, type SaveDialogContext } from './library-save-dialog.tsx'
|
|
@@ -26,6 +26,55 @@ export interface StudioReuse {
|
|
|
26
26
|
voiceId?: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** 每模型可覆盖的参数行(留空 = 自动,沿用上方全局配置)。 */
|
|
30
|
+
const OVERRIDE_ROWS: Array<{ key: string; label: string; type: 'text' | 'number' | 'select'; options?: string[]; placeholder?: string }> = [
|
|
31
|
+
{ key: 'format', label: '输出格式', type: 'select', options: ['mp3', 'wav', 'pcm', 'flac', 'ogg'], placeholder: '自动' },
|
|
32
|
+
{ key: 'duration', label: '时长(秒)', type: 'number', placeholder: '自动' },
|
|
33
|
+
{ key: 'voice', label: '音色', type: 'text', placeholder: '自动' },
|
|
34
|
+
{ key: 'speed', label: '语速', type: 'number', placeholder: '自动' },
|
|
35
|
+
{ key: 'emotion', label: '情绪', type: 'text', placeholder: '自动' },
|
|
36
|
+
{ key: 'sample_rate', label: '采样率', type: 'number', placeholder: '自动' },
|
|
37
|
+
{ key: 'bitrate', label: '码率', type: 'number', placeholder: '自动' },
|
|
38
|
+
{ key: 'lyrics', label: '歌词', type: 'text', placeholder: '自动' },
|
|
39
|
+
{ key: 'seed', label: 'seed', type: 'number', placeholder: '自动' },
|
|
40
|
+
{ key: 'steps', label: 'steps', type: 'number', placeholder: '自动' },
|
|
41
|
+
{ key: 'cfg_scale', label: 'cfg_scale', type: 'number', placeholder: '自动' },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/** 每模型覆盖值 → 请求字段的数值/类型转换(空值跳过)。 */
|
|
45
|
+
function overrideSpread(override: Record<string, string>): Partial<GenerateAudioRequest> {
|
|
46
|
+
const out: Partial<GenerateAudioRequest> = {}
|
|
47
|
+
const val = override.format?.trim() ?? ''
|
|
48
|
+
if (val !== '') out.format = val
|
|
49
|
+
const num = (key: string): number | undefined => {
|
|
50
|
+
const raw = (override[key] ?? '').trim()
|
|
51
|
+
if (raw === '') return undefined
|
|
52
|
+
const parsed = Number(raw)
|
|
53
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
54
|
+
}
|
|
55
|
+
const duration = num('duration')
|
|
56
|
+
if (duration !== undefined) out.duration = duration
|
|
57
|
+
const voice = override.voice?.trim() ?? ''
|
|
58
|
+
if (voice !== '') out.voice = voice
|
|
59
|
+
const speed = num('speed')
|
|
60
|
+
if (speed !== undefined) out.speed = speed
|
|
61
|
+
const emotion = override.emotion?.trim() ?? ''
|
|
62
|
+
if (emotion !== '') out.emotion = emotion
|
|
63
|
+
const sampleRate = num('sample_rate')
|
|
64
|
+
if (sampleRate !== undefined) out.sampleRate = sampleRate
|
|
65
|
+
const bitrate = num('bitrate')
|
|
66
|
+
if (bitrate !== undefined) out.bitrate = bitrate
|
|
67
|
+
const lyrics = override.lyrics?.trim() ?? ''
|
|
68
|
+
if (lyrics !== '') out.lyrics = lyrics
|
|
69
|
+
const seed = num('seed')
|
|
70
|
+
if (seed !== undefined) out.seed = seed
|
|
71
|
+
const steps = num('steps')
|
|
72
|
+
if (steps !== undefined) out.steps = steps
|
|
73
|
+
const cfgScale = num('cfg_scale')
|
|
74
|
+
if (cfgScale !== undefined) out.cfgScale = cfgScale
|
|
75
|
+
return out
|
|
76
|
+
}
|
|
77
|
+
|
|
29
78
|
function useConfig(scope: AudiogenScope) {
|
|
30
79
|
const [value, setValue] = useState(scope.getSnapshot().value)
|
|
31
80
|
useEffect(() => scope.subscribe(() => { setValue(scope.getSnapshot().value) }), [scope])
|
|
@@ -121,6 +170,10 @@ export function StudioView(props: {
|
|
|
121
170
|
const [prompt, setPrompt] = useState('')
|
|
122
171
|
const [previewText, setPreviewText] = useState('')
|
|
123
172
|
const [model, setModel] = useState('')
|
|
173
|
+
// 多模型对比:同一提示词逐模型生成
|
|
174
|
+
const [selectedModels, setSelectedModels] = useState<string[]>([])
|
|
175
|
+
// 每模型参数覆盖(留空 = 自动沿用全局配置)
|
|
176
|
+
const [overrides, setOverrides] = useState<Record<string, Record<string, string>>>({})
|
|
124
177
|
const [voice, setVoice] = useState('')
|
|
125
178
|
const [speed, setSpeed] = useState('')
|
|
126
179
|
const [duration, setDuration] = useState('')
|
|
@@ -140,8 +193,9 @@ export function StudioView(props: {
|
|
|
140
193
|
const [audioChannel, setAudioChannel] = useState('')
|
|
141
194
|
const [subtitle, setSubtitle] = useState(false)
|
|
142
195
|
const [loading, setLoading] = useState(false)
|
|
196
|
+
const [progress, setProgress] = useState('')
|
|
143
197
|
const [error, setError] = useState<string | null>(null)
|
|
144
|
-
const [
|
|
198
|
+
const [results, setResults] = useState<Array<{ model: string; outputs: GeneratedAudio[]; error?: string }>>([])
|
|
145
199
|
// 资源库
|
|
146
200
|
const [saveToLibrary, setSaveToLibrary] = useState(cfg?.autoSaveToLibrary === true)
|
|
147
201
|
const [savedIds, setSavedIds] = useState<Set<string>>(new Set())
|
|
@@ -169,6 +223,33 @@ export function StudioView(props: {
|
|
|
169
223
|
.map(entry => entry.alias)
|
|
170
224
|
}, [modelOptions.models, mode])
|
|
171
225
|
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (visibleModels.length === 0) return
|
|
228
|
+
setSelectedModels(current => {
|
|
229
|
+
const valid = current.filter(item => visibleModels.includes(item))
|
|
230
|
+
if (valid.length > 0) return valid
|
|
231
|
+
return [visibleModels[0]!]
|
|
232
|
+
})
|
|
233
|
+
}, [visibleModels])
|
|
234
|
+
|
|
235
|
+
// 每模型覆盖值随勾选模型收缩(保留仍在勾选中的覆盖)
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
setOverrides(current => {
|
|
238
|
+
const next: Record<string, Record<string, string>> = {}
|
|
239
|
+
for (const alias of selectedModels) {
|
|
240
|
+
if (current[alias] !== undefined) next[alias] = current[alias]!
|
|
241
|
+
}
|
|
242
|
+
return next
|
|
243
|
+
})
|
|
244
|
+
}, [selectedModels])
|
|
245
|
+
|
|
246
|
+
const setOverrideValue = (model: string, key: string, value: string): void => {
|
|
247
|
+
setOverrides(current => ({
|
|
248
|
+
...current,
|
|
249
|
+
[model]: { ...(current[model] ?? {}), [key]: value },
|
|
250
|
+
}))
|
|
251
|
+
}
|
|
252
|
+
|
|
172
253
|
useEffect(() => {
|
|
173
254
|
if (visibleModels.length > 0 && !visibleModels.includes(model)) {
|
|
174
255
|
setModel(visibleModels[0]!)
|
|
@@ -180,7 +261,10 @@ export function StudioView(props: {
|
|
|
180
261
|
if (reuse === undefined || reuse === null) return
|
|
181
262
|
setMode(reuse.mode)
|
|
182
263
|
if (reuse.voiceId !== undefined || reuse.voice !== undefined) setVoice(reuse.voiceId ?? reuse.voice ?? '')
|
|
183
|
-
if (reuse.model !== undefined && reuse.model !== '')
|
|
264
|
+
if (reuse.model !== undefined && reuse.model !== '') {
|
|
265
|
+
setModel(reuse.model)
|
|
266
|
+
setSelectedModels([reuse.model])
|
|
267
|
+
}
|
|
184
268
|
}, [reuse?.nonce])
|
|
185
269
|
|
|
186
270
|
const submit = async (): Promise<void> => {
|
|
@@ -188,49 +272,80 @@ export function StudioView(props: {
|
|
|
188
272
|
setError(tt('prompt.required'))
|
|
189
273
|
return
|
|
190
274
|
}
|
|
275
|
+
// 待生成模型列表:音色设计单渠道;其余支持多模型(同一提示词逐一生成,供对比)。
|
|
276
|
+
const targets = mode === 'voice_design'
|
|
277
|
+
? ['']
|
|
278
|
+
: selectedModels.length > 0
|
|
279
|
+
? selectedModels
|
|
280
|
+
: (model !== '' ? [model] : visibleModels.length > 0 ? [visibleModels[0]!] : [])
|
|
281
|
+
if (targets.length === 0) {
|
|
282
|
+
setError('当前模式暂无可用模型')
|
|
283
|
+
return
|
|
284
|
+
}
|
|
191
285
|
setLoading(true)
|
|
192
286
|
setError(null)
|
|
287
|
+
setProgress('')
|
|
288
|
+
const gathered: Array<{ model: string; outputs: GeneratedAudio[]; error?: string }> = []
|
|
289
|
+
let anySaved = false
|
|
193
290
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
291
|
+
for (const [index, target] of targets.entries()) {
|
|
292
|
+
setProgress(`生成中 ${index + 1}/${targets.length}(${target || '音色设计'})…`)
|
|
293
|
+
let response
|
|
294
|
+
try {
|
|
295
|
+
response = await api.generate({
|
|
296
|
+
mode,
|
|
297
|
+
model: target,
|
|
298
|
+
prompt: prompt.trim(),
|
|
299
|
+
saveToLibrary,
|
|
300
|
+
...(mode === 'voice_design' && designChannelId !== '' ? { channelId: designChannelId } : {}),
|
|
301
|
+
...(previewText.trim() !== '' ? { previewText: previewText.trim() } : {}),
|
|
302
|
+
...(voice.trim() !== '' ? { voice: voice.trim() } : {}),
|
|
303
|
+
...(speed.trim() !== '' ? { speed: Number(speed) } : {}),
|
|
304
|
+
...(duration.trim() !== '' ? { duration: Number(duration) } : {}),
|
|
305
|
+
...(lyrics.trim() !== '' ? { lyrics: lyrics.trim() } : {}),
|
|
306
|
+
...(instrumental ? { isInstrumental: true } : {}),
|
|
307
|
+
...(loop ? { loop: true } : {}),
|
|
308
|
+
...(promptInfluence.trim() !== '' ? { promptInfluence: Number(promptInfluence) } : {}),
|
|
309
|
+
...(format.trim() !== '' ? { format: format.trim() } : {}),
|
|
310
|
+
...(emotion.trim() !== '' ? { emotion: emotion.trim() } : {}),
|
|
311
|
+
...(vol.trim() !== '' ? { vol: Number(vol) } : {}),
|
|
312
|
+
...(pitch.trim() !== '' ? { pitch: Number(pitch) } : {}),
|
|
313
|
+
...(toneText.trim() !== '' ? { pronunciationTone: toneText.split('\n').map(item => item.trim()).filter(item => item !== '') } : {}),
|
|
314
|
+
...(sampleRate.trim() !== '' ? { sampleRate: Number(sampleRate) } : {}),
|
|
315
|
+
...(bitrate.trim() !== '' ? { bitrate: Number(bitrate) } : {}),
|
|
316
|
+
...(audioChannel.trim() !== '' ? { audioChannel: Number(audioChannel) } : {}),
|
|
317
|
+
...(subtitle ? { subtitleEnable: true } : {}),
|
|
318
|
+
// 每模型参数覆盖(后置覆盖全局;仅音色设计外生效)
|
|
319
|
+
...(target === '' ? {} : overrideSpread(overrides[target] ?? {})),
|
|
320
|
+
})
|
|
321
|
+
} catch (err) {
|
|
322
|
+
gathered.push({ model: target, outputs: [], error: err instanceof Error ? err.message : String(err) })
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
gathered.push({ model: target, outputs: [], error: response.message ?? '生成失败' })
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
const generated = response.outputs ?? []
|
|
330
|
+
gathered.push({ model: target, outputs: generated })
|
|
331
|
+
if ((response.resources?.length ?? 0) > 0 && saveToLibrary) anySaved = true
|
|
221
332
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
setSavedIds(new Set(generated.map(item => item.id)))
|
|
333
|
+
setResults(gathered)
|
|
334
|
+
if (anySaved) {
|
|
335
|
+
setSavedIds(current => new Set([...current, ...gathered.flatMap(group => group.outputs.map(item => item.id))]))
|
|
226
336
|
props.showToast('已保存到资源库')
|
|
227
337
|
props.onLibraryChanged()
|
|
228
338
|
}
|
|
339
|
+
const failed = gathered.filter(group => group.error !== undefined && group.outputs.length === 0)
|
|
340
|
+
if (failed.length > 0 && failed.length === gathered.length) {
|
|
341
|
+
setError(failed.map(group => `「${group.model || '音色设计'}」${group.error}`).join(';'))
|
|
342
|
+
}
|
|
229
343
|
reload()
|
|
230
344
|
} catch (err) {
|
|
231
345
|
setError(err instanceof Error ? err.message : String(err))
|
|
232
346
|
} finally {
|
|
233
347
|
setLoading(false)
|
|
348
|
+
setProgress('')
|
|
234
349
|
}
|
|
235
350
|
}
|
|
236
351
|
|
|
@@ -301,14 +416,65 @@ export function StudioView(props: {
|
|
|
301
416
|
|
|
302
417
|
{needModel ? (
|
|
303
418
|
<label className={css.label}>
|
|
304
|
-
<span>{tt('model.label')}
|
|
305
|
-
<
|
|
306
|
-
{visibleModels.length === 0 ? <
|
|
307
|
-
{visibleModels.map(item =>
|
|
308
|
-
|
|
419
|
+
<span>{tt('model.label')}(可多选,同一提示词逐个生成以对比)</span>
|
|
420
|
+
<div className={css.modelCheckList}>
|
|
421
|
+
{visibleModels.length === 0 ? <p className={css.hint}>(当前模式暂无可用模型)</p> : null}
|
|
422
|
+
{visibleModels.map(item => (
|
|
423
|
+
<label key={item} className={css.checkbox}>
|
|
424
|
+
<input
|
|
425
|
+
type="checkbox"
|
|
426
|
+
checked={selectedModels.includes(item)}
|
|
427
|
+
onChange={event => {
|
|
428
|
+
const checked = event.target.checked
|
|
429
|
+
setSelectedModels(current => checked ? [...new Set([...current, item])] : current.filter(alias => alias !== item))
|
|
430
|
+
if (checked && (model === '' || !visibleModels.includes(model))) setModel(item)
|
|
431
|
+
}}
|
|
432
|
+
/>
|
|
433
|
+
<span title="点击生成对比">{item}</span>
|
|
434
|
+
</label>
|
|
435
|
+
))}
|
|
436
|
+
</div>
|
|
309
437
|
</label>
|
|
310
438
|
) : null}
|
|
311
439
|
|
|
440
|
+
{needModel && selectedModels.length > 0 ? (
|
|
441
|
+
<details className={css.advanced}>
|
|
442
|
+
<summary>每模型参数覆盖(默认自动:沿用上方相同配置,如输出格式均为 mp3)</summary>
|
|
443
|
+
<div className={css.overrideTable}>
|
|
444
|
+
<div className={css.overrideRow}>
|
|
445
|
+
<span className={`${css.overrideCell} ${css.overrideCellHead}`} />
|
|
446
|
+
{selectedModels.map(item => <span key={item} className={`${css.overrideCell} ${css.overrideCellHead}`}>{item}</span>)}
|
|
447
|
+
</div>
|
|
448
|
+
{OVERRIDE_ROWS.map(row => (
|
|
449
|
+
<div key={row.key} className={css.overrideRow}>
|
|
450
|
+
<span className={css.overrideCell} title={row.label}>{row.label}</span>
|
|
451
|
+
{selectedModels.map(item => {
|
|
452
|
+
const value = overrides[item]?.[row.key] ?? ''
|
|
453
|
+
return (
|
|
454
|
+
<span key={item} className={css.overrideCell}>
|
|
455
|
+
{row.type === 'select' ? (
|
|
456
|
+
<select className={css.input} value={value} onChange={event => setOverrideValue(item, row.key, event.target.value)}>
|
|
457
|
+
<option value="">自动</option>
|
|
458
|
+
{row.options!.map(option => <option key={option} value={option}>{option}</option>)}
|
|
459
|
+
</select>
|
|
460
|
+
) : (
|
|
461
|
+
<input
|
|
462
|
+
className={css.input}
|
|
463
|
+
type={row.type === 'number' ? 'number' : 'text'}
|
|
464
|
+
value={value}
|
|
465
|
+
placeholder={row.placeholder ?? '自动'}
|
|
466
|
+
onChange={event => setOverrideValue(item, row.key, event.target.value)}
|
|
467
|
+
/>
|
|
468
|
+
)}
|
|
469
|
+
</span>
|
|
470
|
+
)
|
|
471
|
+
})}
|
|
472
|
+
</div>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
</details>
|
|
476
|
+
) : null}
|
|
477
|
+
|
|
312
478
|
{mode === 'tts' ? (
|
|
313
479
|
<label className={css.label}>
|
|
314
480
|
<span>{tt('voice.label')}</span>
|
|
@@ -448,13 +614,13 @@ export function StudioView(props: {
|
|
|
448
614
|
|
|
449
615
|
{!connected && <p className={css.hint}>{tt('config.missing')}</p>}
|
|
450
616
|
<button type="button" className={css.generate} disabled={loading || !connected || (needModel && visibleModels.length === 0)} onClick={() => void submit()}>
|
|
451
|
-
{loading ? tt('generating') : tt('generate')}
|
|
617
|
+
{loading ? (progress !== '' ? progress : tt('generating')) : tt('generate')}
|
|
452
618
|
</button>
|
|
453
619
|
</div>
|
|
454
620
|
|
|
455
621
|
<div className={css.resultCol}>
|
|
456
622
|
{error !== null ? <p className={css.error}>{error}</p> : null}
|
|
457
|
-
{
|
|
623
|
+
{results.length === 0 ? (
|
|
458
624
|
<div className={css.resultEmpty}>
|
|
459
625
|
<span className={css.resultEmptyIcon}>🎵</span>
|
|
460
626
|
<p>{tt('result.empty')}</p>
|
|
@@ -463,59 +629,72 @@ export function StudioView(props: {
|
|
|
463
629
|
) : (
|
|
464
630
|
<>
|
|
465
631
|
<div className={css.resultMeta}>
|
|
466
|
-
<span>{tt('result.done', { count: outputs.length })}</span>
|
|
632
|
+
<span>{tt('result.done', { count: results.reduce((sum, group) => sum + group.outputs.length, 0) })}</span>
|
|
467
633
|
<span className={css.resultModeChip}>{modeLabel}</span>
|
|
468
634
|
</div>
|
|
469
|
-
<div className={css.
|
|
470
|
-
{
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
{saved ? (
|
|
477
|
-
<span className={css.savedChip}><CheckIcon /> 已入库</span>
|
|
478
|
-
) : null}
|
|
479
|
-
<span className={css.audioCardIndex}>#{index + 1}</span>
|
|
480
|
-
</div>
|
|
481
|
-
<AudioPlayer src={dataUrlOf(audio)} itemKey={audio.id} />
|
|
482
|
-
<div className={css.audioCardActions}>
|
|
483
|
-
<a className={css.ghostButton} href={dataUrlOf(audio)} download={`generated-${index + 1}.${audio.mime.split('/')[1]?.replace('mpeg', 'mp3') ?? 'mp3'}`}>
|
|
484
|
-
<DownloadIcon /> 下载
|
|
485
|
-
</a>
|
|
486
|
-
{saved ? (
|
|
487
|
-
<button type="button" className={css.ghostButton} onClick={() => props.showToast('该音频已加入资源库')}>
|
|
488
|
-
<CheckIcon /> 已入库
|
|
489
|
-
</button>
|
|
490
|
-
) : (
|
|
491
|
-
<button
|
|
492
|
-
type="button"
|
|
493
|
-
className={css.ghostButton}
|
|
494
|
-
onClick={() => openSaveDialog([audio], {
|
|
495
|
-
mode,
|
|
496
|
-
prompt: prompt.trim(),
|
|
497
|
-
...(voice.trim() !== '' ? { voice: voice.trim() } : {}),
|
|
498
|
-
...(audio.voiceId === undefined ? {} : { voiceId: audio.voiceId }),
|
|
499
|
-
...(model !== '' ? { model } : {}),
|
|
500
|
-
...(channels.length > 0 ? { channel: channels.find(candidate => candidate.id === (mode === 'voice_design' ? designChannelId : modelOptions.defaultChannelId))?.name ?? channels[0]?.name ?? '' } : {}),
|
|
501
|
-
params: {
|
|
502
|
-
mode,
|
|
503
|
-
model: mode === 'voice_design' ? '' : (model || visibleModels[0]) ?? '',
|
|
504
|
-
prompt: prompt.trim(),
|
|
505
|
-
...(voice.trim() !== '' ? { voice: voice.trim() } : {}),
|
|
506
|
-
...(speed.trim() !== '' ? { speed: Number(speed) } : {}),
|
|
507
|
-
...(duration.trim() !== '' ? { duration: Number(duration) } : {}),
|
|
508
|
-
...(format.trim() !== '' ? { format: format.trim() } : {}),
|
|
509
|
-
},
|
|
510
|
-
})}
|
|
511
|
-
>
|
|
512
|
-
<StarIcon /> 加入资源库
|
|
513
|
-
</button>
|
|
514
|
-
)}
|
|
515
|
-
</div>
|
|
635
|
+
<div className={css.resultGroups}>
|
|
636
|
+
{results.map((group, groupIndex) => (
|
|
637
|
+
<div className={css.resultGroup} key={`${group.model}-${groupIndex}`}>
|
|
638
|
+
<div className={css.resultGroupHead}>
|
|
639
|
+
<span className={css.resultGroupChip}>{group.model || '音色设计'}</span>
|
|
640
|
+
{group.error !== undefined ? <span className={css.resultGroupError}>生成失败:{group.error}</span> : null}
|
|
641
|
+
{group.outputs.length > 0 ? <span className={css.resultGroupCount}>{group.outputs.length} 段</span> : null}
|
|
516
642
|
</div>
|
|
517
|
-
|
|
518
|
-
|
|
643
|
+
{group.outputs.length > 0 ? (
|
|
644
|
+
<div className={css.audioList}>
|
|
645
|
+
{group.outputs.map((audio, index) => {
|
|
646
|
+
const saved = savedIds.has(audio.id)
|
|
647
|
+
return (
|
|
648
|
+
<div className={css.audioCard} key={audio.id} data-saved={saved ? 'true' : 'false'}>
|
|
649
|
+
<div className={css.audioCardHead}>
|
|
650
|
+
{audio.voiceId !== undefined ? <span className={css.voiceIdChip} title="新音色 ID">新音色 {audio.voiceId}</span> : null}
|
|
651
|
+
{saved ? (
|
|
652
|
+
<span className={css.savedChip}><CheckIcon /> 已入库</span>
|
|
653
|
+
) : null}
|
|
654
|
+
<span className={css.audioCardIndex}>#{index + 1}</span>
|
|
655
|
+
</div>
|
|
656
|
+
<AudioPlayer src={dataUrlOf(audio)} itemKey={audio.id} />
|
|
657
|
+
<div className={css.audioCardActions}>
|
|
658
|
+
<a className={css.ghostButton} href={dataUrlOf(audio)} download={`generated-${groupIndex + 1}-${index + 1}.${audio.mime.split('/')[1]?.replace('mpeg', 'mp3') ?? 'mp3'}`}>
|
|
659
|
+
<DownloadIcon /> 下载
|
|
660
|
+
</a>
|
|
661
|
+
{saved ? (
|
|
662
|
+
<button type="button" className={css.ghostButton} onClick={() => props.showToast('该音频已加入资源库')}>
|
|
663
|
+
<CheckIcon /> 已入库
|
|
664
|
+
</button>
|
|
665
|
+
) : (
|
|
666
|
+
<button
|
|
667
|
+
type="button"
|
|
668
|
+
className={css.ghostButton}
|
|
669
|
+
onClick={() => openSaveDialog([audio], {
|
|
670
|
+
mode,
|
|
671
|
+
prompt: prompt.trim(),
|
|
672
|
+
...(voice.trim() !== '' ? { voice: voice.trim() } : {}),
|
|
673
|
+
...(audio.voiceId === undefined ? {} : { voiceId: audio.voiceId }),
|
|
674
|
+
...(group.model !== '' ? { model: group.model } : {}),
|
|
675
|
+
...(channels.length > 0 ? { channel: channels.find(candidate => candidate.id === (mode === 'voice_design' ? designChannelId : modelOptions.defaultChannelId))?.name ?? channels[0]?.name ?? '' } : {}),
|
|
676
|
+
params: {
|
|
677
|
+
mode,
|
|
678
|
+
model: mode === 'voice_design' ? '' : group.model,
|
|
679
|
+
prompt: prompt.trim(),
|
|
680
|
+
...(voice.trim() !== '' ? { voice: voice.trim() } : {}),
|
|
681
|
+
...(speed.trim() !== '' ? { speed: Number(speed) } : {}),
|
|
682
|
+
...(duration.trim() !== '' ? { duration: Number(duration) } : {}),
|
|
683
|
+
...(format.trim() !== '' ? { format: format.trim() } : {}),
|
|
684
|
+
},
|
|
685
|
+
})}
|
|
686
|
+
>
|
|
687
|
+
<StarIcon /> 加入资源库
|
|
688
|
+
</button>
|
|
689
|
+
)}
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
)
|
|
693
|
+
})}
|
|
694
|
+
</div>
|
|
695
|
+
) : null}
|
|
696
|
+
</div>
|
|
697
|
+
))}
|
|
519
698
|
</div>
|
|
520
699
|
</>
|
|
521
700
|
)}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
11
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, copyFileSync } from 'node:fs'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
12
15
|
import z from 'schemastery'
|
|
13
16
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
14
17
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
@@ -81,6 +84,34 @@ function guidanceFor(channels: AudioChannel[], defaultChannelId: string): string
|
|
|
81
84
|
return `${AUDIOGEN_GUIDANCE} 当前渠道与模型:${table}。`
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/**
|
|
88
|
+
* 把随包分发的技能(skills/<id>/SKILL.md,含 frontmatter)同步到 DSH 用户技能根
|
|
89
|
+
* `~/.dsh/skills/<id>/SKILL.md` —— DSH web 会话的 skill-filesystem(standard 等
|
|
90
|
+
* preset 行)会扫描用户根,使会话可直接触发这些技能。仅创建缺失文件,绝不覆盖
|
|
91
|
+
* 用户已有内容;任何失败仅告警,不影响插件本身。
|
|
92
|
+
*/
|
|
93
|
+
function syncBundledSkills(): void {
|
|
94
|
+
try {
|
|
95
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
96
|
+
const sourceRoot = join(packageRoot, 'skills')
|
|
97
|
+
if (existsSync(sourceRoot) !== true) return
|
|
98
|
+
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? '', '.dsh')
|
|
99
|
+
const targetRoot = join(dshHome, 'skills')
|
|
100
|
+
for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
101
|
+
if (entry.isDirectory() !== true) continue
|
|
102
|
+
const sourceFile = join(sourceRoot, entry.name, 'SKILL.md')
|
|
103
|
+
if (existsSync(sourceFile) !== true) continue
|
|
104
|
+
const targetDir = join(targetRoot, entry.name)
|
|
105
|
+
const targetFile = join(targetDir, 'SKILL.md')
|
|
106
|
+
if (existsSync(targetFile)) continue
|
|
107
|
+
mkdirSync(targetDir, { recursive: true })
|
|
108
|
+
copyFileSync(sourceFile, targetFile)
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// 技能同步为最佳努力:失败不阻断插件启动。
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
function normalizeChannels(value: unknown): ChannelConfig[] {
|
|
85
116
|
if (!Array.isArray(value)) return []
|
|
86
117
|
const out: ChannelConfig[] = []
|
|
@@ -122,6 +153,7 @@ export interface EffectiveConfig {
|
|
|
122
153
|
}
|
|
123
154
|
|
|
124
155
|
export function apply(ctx: Context, config?: Config): void {
|
|
156
|
+
syncBundledSkills()
|
|
125
157
|
let current: () => Config = () => config ?? {}
|
|
126
158
|
|
|
127
159
|
const resolve = (): EffectiveConfig => {
|
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.1'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
|
|
14
14
|
export const SETTINGS_API = {
|