dsh-audiogen 0.4.7 → 0.4.9

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/index.js CHANGED
@@ -1688,6 +1688,7 @@ function parseGenerateRequest(body) {
1688
1688
  ...num(body.cfgScale) !== void 0 ? { cfgScale: num(body.cfgScale) } : {},
1689
1689
  ...typeof body.format === "string" && body.format.trim() !== "" ? { format: body.format.trim() } : {},
1690
1690
  ...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
1691
+ ...typeof body.taskId === "string" && body.taskId.trim() !== "" ? { taskId: body.taskId.trim() } : {},
1691
1692
  ...str(body.emotion) !== void 0 ? { emotion: str(body.emotion) } : {},
1692
1693
  ...num(body.vol) !== void 0 ? { vol: num(body.vol) } : {},
1693
1694
  ...num(body.pitch) !== void 0 ? { pitch: num(body.pitch) } : {},
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.7",
4
+ "version": "0.4.9",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -14,7 +14,7 @@ import { globalFieldSpecs, overrideRowSpecs, presetLabel, type FieldSpec } from
14
14
  import { tt } from './helpers.ts'
15
15
  import {
16
16
  HISTORY_API,
17
- type AudioMode, type GeneratedAudio, type GenerateAudioRequest, type HistoryEntry, type LibraryEntry,
17
+ type AudioMode, type GeneratedAudio, type GenerateAudioRequest, type HistoryAudioRef, type HistoryEntry, type LibraryEntry,
18
18
  } from '../protocol.ts'
19
19
  import type { ModelOption } from './settings-scope.ts'
20
20
  import { AudioPlayer } from './audio-player.tsx'
@@ -161,6 +161,11 @@ function dataUrlOf(audio: GeneratedAudio): string {
161
161
  return `data:${audio.mime};base64,${audio.b64}`
162
162
  }
163
163
 
164
+ /** 结果卡片音频源:优先同源 URL(历史恢复的音频只有 URL 没有 b64),无 URL 再回退 data URL。 */
165
+ function srcOf(audio: GeneratedAudio): string {
166
+ return audio.url !== '' ? audio.url : dataUrlOf(audio)
167
+ }
168
+
164
169
  function fileNameOf(url: string): string {
165
170
  try {
166
171
  return decodeURIComponent(new URL(url, 'http://localhost').pathname.split('/').pop() ?? '')
@@ -533,8 +538,10 @@ export function StudioView(props: {
533
538
  setTasks(current => current.filter(task => task.id !== taskId))
534
539
  }
535
540
 
536
- /** 从历史参数回填表单(参考 AI 生图「恢复」):配置 + prompt 一键复用。 */
537
- const restoreFromParams = (params: Record<string, unknown>, modeValue: AudioMode, singleModel: string, compareModelsRestore?: string[]): void => {
541
+ /** 从历史参数回填表单(参考 AI 生图「恢复」):配置 + prompt 一键复用。
542
+ * compareModelsRestore:对比任务恢复为对比模式;overridesRestore 由各模型 params 差异重建;
543
+ * restoredGroups:把历史音频以「已完成任务」形式放回结果列,可直接试听/下载而无需重新生成。 */
544
+ const restoreFromParams = (params: Record<string, unknown>, modeValue: AudioMode, singleModel: string, compareModelsRestore?: string[], overridesRestore?: Record<string, Record<string, string>>, restoredGroups?: Array<{ model: string; audio: GeneratedAudio[] }>): void => {
538
545
  const str = (key: string): string | undefined => {
539
546
  const v = params[key]
540
547
  return typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined
@@ -606,7 +613,57 @@ export function StudioView(props: {
606
613
  if (previewValue !== undefined) setPreviewText(previewValue)
607
614
  const channelIdValue = str('channelId')
608
615
  if (channelIdValue !== undefined && modeValue === 'voice_design') setDesignChannelId(channelIdValue)
609
- props.showToast('已恢复该次生成的配置,可直接再次生成')
616
+ // 恢复对比任务时重建每模型参数覆盖(各模型 params 与第一项的差异);单条恢复清空覆盖。
617
+ setOverrides(overridesRestore ?? {})
618
+ // 把历史音频以「已完成任务」放回结果列(不重新生成,直接用同源 URL 试听/下载)。
619
+ if (restoredGroups !== undefined && restoredGroups.length > 0) {
620
+ const now = Date.now()
621
+ const total = restoredGroups.reduce((sum, group) => sum + group.audio.length, 0)
622
+ const restoredTask: StudioTask = {
623
+ id: `t-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
624
+ mode: modeValue,
625
+ kind: restoredGroups.length > 1 ? 'compare' : 'single',
626
+ prompt: promptValue ?? '',
627
+ label: restoredGroups.length > 1 ? `对比 ${restoredGroups.map(group => group.model).join(' / ')}` : restoredGroups[0]!.model,
628
+ status: 'done',
629
+ progress: { done: total, total, current: '' },
630
+ startedAt: now,
631
+ finishedAt: now,
632
+ groups: restoredGroups.map(group => ({ model: group.model, state: 'done' as const, outputs: group.audio })),
633
+ }
634
+ setTasks(current => [restoredTask, ...current])
635
+ }
636
+ props.showToast(restoredGroups !== undefined && restoredGroups.length > 0 ? '已恢复配置与音频(历史结果已放回中间栏)' : '已恢复该次生成的配置,可直接再次生成')
637
+ }
638
+
639
+ /** 历史音频引用 → 结果列卡片可用的音频对象(复用同源 URL,不重新生成)。 */
640
+ const historyAudioToGenerated = (entryId: string, refs: HistoryAudioRef[]): GeneratedAudio[] =>
641
+ refs.map((ref, index) => ({
642
+ id: `${entryId}-${index}`,
643
+ file: '',
644
+ b64: '',
645
+ mime: ref.mime,
646
+ bytes: 0,
647
+ url: ref.url,
648
+ ...(ref.voiceId === undefined ? {} : { voiceId: ref.voiceId }),
649
+ }))
650
+
651
+ /** 由对比历史条目重建每模型参数覆盖:以第一项为全局基准,其余条目与基准不同的字段即覆盖。 */
652
+ const overridesOfCompare = (models: Array<{ model: string; entry: HistoryEntry }>): Record<string, Record<string, string>> => {
653
+ const base = (models[0]?.entry.params ?? {}) as Record<string, unknown>
654
+ const out: Record<string, Record<string, string>> = {}
655
+ for (const item of models.slice(1)) {
656
+ const params = (item.entry.params ?? {}) as Record<string, unknown>
657
+ const diff: Record<string, string> = {}
658
+ for (const [key, value] of Object.entries(params)) {
659
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') continue
660
+ if (key === 'taskId' || key === 'upstream') continue
661
+ if (typeof base[key] === typeof value && String(base[key]) === String(value)) continue
662
+ diff[key] = String(value)
663
+ }
664
+ if (Object.keys(diff).length > 0) out[item.model] = diff
665
+ }
666
+ return out
610
667
  }
611
668
 
612
669
  /** 调用宿主增强(Agent 默认模型),结果先预览再应用。 */
@@ -812,9 +869,9 @@ export function StudioView(props: {
812
869
  ) : null}
813
870
  <span className={css.audioCardIndex}>#{index + 1}</span>
814
871
  </div>
815
- <AudioPlayer src={dataUrlOf(audio)} itemKey={`${label}-${audio.id}`} />
872
+ <AudioPlayer src={srcOf(audio)} itemKey={`${label}-${audio.id}`} />
816
873
  <div className={css.audioCardActions}>
817
- <a className={css.ghostButton} href={dataUrlOf(audio)} download={`generated-${index + 1}.${audio.mime.split('/')[1]?.replace('mpeg', 'mp3') ?? 'mp3'}`}>
874
+ <a className={css.ghostButton} href={srcOf(audio)} download={`generated-${index + 1}.${audio.mime.split('/')[1]?.replace('mpeg', 'mp3') ?? 'mp3'}`}>
818
875
  <DownloadIcon /> 下载
819
876
  </a>
820
877
  {saved ? (
@@ -1235,6 +1292,8 @@ export function StudioView(props: {
1235
1292
  item.mode,
1236
1293
  item.models[0]?.model ?? '',
1237
1294
  item.models.map(model => model.model),
1295
+ overridesOfCompare(item.models),
1296
+ item.models.map(model => ({ model: model.model, audio: historyAudioToGenerated(model.entry.id, model.entry.audio) })),
1238
1297
  )}>↺</button>
1239
1298
  <button type="button" className={css.historyIcon} title="删除整个对比任务" onClick={() => void deleteHistoryEntries(item.models.map(model => model.entry.id))}>✕</button>
1240
1299
  </div>
@@ -1255,6 +1314,9 @@ export function StudioView(props: {
1255
1314
  (entry.params ?? {}) as Record<string, unknown>,
1256
1315
  entry.mode,
1257
1316
  entry.model,
1317
+ undefined,
1318
+ undefined,
1319
+ [{ model: entry.model, audio: historyAudioToGenerated(entry.id, entry.audio) }],
1258
1320
  )}>↺</button>
1259
1321
  <button type="button" className={css.historyIcon} title="删除这条记录" onClick={() => void deleteHistoryEntries([entry.id])}>✕</button>
1260
1322
  <button type="button" className={css.historyIcon} title="加入资源库" onClick={() => openSaveDialog(audioRefsOfEntry(entry), contextOfEntry(entry))}>
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.7'
11
+ export const PLUGIN_VERSION = '0.4.9'
12
12
 
13
13
  /** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
14
14
  export const SETTINGS_API = {
package/src/routes.ts CHANGED
@@ -144,6 +144,8 @@ function parseGenerateRequest(body: Record<string, unknown>): GenerateAudioReque
144
144
  ...(num(body.cfgScale) !== undefined ? { cfgScale: num(body.cfgScale)! } : {}),
145
145
  ...(typeof body.format === 'string' && body.format.trim() !== '' ? { format: body.format.trim() } : {}),
146
146
  ...(typeof body.channelId === 'string' && body.channelId !== '' ? { channelId: body.channelId } : {}),
147
+ // 对比任务的任务 id:随 params 一起入历史,供客户端按 taskId 聚合出对比卡片。
148
+ ...(typeof body.taskId === 'string' && body.taskId.trim() !== '' ? { taskId: body.taskId.trim() } : {}),
147
149
  // ---- MiniMax TTS 专属字段(其他厂商忽略) ----
148
150
  ...(str(body.emotion) !== undefined ? { emotion: str(body.emotion)! } : {}),
149
151
  ...(num(body.vol) !== undefined ? { vol: num(body.vol)! } : {}),