dsh-audiogen 0.4.2 → 0.4.4
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 +536 -357
- package/lib/client.js.map +1 -1
- package/lib/index.js +6 -2
- package/package.json +1 -1
- package/src/client/SettingsCard.tsx +6 -1
- package/src/client/api.ts +7 -0
- package/src/client/audio-panel.module.css +100 -2
- package/src/client/studio-view.tsx +133 -5
- package/src/index.ts +8 -5
- package/src/protocol.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -3035,7 +3035,7 @@ const Config = z.object({
|
|
|
3035
3035
|
defaultChannelId: z.string().default(""),
|
|
3036
3036
|
defaultModel: z.string().default(""),
|
|
3037
3037
|
autoSaveToLibrary: z.boolean().default(false),
|
|
3038
|
-
maxConcurrentGenerations: z.number().default(DEFAULT_MAX_CONCURRENT)
|
|
3038
|
+
maxConcurrentGenerations: z.union([z.number(), z.string()]).default(DEFAULT_MAX_CONCURRENT)
|
|
3039
3039
|
});
|
|
3040
3040
|
const DEFAULT_ENABLED = true;
|
|
3041
3041
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -3129,7 +3129,11 @@ function apply(ctx, config) {
|
|
|
3129
3129
|
defaultChannelId,
|
|
3130
3130
|
defaultModel: typeof value.defaultModel === "string" ? value.defaultModel.trim() : "",
|
|
3131
3131
|
autoSaveToLibrary: value.autoSaveToLibrary === true,
|
|
3132
|
-
maxConcurrentGenerations:
|
|
3132
|
+
maxConcurrentGenerations: (() => {
|
|
3133
|
+
const rawMax = value.maxConcurrentGenerations;
|
|
3134
|
+
const parsedMax = typeof rawMax === "number" ? rawMax : typeof rawMax === "string" && rawMax.trim() !== "" ? Number(rawMax.trim()) : NaN;
|
|
3135
|
+
return Number.isFinite(parsedMax) ? Math.max(1, Math.min(20, Math.floor(parsedMax))) : DEFAULT_MAX_CONCURRENT;
|
|
3136
|
+
})()
|
|
3133
3137
|
};
|
|
3134
3138
|
};
|
|
3135
3139
|
const budget = createGenerationBudget(() => resolve().maxConcurrentGenerations);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-audiogen",
|
|
3
3
|
"description": "AI audio generation plugin for the dsh web GUI: multi-vendor TTS/music/sound-effect channels (OpenAI-compatible, ElevenLabs, MiniMax, Stability AI and custom), per-channel model/voice catalogs, Agent tool and a sidebar AI 音频 panel.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -737,7 +737,12 @@ export function AudioGenSettingsCard(props: AudioGenSettingsCardProps) {
|
|
|
737
737
|
className={css.input}
|
|
738
738
|
value={state.maxConcurrentGenerations.text}
|
|
739
739
|
disabled={!state.writable}
|
|
740
|
-
onChange={event =>
|
|
740
|
+
onChange={event => {
|
|
741
|
+
const raw = event.target.value
|
|
742
|
+
const parsed = Number(raw)
|
|
743
|
+
const value = raw === '' || !Number.isFinite(parsed) ? '' : String(Math.max(1, Math.min(20, Math.floor(parsed))))
|
|
744
|
+
props.edit('maxConcurrentGenerations', value)
|
|
745
|
+
}}
|
|
741
746
|
/>
|
|
742
747
|
</label>
|
|
743
748
|
</div>
|
package/src/client/api.ts
CHANGED
|
@@ -48,6 +48,13 @@ export class AudiogenApi {
|
|
|
48
48
|
return body.ok === true ? (body.history ?? []) : []
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** 删除一条历史记录(返回删后的列表)。 */
|
|
52
|
+
async removeHistory(id: string): Promise<HistoryEntry[]> {
|
|
53
|
+
const response = await postJson(HISTORY_API.remove, { id })
|
|
54
|
+
const body = await response.json() as { ok?: boolean; history?: HistoryEntry[] }
|
|
55
|
+
return body.ok === true ? (body.history ?? []) : []
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
async clearHistory(): Promise<void> {
|
|
52
59
|
await postJson(HISTORY_API.clear, {})
|
|
53
60
|
}
|
|
@@ -953,7 +953,7 @@
|
|
|
953
953
|
font-weight: 600;
|
|
954
954
|
padding: 3px 10px;
|
|
955
955
|
border-radius: 999px;
|
|
956
|
-
background: var(--dsw-alias-bg-hover,
|
|
956
|
+
background: var(--dsw-alias-interactive-bg-hover-accent, rgba(38, 49, 72, 0.14));
|
|
957
957
|
border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
|
|
958
958
|
color: var(--dsw-alias-label-primary, #1f2328);
|
|
959
959
|
}
|
|
@@ -1083,7 +1083,8 @@
|
|
|
1083
1083
|
}
|
|
1084
1084
|
|
|
1085
1085
|
.historyTab[data-active='true'] {
|
|
1086
|
-
|
|
1086
|
+
/* 半透明交互强调色(亮色=深色 tint,暗色=白色 tint),避免选中整块白底 */
|
|
1087
|
+
background: var(--dsw-alias-interactive-bg-hover-accent, rgba(38, 49, 72, 0.14));
|
|
1087
1088
|
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1088
1089
|
font-weight: 600;
|
|
1089
1090
|
}
|
|
@@ -1133,3 +1134,100 @@
|
|
|
1133
1134
|
color: var(--dsw-alias-label-tertiary, #9ca3af);
|
|
1134
1135
|
padding: 0 6px;
|
|
1135
1136
|
}
|
|
1137
|
+
|
|
1138
|
+
.historyTime {
|
|
1139
|
+
font-size: 11px;
|
|
1140
|
+
color: var(--dsw-alias-label-tertiary, #9ca3af);
|
|
1141
|
+
margin: 2px 0;
|
|
1142
|
+
white-space: nowrap;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/* 表单分区与两列排布 */
|
|
1146
|
+
.formSection {
|
|
1147
|
+
font-size: 11px;
|
|
1148
|
+
font-weight: 700;
|
|
1149
|
+
letter-spacing: 0.04em;
|
|
1150
|
+
color: var(--dsw-alias-label-tertiary, #9ca3af);
|
|
1151
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1, #e5e7eb);
|
|
1152
|
+
padding-bottom: 4px;
|
|
1153
|
+
margin: 10px 0 2px;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.formFields {
|
|
1157
|
+
display: grid;
|
|
1158
|
+
grid-template-columns: 1fr 1fr;
|
|
1159
|
+
gap: 10px;
|
|
1160
|
+
align-items: end;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.fieldCell {
|
|
1164
|
+
min-width: 0;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.fieldFull {
|
|
1168
|
+
grid-column: 1 / -1;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/* 模式胶囊 */
|
|
1172
|
+
.modeIcon {
|
|
1173
|
+
font-size: 13px;
|
|
1174
|
+
margin-right: 4px;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
.modeButton[data-active='true'] {
|
|
1178
|
+
border-color: var(--dsw-alias-interactive-bg-hover-accent, rgba(38, 49, 72, 0.14));
|
|
1179
|
+
background: var(--dsw-alias-interactive-bg-hover-accent, rgba(38, 49, 72, 0.14));
|
|
1180
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1181
|
+
font-weight: 600;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/* 任务细进度条 */
|
|
1185
|
+
.taskBar {
|
|
1186
|
+
display: block;
|
|
1187
|
+
flex: 1;
|
|
1188
|
+
min-width: 60px;
|
|
1189
|
+
height: 4px;
|
|
1190
|
+
border-radius: 999px;
|
|
1191
|
+
background: var(--dsw-alias-border-l1, #e5e7eb);
|
|
1192
|
+
overflow: hidden;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
.taskBar i {
|
|
1196
|
+
display: block;
|
|
1197
|
+
height: 100%;
|
|
1198
|
+
border-radius: 999px;
|
|
1199
|
+
background: var(--dsw-alias-interactive-bg-hover-accent, rgba(38, 49, 72, 0.35));
|
|
1200
|
+
transition: width 0.3s;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/* 历史行内图标操作 */
|
|
1204
|
+
.historyIcon {
|
|
1205
|
+
width: 26px;
|
|
1206
|
+
height: 26px;
|
|
1207
|
+
display: inline-flex;
|
|
1208
|
+
align-items: center;
|
|
1209
|
+
justify-content: center;
|
|
1210
|
+
border-radius: 8px;
|
|
1211
|
+
border: 1px solid transparent;
|
|
1212
|
+
background: none;
|
|
1213
|
+
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
1214
|
+
font-size: 14px;
|
|
1215
|
+
cursor: pointer;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.historyIcon:hover {
|
|
1219
|
+
background: var(--dsw-alias-interactive-bg-hover-accent, rgba(38, 49, 72, 0.14));
|
|
1220
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.historyIcon[title^='删除']:hover {
|
|
1224
|
+
color: #dc2626;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/* 历史 prompt 两行截断 */
|
|
1228
|
+
.historyPrompt {
|
|
1229
|
+
display: -webkit-box;
|
|
1230
|
+
-webkit-line-clamp: 2;
|
|
1231
|
+
-webkit-box-orient: vertical;
|
|
1232
|
+
overflow: hidden;
|
|
1233
|
+
}
|
|
@@ -101,6 +101,14 @@ function modeLabelOf(mode: AudioMode): string {
|
|
|
101
101
|
return tt('mode.voiceDesign')
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** 历史时间显示(YYYY-MM-DD HH:mm)。 */
|
|
105
|
+
function formatClock(timestamp: number): string {
|
|
106
|
+
const date = new Date(timestamp)
|
|
107
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
108
|
+
const pad = (n: number): string => String(n).padStart(2, '0')
|
|
109
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
function useConfig(scope: AudiogenScope) {
|
|
105
113
|
const [value, setValue] = useState(scope.getSnapshot().value)
|
|
106
114
|
useEffect(() => scope.subscribe(() => { setValue(scope.getSnapshot().value) }), [scope])
|
|
@@ -499,6 +507,87 @@ export function StudioView(props: {
|
|
|
499
507
|
setTasks(current => current.filter(task => task.id !== taskId))
|
|
500
508
|
}
|
|
501
509
|
|
|
510
|
+
/** 从历史参数回填表单(参考 AI 生图「恢复」):配置 + prompt 一键复用。 */
|
|
511
|
+
const restoreFromParams = (params: Record<string, unknown>, modeValue: AudioMode, singleModel: string, compareModelsRestore?: string[]): void => {
|
|
512
|
+
const str = (key: string): string | undefined => {
|
|
513
|
+
const v = params[key]
|
|
514
|
+
return typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined
|
|
515
|
+
}
|
|
516
|
+
const num = (key: string): number | undefined => {
|
|
517
|
+
const v = params[key]
|
|
518
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v
|
|
519
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
520
|
+
const parsed = Number(v)
|
|
521
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
522
|
+
}
|
|
523
|
+
return undefined
|
|
524
|
+
}
|
|
525
|
+
const bool = (key: string): boolean | undefined => typeof params[key] === 'boolean' ? params[key] as boolean : undefined
|
|
526
|
+
setMode(modeValue)
|
|
527
|
+
const modelValue = str('model') ?? singleModel
|
|
528
|
+
if (modelValue !== '') setModel(modelValue)
|
|
529
|
+
if (compareModelsRestore !== undefined && compareModelsRestore.length > 0) {
|
|
530
|
+
setCompareMode(true)
|
|
531
|
+
setCompareModels(compareModelsRestore)
|
|
532
|
+
} else {
|
|
533
|
+
setCompareMode(false)
|
|
534
|
+
}
|
|
535
|
+
const voiceValue = str('voice')
|
|
536
|
+
if (voiceValue !== undefined) setVoice(voiceValue)
|
|
537
|
+
const speedValue = num('speed')
|
|
538
|
+
if (speedValue !== undefined) setSpeed(String(speedValue))
|
|
539
|
+
const durationValue = num('duration')
|
|
540
|
+
if (durationValue !== undefined) setDuration(String(durationValue))
|
|
541
|
+
const formatValue = str('format')
|
|
542
|
+
if (formatValue !== undefined) setFormat(formatValue)
|
|
543
|
+
const lyricsValue = str('lyrics')
|
|
544
|
+
if (lyricsValue !== undefined) setLyrics(lyricsValue)
|
|
545
|
+
const instrumentalValue = bool('isInstrumental')
|
|
546
|
+
if (instrumentalValue !== undefined) setInstrumental(instrumentalValue)
|
|
547
|
+
const loopValue = bool('loop')
|
|
548
|
+
if (loopValue !== undefined) setLoop(loopValue)
|
|
549
|
+
const influenceValue = num('promptInfluence')
|
|
550
|
+
if (influenceValue !== undefined) setPromptInfluence(String(influenceValue))
|
|
551
|
+
const emotionValue = str('emotion')
|
|
552
|
+
if (emotionValue !== undefined) setEmotion(emotionValue)
|
|
553
|
+
const volValue = num('vol')
|
|
554
|
+
if (volValue !== undefined) setVol(String(volValue))
|
|
555
|
+
const pitchValue = num('pitch')
|
|
556
|
+
if (pitchValue !== undefined) setPitch(String(pitchValue))
|
|
557
|
+
if (Array.isArray(params.pronunciationTone)) {
|
|
558
|
+
setToneText(params.pronunciationTone.filter((item): item is string => typeof item === 'string').join('\n'))
|
|
559
|
+
}
|
|
560
|
+
const sampleRateValue = num('sampleRate')
|
|
561
|
+
if (sampleRateValue !== undefined) setSampleRate(String(sampleRateValue))
|
|
562
|
+
const bitrateValue = num('bitrate')
|
|
563
|
+
if (bitrateValue !== undefined) setBitrate(String(bitrateValue))
|
|
564
|
+
const channelValue = num('audioChannel')
|
|
565
|
+
if (channelValue !== undefined) setAudioChannel(String(channelValue))
|
|
566
|
+
const subtitleValue = bool('subtitleEnable')
|
|
567
|
+
if (subtitleValue !== undefined) setSubtitle(subtitleValue)
|
|
568
|
+
const seedValue = num('seed')
|
|
569
|
+
if (seedValue !== undefined) setSeed(String(seedValue))
|
|
570
|
+
const stepsValue = num('steps')
|
|
571
|
+
if (stepsValue !== undefined) setSteps(String(stepsValue))
|
|
572
|
+
const cfgValue = num('cfgScale')
|
|
573
|
+
if (cfgValue !== undefined) setCfgScale(String(cfgValue))
|
|
574
|
+
const previewValue = str('previewText')
|
|
575
|
+
if (previewValue !== undefined) setPreviewText(previewValue)
|
|
576
|
+
const channelIdValue = str('channelId')
|
|
577
|
+
if (channelIdValue !== undefined && modeValue === 'voice_design') setDesignChannelId(channelIdValue)
|
|
578
|
+
props.showToast('已恢复该次生成的配置,可直接再次生成')
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** 删除历史记录(对比任务卡删除该任务的全部模型条目)。 */
|
|
582
|
+
const deleteHistoryEntries = async (ids: string[]): Promise<void> => {
|
|
583
|
+
try {
|
|
584
|
+
for (const id of ids) await api.removeHistory(id)
|
|
585
|
+
} catch {
|
|
586
|
+
// best-effort
|
|
587
|
+
}
|
|
588
|
+
reload()
|
|
589
|
+
}
|
|
590
|
+
|
|
502
591
|
const openSaveDialog = (files: GeneratedAudio[], context: SaveDialogContext): void => {
|
|
503
592
|
setSaveDialog({ files, context })
|
|
504
593
|
}
|
|
@@ -767,7 +856,7 @@ export function StudioView(props: {
|
|
|
767
856
|
<div className={css.studio}>
|
|
768
857
|
<div className={css.formCol}>
|
|
769
858
|
<div className={css.modeRow}>
|
|
770
|
-
{(['tts', 'music', 'sfx', 'voice_design'] as
|
|
859
|
+
{([['tts', '🎙️'], ['music', '🎵'], ['sfx', '🔊'], ['voice_design', '🎨']] as Array<[AudioMode, string]>).map(([item, icon]) => (
|
|
771
860
|
<button
|
|
772
861
|
key={item}
|
|
773
862
|
type="button"
|
|
@@ -775,11 +864,13 @@ export function StudioView(props: {
|
|
|
775
864
|
data-active={mode === item ? 'true' : 'false'}
|
|
776
865
|
onClick={() => setMode(item)}
|
|
777
866
|
>
|
|
778
|
-
{
|
|
867
|
+
<span className={css.modeIcon}>{icon}</span>
|
|
868
|
+
{modeLabelOf(item)}
|
|
779
869
|
</button>
|
|
780
870
|
))}
|
|
781
871
|
</div>
|
|
782
872
|
|
|
873
|
+
<p className={css.formSection}>输入</p>
|
|
783
874
|
<label className={css.label}>
|
|
784
875
|
<span>{mode === 'voice_design' ? '音色描述' : mode === 'tts' ? '文本' : '提示词'}</span>
|
|
785
876
|
<textarea className={css.textarea} value={prompt} onChange={event => setPrompt(event.target.value)} placeholder={tt('prompt.placeholder')} />
|
|
@@ -806,6 +897,7 @@ export function StudioView(props: {
|
|
|
806
897
|
</>
|
|
807
898
|
) : null}
|
|
808
899
|
|
|
900
|
+
<p className={css.formSection}>模型</p>
|
|
809
901
|
{needModel ? (
|
|
810
902
|
<label className={css.checkbox} title="选择多个模型,用相同参数逐个生成,便于对比效果">
|
|
811
903
|
<input type="checkbox" checked={compareMode} onChange={event => setCompareMode(event.target.checked)} />
|
|
@@ -902,7 +994,14 @@ export function StudioView(props: {
|
|
|
902
994
|
)
|
|
903
995
|
) : null}
|
|
904
996
|
|
|
905
|
-
{globalSpecs.
|
|
997
|
+
{globalSpecs.some(spec => spec.advanced !== true) ? <p className={css.formSection}>生成参数</p> : null}
|
|
998
|
+
<div className={css.formFields}>
|
|
999
|
+
{globalSpecs.filter(spec => spec.advanced !== true).map(spec => (
|
|
1000
|
+
<div key={spec.key} className={spec.key === 'lyrics' || spec.key === 'toneText' ? css.fieldFull : css.fieldCell}>
|
|
1001
|
+
{renderField(spec)}
|
|
1002
|
+
</div>
|
|
1003
|
+
))}
|
|
1004
|
+
</div>
|
|
906
1005
|
|
|
907
1006
|
{mode === 'tts' && globalSpecs.some(spec => spec.advanced === true) ? (
|
|
908
1007
|
<details className={css.advanced}>
|
|
@@ -935,6 +1034,15 @@ export function StudioView(props: {
|
|
|
935
1034
|
<span className={css.resultEmptyIcon}>🎵</span>
|
|
936
1035
|
<p>{tt('result.empty')}</p>
|
|
937
1036
|
<p className={css.resultEmptyHint}>点击「开始生成」即创建一个任务,可同时进行多个;勾选「模型对比」用多个模型同参数生成对比</p>
|
|
1037
|
+
<button type="button" className={css.ghostButton} onClick={() => {
|
|
1038
|
+
const examples: Record<AudioMode, string> = {
|
|
1039
|
+
tts: '今天是不是很开心呀(laughs),当然了!我们一起去公园散步吧。',
|
|
1040
|
+
music: 'Cinematic orchestral piece with a clear "before/after" transition at 1:00, starting minimalist piano + strings, then full orchestra entrance with timpani and brass at the 1-minute mark.',
|
|
1041
|
+
sfx: '科技感 UI 提示音:清脆短促,带轻微回声与空气感。',
|
|
1042
|
+
voice_design: '讲述悬疑故事的播音员,声音低沉富有磁性,语速时快时慢,营造紧张神秘的氛围。',
|
|
1043
|
+
}
|
|
1044
|
+
setPrompt(examples[mode] ?? '')
|
|
1045
|
+
}}>填入示例 prompt</button>
|
|
938
1046
|
</div>
|
|
939
1047
|
) : (
|
|
940
1048
|
<div className={css.taskList}>
|
|
@@ -955,6 +1063,9 @@ export function StudioView(props: {
|
|
|
955
1063
|
<span className={css.resultModeChip}>{task.mode}</span>
|
|
956
1064
|
<span className={css.taskLabel} title={task.prompt}>{task.label}</span>
|
|
957
1065
|
<span className={css.taskStatus} data-state={task.status}>{statusText}</span>
|
|
1066
|
+
{task.status === 'running' ? (
|
|
1067
|
+
<span className={css.taskBar}><i style={{ width: `${task.progress.total > 0 ? Math.round((task.progress.done / task.progress.total) * 100) : 0}%` }} /></span>
|
|
1068
|
+
) : null}
|
|
958
1069
|
<span className={css.taskActions}>
|
|
959
1070
|
{task.status === 'running' ? (
|
|
960
1071
|
<button type="button" className={css.ghostButton} onClick={() => cancelTask(task.id)}>取消</button>
|
|
@@ -1023,6 +1134,7 @@ export function StudioView(props: {
|
|
|
1023
1134
|
<summary className={css.historyCompareSummary}>
|
|
1024
1135
|
<span className={css.historyPrompt}>{item.prompt}</span>
|
|
1025
1136
|
<span className={css.historyCompareBadge}>对比 · {item.models.length} 个模型</span>
|
|
1137
|
+
<span className={css.historyTime}>{formatClock(item.createdAt)}</span>
|
|
1026
1138
|
</summary>
|
|
1027
1139
|
<div className={css.historyMeta}>{modeLabelOf(item.mode)} · {item.models.map(model => model.model).join(' / ')}</div>
|
|
1028
1140
|
{item.models.map(model => (
|
|
@@ -1040,6 +1152,15 @@ export function StudioView(props: {
|
|
|
1040
1152
|
</div>
|
|
1041
1153
|
</div>
|
|
1042
1154
|
))}
|
|
1155
|
+
<div className={css.historyActions}>
|
|
1156
|
+
<button type="button" className={css.historyIcon} title="恢复(回填配置与全部模型)" onClick={() => restoreFromParams(
|
|
1157
|
+
(item.models[0]?.entry.params ?? {}) as Record<string, unknown>,
|
|
1158
|
+
item.mode,
|
|
1159
|
+
item.models[0]?.model ?? '',
|
|
1160
|
+
item.models.map(model => model.model),
|
|
1161
|
+
)}>↺</button>
|
|
1162
|
+
<button type="button" className={css.historyIcon} title="删除整个对比任务" onClick={() => void deleteHistoryEntries(item.models.map(model => model.entry.id))}>✕</button>
|
|
1163
|
+
</div>
|
|
1043
1164
|
</details>
|
|
1044
1165
|
)
|
|
1045
1166
|
}
|
|
@@ -1048,12 +1169,19 @@ export function StudioView(props: {
|
|
|
1048
1169
|
<div className={css.historyItem} key={item.key}>
|
|
1049
1170
|
<div className={css.historyPrompt}>{entry.prompt}</div>
|
|
1050
1171
|
<div className={css.historyMeta}>{modeLabelOf(entry.mode)} · {entry.model}{entry.channel ? ` · ${entry.channel}` : ''}</div>
|
|
1172
|
+
<div className={css.historyTime}>{formatClock(entry.createdAt)}</div>
|
|
1051
1173
|
{entry.audio.map((audio, index) => (
|
|
1052
1174
|
<AudioPlayer key={index} src={audio.url} compact itemKey={`${entry.id}-${index}`} />
|
|
1053
1175
|
))}
|
|
1054
1176
|
<div className={css.historyActions}>
|
|
1055
|
-
<button type="button" className={css.
|
|
1056
|
-
<
|
|
1177
|
+
<button type="button" className={css.historyIcon} title="恢复(回填配置与 prompt)" onClick={() => restoreFromParams(
|
|
1178
|
+
(entry.params ?? {}) as Record<string, unknown>,
|
|
1179
|
+
entry.mode,
|
|
1180
|
+
entry.model,
|
|
1181
|
+
)}>↺</button>
|
|
1182
|
+
<button type="button" className={css.historyIcon} title="删除这条记录" onClick={() => void deleteHistoryEntries([entry.id])}>✕</button>
|
|
1183
|
+
<button type="button" className={css.historyIcon} title="加入资源库" onClick={() => openSaveDialog(audioRefsOfEntry(entry), contextOfEntry(entry))}>
|
|
1184
|
+
<StarIcon />
|
|
1057
1185
|
</button>
|
|
1058
1186
|
</div>
|
|
1059
1187
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -41,7 +41,8 @@ export interface Config {
|
|
|
41
41
|
defaultChannelId?: string
|
|
42
42
|
defaultModel?: string
|
|
43
43
|
autoSaveToLibrary?: boolean
|
|
44
|
-
|
|
44
|
+
/** 设置卡按文本编辑,保存值可能是数字或数字字符串。 */
|
|
45
|
+
maxConcurrentGenerations?: number | string
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const DEFAULT_MAX_CONCURRENT = 5
|
|
@@ -64,7 +65,7 @@ export const Config: z<Config> = z.object({
|
|
|
64
65
|
defaultChannelId: z.string().default(''),
|
|
65
66
|
defaultModel: z.string().default(''),
|
|
66
67
|
autoSaveToLibrary: z.boolean().default(false),
|
|
67
|
-
maxConcurrentGenerations: z.number().default(DEFAULT_MAX_CONCURRENT),
|
|
68
|
+
maxConcurrentGenerations: z.union([z.number(), z.string()]).default(DEFAULT_MAX_CONCURRENT),
|
|
68
69
|
})
|
|
69
70
|
|
|
70
71
|
const DEFAULT_ENABLED = true
|
|
@@ -184,9 +185,11 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
184
185
|
defaultChannelId,
|
|
185
186
|
defaultModel: typeof value.defaultModel === 'string' ? value.defaultModel.trim() : '',
|
|
186
187
|
autoSaveToLibrary: value.autoSaveToLibrary === true,
|
|
187
|
-
maxConcurrentGenerations:
|
|
188
|
-
|
|
189
|
-
:
|
|
188
|
+
maxConcurrentGenerations: (() => {
|
|
189
|
+
const rawMax = value.maxConcurrentGenerations
|
|
190
|
+
const parsedMax = typeof rawMax === 'number' ? rawMax : typeof rawMax === 'string' && rawMax.trim() !== '' ? Number(rawMax.trim()) : NaN
|
|
191
|
+
return Number.isFinite(parsedMax) ? Math.max(1, Math.min(20, Math.floor(parsedMax))) : DEFAULT_MAX_CONCURRENT
|
|
192
|
+
})(),
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
|
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.4'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
|
|
14
14
|
export const SETTINGS_API = {
|