dsh-audiogen 0.3.4 → 0.4.0
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/README.md +7 -1
- package/lib/client.js +2368 -630
- package/lib/client.js.map +1 -1
- package/lib/index.js +1022 -68
- package/package.json +1 -1
- package/skills/design/SKILL.md +7 -1
- package/skills/music/SKILL.md +37 -0
- package/skills/sfx/SKILL.md +17 -3
- package/src/agent-audio-tools.ts +159 -14
- package/src/audio-engine.ts +284 -18
- package/src/audio-presets.ts +10 -4
- package/src/audio-store.ts +251 -3
- package/src/client/AudioGenPanel.tsx +59 -334
- package/src/client/SettingsCard.tsx +9 -0
- package/src/client/api.ts +45 -2
- package/src/client/audio-panel.module.css +652 -79
- package/src/client/audio-player.tsx +87 -0
- package/src/client/icons.tsx +170 -0
- package/src/client/library-save-dialog.tsx +173 -0
- package/src/client/library-view.tsx +511 -0
- package/src/client/library.module.css +686 -0
- package/src/client/locales.ts +6 -0
- package/src/client/settings-scope.ts +2 -0
- package/src/client/studio-view.tsx +560 -0
- package/src/index.ts +6 -0
- package/src/protocol.ts +130 -1
- package/src/routes.ts +247 -8
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.
|
|
11
|
+
export const PLUGIN_VERSION = '0.4.0'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
|
|
14
14
|
export const SETTINGS_API = {
|
|
@@ -41,12 +41,30 @@ export const HISTORY_API = {
|
|
|
41
41
|
audio: '/api/dsh-audiogen/history/audio',
|
|
42
42
|
} as const
|
|
43
43
|
|
|
44
|
+
/** Host-persisted resource-library routes. */
|
|
45
|
+
export const LIBRARY_API = {
|
|
46
|
+
list: '/api/dsh-audiogen/library/list',
|
|
47
|
+
save: '/api/dsh-audiogen/library/save',
|
|
48
|
+
update: '/api/dsh-audiogen/library/update',
|
|
49
|
+
remove: '/api/dsh-audiogen/library/remove',
|
|
50
|
+
audio: '/api/dsh-audiogen/library/audio',
|
|
51
|
+
} as const
|
|
52
|
+
|
|
44
53
|
/** Maximum number of history entries retained host-side (oldest evicted). */
|
|
45
54
|
export const HISTORY_MAX = 50
|
|
46
55
|
|
|
47
56
|
/** Audio generation modes. */
|
|
48
57
|
export type AudioMode = 'tts' | 'music' | 'sfx' | 'voice_design'
|
|
49
58
|
|
|
59
|
+
/** Resource-library entry kinds (map to directories under library/). */
|
|
60
|
+
export type LibraryType = 'voice' | 'music' | 'sfx' | 'tts'
|
|
61
|
+
|
|
62
|
+
/** Voice resources: gender bucket (male / female / custom). */
|
|
63
|
+
export type VoiceCategory = 'male' | 'female' | 'custom'
|
|
64
|
+
|
|
65
|
+
/** All library types, for iteration and validation. */
|
|
66
|
+
export const LIBRARY_TYPES: readonly LibraryType[] = ['voice', 'music', 'sfx', 'tts'] as const
|
|
67
|
+
|
|
50
68
|
/** The capability category of an audio model/voice. */
|
|
51
69
|
export type AudioModelCategory =
|
|
52
70
|
| 'tts'
|
|
@@ -118,6 +136,16 @@ export interface GenerateAudioRequest {
|
|
|
118
136
|
lyrics?: string
|
|
119
137
|
/** MiniMax 是否生成纯音乐(无歌词/人声);true 时 lyrics 可为空。 */
|
|
120
138
|
isInstrumental?: boolean
|
|
139
|
+
/** ElevenLabs 音效:生成无缝循环音效(loop,仅 eleven_text_to_sound_v2)。 */
|
|
140
|
+
loop?: boolean
|
|
141
|
+
/** ElevenLabs 音效:提示词影响度 0-1(prompt_influence,默认 0.3)。 */
|
|
142
|
+
promptInfluence?: number
|
|
143
|
+
/** Stable Audio 随机种子(seed,0-4294967294,默认 0=随机)。 */
|
|
144
|
+
seed?: number
|
|
145
|
+
/** Stable Audio 采样步数(steps,按模型收敛:stable-audio-2: 30-100;2.5/3: 4-8)。 */
|
|
146
|
+
steps?: number
|
|
147
|
+
/** Stable Audio 提示词遵循度(cfg_scale 1-25;stable-audio-2 默认 7,2.5/3 默认 1)。 */
|
|
148
|
+
cfgScale?: number
|
|
121
149
|
/** Output format, e.g. mp3, wav, pcm. */
|
|
122
150
|
format?: string
|
|
123
151
|
/** Channel this request targets (host falls back to default). */
|
|
@@ -158,6 +186,8 @@ export interface GenerateAudioRequest {
|
|
|
158
186
|
voiceModify?: { pitch?: number; intensity?: number; timbre?: number; soundEffects?: string }
|
|
159
187
|
/** MiniMax 双音色混合权重(timbre_weights)。 */
|
|
160
188
|
timbreWeights?: Array<{ voiceId: string; weight: number }>
|
|
189
|
+
/** 生成完成后自动保存到资源库(面板勾选;宿主还会并入 autoSaveToLibrary 设置)。 */
|
|
190
|
+
saveToLibrary?: boolean
|
|
161
191
|
}
|
|
162
192
|
|
|
163
193
|
/** One generated audio, normalized host-side to base64. */
|
|
@@ -174,6 +204,8 @@ export interface GeneratedAudio {
|
|
|
174
204
|
url: string
|
|
175
205
|
/** Stable audio id / file name. */
|
|
176
206
|
id: string
|
|
207
|
+
/** File name inside the audio/ store dir. */
|
|
208
|
+
file: string
|
|
177
209
|
/** Optional voice id returned by a voice-design API. */
|
|
178
210
|
voiceId?: string
|
|
179
211
|
}
|
|
@@ -214,6 +246,8 @@ export interface HistoryEntry {
|
|
|
214
246
|
audio: HistoryAudioRef[]
|
|
215
247
|
channelId?: string
|
|
216
248
|
channel?: string
|
|
249
|
+
/** Full resolved generation-request snapshot (provenance; no secrets). */
|
|
250
|
+
params?: Record<string, unknown>
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
/** A history entry the client submits for persistence (audio still carries base64). */
|
|
@@ -231,8 +265,101 @@ export interface HistoryEntryInput {
|
|
|
231
265
|
audio: GeneratedAudio[]
|
|
232
266
|
channelId?: string
|
|
233
267
|
channel?: string
|
|
268
|
+
/** Full resolved generation-request snapshot (provenance; no secrets). */
|
|
269
|
+
params?: Record<string, unknown>
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --------------------------------------------------------------- resource library
|
|
273
|
+
|
|
274
|
+
/** File name / id pair the client hands the host so it can copy a stored audio file. */
|
|
275
|
+
export interface LibraryAudioInput {
|
|
276
|
+
/** Audio id (UUID) as stored in the audio/ dir. */
|
|
277
|
+
id: string
|
|
278
|
+
/** File name inside the audio/ dir (e.g. <uuid>.mp3). */
|
|
279
|
+
file: string
|
|
280
|
+
/** MIME type. */
|
|
281
|
+
mime: string
|
|
282
|
+
/** Optional generated voice id (voice_design outputs). */
|
|
283
|
+
voiceId?: string
|
|
284
|
+
/** Optional duration in seconds. */
|
|
285
|
+
duration?: number
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Full provenance of one library resource. */
|
|
289
|
+
export interface LibraryProvenance {
|
|
290
|
+
mode: AudioMode
|
|
291
|
+
prompt: string
|
|
292
|
+
/** Channel display name snapshot. */
|
|
293
|
+
channel?: string
|
|
294
|
+
channelId?: string
|
|
295
|
+
/** Provider API base URL (non-secret). */
|
|
296
|
+
apiUrl?: string
|
|
297
|
+
/** Model/voice alias as configured. */
|
|
298
|
+
model?: string
|
|
299
|
+
/** Upstream model/voice id actually sent to the provider. */
|
|
300
|
+
upstream?: string
|
|
301
|
+
/** Voice alias used for TTS. */
|
|
302
|
+
voice?: string
|
|
303
|
+
/** voiceId returned by the provider (voice_design). */
|
|
304
|
+
voiceId?: string
|
|
305
|
+
/** Full resolved generation-request snapshot (no secrets). */
|
|
306
|
+
params?: Record<string, unknown>
|
|
234
307
|
}
|
|
235
308
|
|
|
309
|
+
/** One audio file inside a library entry. */
|
|
310
|
+
export interface LibraryFileRef {
|
|
311
|
+
/** Same-origin URL (LIBRARY_API.audio/<rel>). */
|
|
312
|
+
url: string
|
|
313
|
+
/** Relative path under library/ (e.g. voice/male/<id>.mp3). */
|
|
314
|
+
rel: string
|
|
315
|
+
/** MIME type. */
|
|
316
|
+
mime: string
|
|
317
|
+
/** Exact encoded byte length. */
|
|
318
|
+
bytes: number
|
|
319
|
+
/** Duration in seconds when known. */
|
|
320
|
+
duration?: number
|
|
321
|
+
/** Optional generated voice id (voice_design outputs). */
|
|
322
|
+
voiceId?: string
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** One curated resource in the library. */
|
|
326
|
+
export interface LibraryEntry {
|
|
327
|
+
id: string
|
|
328
|
+
createdAt: number
|
|
329
|
+
type: LibraryType
|
|
330
|
+
/** voice: male/female/custom; tts: the speaking voice key; others: ''. */
|
|
331
|
+
category?: string
|
|
332
|
+
name: string
|
|
333
|
+
tags: string[]
|
|
334
|
+
note?: string
|
|
335
|
+
files: LibraryFileRef[]
|
|
336
|
+
provenance: LibraryProvenance
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Client → host save request (audioFiles reference files in the audio/ dir). */
|
|
340
|
+
export interface LibrarySaveRequest {
|
|
341
|
+
audioFiles: LibraryAudioInput[]
|
|
342
|
+
type: LibraryType
|
|
343
|
+
category?: string
|
|
344
|
+
name?: string
|
|
345
|
+
tags?: string[]
|
|
346
|
+
note?: string
|
|
347
|
+
provenance: LibraryProvenance
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Client → host update request (moving type/category relocates files). */
|
|
351
|
+
export interface LibraryUpdateRequest {
|
|
352
|
+
id: string
|
|
353
|
+
name?: string
|
|
354
|
+
tags?: string[]
|
|
355
|
+
note?: string
|
|
356
|
+
category?: string
|
|
357
|
+
type?: LibraryType
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Default name length when no name was given. */
|
|
361
|
+
export const LIBRARY_NAME_MAX = 40
|
|
362
|
+
|
|
236
363
|
/** The plugin settings fields edited by the settings card and panel. */
|
|
237
364
|
export interface AudiogenConfig {
|
|
238
365
|
enabled?: boolean
|
|
@@ -243,4 +370,6 @@ export interface AudiogenConfig {
|
|
|
243
370
|
defaultChannelId?: string
|
|
244
371
|
/** Optional default voice/model alias for quick generation. */
|
|
245
372
|
defaultModel?: string
|
|
373
|
+
/** 生成完成后自动加入资源库(面板与 Agent 生成均生效;单次可取消勾选)。 */
|
|
374
|
+
autoSaveToLibrary?: boolean
|
|
246
375
|
}
|
package/src/routes.ts
CHANGED
|
@@ -13,10 +13,11 @@ import { SettingsConflictError, settingsNamespace, type SettingsDescriptor } fro
|
|
|
13
13
|
import { generateAudio, AudioGenError, type AudioChannel } from './audio-engine.ts'
|
|
14
14
|
import { discoverAudioModels } from './audio-models.ts'
|
|
15
15
|
import { AUDIO_PRESETS } from './audio-presets.ts'
|
|
16
|
-
import { appendHistory, clearHistory, listHistory, readAudioFile, removeHistory, saveAudioFile } from './audio-store.ts'
|
|
16
|
+
import { appendHistory, clearHistory, listHistory, readAudioFile, removeHistory, saveAudioFile, listLibrary, saveToLibrary, updateLibraryEntry, removeLibraryEntries, readLibraryFile } from './audio-store.ts'
|
|
17
17
|
import {
|
|
18
|
-
AUDIO_API, AUDIOGEN_SETTINGS_NAMESPACE, GENERATE_API, HISTORY_API, MODEL_API, PRESETS_API, SETTINGS_API,
|
|
19
|
-
|
|
18
|
+
AUDIO_API, AUDIOGEN_SETTINGS_NAMESPACE, GENERATE_API, HISTORY_API, LIBRARY_API, MODEL_API, PRESETS_API, SETTINGS_API,
|
|
19
|
+
LIBRARY_TYPES,
|
|
20
|
+
type GenerateAudioRequest, type GeneratedAudio, type HistoryEntryInput, type LibraryAudioInput, type LibraryProvenance, type LibraryType,
|
|
20
21
|
} from './protocol.ts'
|
|
21
22
|
|
|
22
23
|
const MAX_JSON_BODY_BYTES = 16 * 1024 * 1024
|
|
@@ -38,6 +39,8 @@ export interface ChannelsView {
|
|
|
38
39
|
export interface AudiogenRoutesDeps {
|
|
39
40
|
settings: SettingsSeam
|
|
40
41
|
resolveChannels: () => ChannelsView
|
|
42
|
+
/** Whether the auto-save-to-library setting is on. */
|
|
43
|
+
autoSave: () => boolean
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
@@ -124,6 +127,11 @@ function parseGenerateRequest(body: Record<string, unknown>): GenerateAudioReque
|
|
|
124
127
|
...(num(body.duration) !== undefined ? { duration: num(body.duration)! } : {}),
|
|
125
128
|
...(typeof body.lyrics === 'string' && body.lyrics.trim() !== '' ? { lyrics: body.lyrics.trim() } : {}),
|
|
126
129
|
...(typeof body.isInstrumental === 'boolean' ? { isInstrumental: body.isInstrumental } : {}),
|
|
130
|
+
...(typeof body.loop === 'boolean' ? { loop: body.loop } : {}),
|
|
131
|
+
...(num(body.promptInfluence) !== undefined ? { promptInfluence: num(body.promptInfluence)! } : {}),
|
|
132
|
+
...(num(body.seed) !== undefined ? { seed: num(body.seed)! } : {}),
|
|
133
|
+
...(num(body.steps) !== undefined ? { steps: num(body.steps)! } : {}),
|
|
134
|
+
...(num(body.cfgScale) !== undefined ? { cfgScale: num(body.cfgScale)! } : {}),
|
|
127
135
|
...(typeof body.format === 'string' && body.format.trim() !== '' ? { format: body.format.trim() } : {}),
|
|
128
136
|
...(typeof body.channelId === 'string' && body.channelId !== '' ? { channelId: body.channelId } : {}),
|
|
129
137
|
// ---- MiniMax TTS 专属字段(其他厂商忽略) ----
|
|
@@ -142,6 +150,7 @@ function parseGenerateRequest(body: Record<string, unknown>): GenerateAudioReque
|
|
|
142
150
|
...(str(body.languageBoost) !== undefined ? { languageBoost: str(body.languageBoost)! } : {}),
|
|
143
151
|
...(voiceModify !== undefined ? { voiceModify } : {}),
|
|
144
152
|
...(timbreWeights !== undefined && timbreWeights.length > 0 ? { timbreWeights } : {}),
|
|
153
|
+
...(flag(body.saveToLibrary) !== undefined ? { saveToLibrary: flag(body.saveToLibrary)! } : {}),
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
|
|
@@ -180,11 +189,15 @@ function resolveChannelRequest(
|
|
|
180
189
|
const defaults = view.channels.find(candidate => candidate.id === view.defaultChannelId) ?? view.channels[0]
|
|
181
190
|
const target = explicit ?? defaults
|
|
182
191
|
const asked = request.model.trim()
|
|
192
|
+
|
|
193
|
+
// 音色设计不按模型解析渠道:总是走 channelId(面板选择器)或默认渠道,
|
|
194
|
+
// 并丢弃可能残留的模型值,避免上一个模式的模型把渠道带偏。
|
|
195
|
+
if (request.mode === 'voice_design') {
|
|
196
|
+
if (target === undefined) return { ok: false, code: 'no-channels', message: '尚未配置任何渠道' }
|
|
197
|
+
return { ok: true, request: { ...request, model: '', upstream: undefined, channelId: target.id, channel: target.name } }
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
if (asked === '') {
|
|
184
|
-
if (request.mode === 'voice_design') {
|
|
185
|
-
if (target === undefined) return { ok: false, code: 'no-channels', message: '尚未配置任何渠道' }
|
|
186
|
-
return { ok: true, request: { ...request, channelId: target.id, channel: target.name } }
|
|
187
|
-
}
|
|
188
201
|
const alias = target?.models[0]?.alias ?? ''
|
|
189
202
|
if (alias === '') {
|
|
190
203
|
return { ok: false, code: 'no-models', message: `渠道「${target?.name ?? ''}」尚未配置模型/音色,请先在设置中添加` }
|
|
@@ -202,6 +215,74 @@ function resolveChannelRequest(
|
|
|
202
215
|
return { ok: true, request: { ...request, model: asked, upstream: mapping.id, channelId: picked.id, channel: picked.name } }
|
|
203
216
|
}
|
|
204
217
|
|
|
218
|
+
/** Build the library type from a generation mode (voice_design → voice). */
|
|
219
|
+
function libraryTypeOf(mode: GenerateAudioRequest['mode']): LibraryType {
|
|
220
|
+
if (mode === 'voice_design') return 'voice'
|
|
221
|
+
return mode
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Provenance snapshot straight from a resolved generate request. */
|
|
225
|
+
function provenanceOf(request: GenerateAudioRequest, apiUrl: string): LibraryProvenance {
|
|
226
|
+
return {
|
|
227
|
+
mode: request.mode,
|
|
228
|
+
prompt: request.prompt,
|
|
229
|
+
...(request.channel === undefined ? {} : { channel: request.channel }),
|
|
230
|
+
...(request.channelId === undefined ? {} : { channelId: request.channelId }),
|
|
231
|
+
...(apiUrl === '' ? {} : { apiUrl }),
|
|
232
|
+
...(request.model === undefined || request.model === '' ? {} : { model: request.model }),
|
|
233
|
+
...(request.upstream === undefined || request.upstream === '' ? {} : { upstream: request.upstream }),
|
|
234
|
+
...(request.voice === undefined ? {} : { voice: request.voice }),
|
|
235
|
+
params: { ...request },
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const strOf = (value: unknown): string | undefined => typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
|
|
240
|
+
const strListOf = (value: unknown): string[] | undefined =>
|
|
241
|
+
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string' && item.trim() !== '').map(item => item.trim()) : undefined
|
|
242
|
+
const parseModeOf = (value: unknown): GenerateAudioRequest['mode'] =>
|
|
243
|
+
value === 'music' ? 'music' : value === 'sfx' ? 'sfx' : value === 'voice_design' ? 'voice_design' : 'tts'
|
|
244
|
+
const parseLibraryTypeOf = (value: unknown): LibraryType | undefined =>
|
|
245
|
+
(LIBRARY_TYPES as readonly unknown[]).includes(value) ? value as LibraryType : undefined
|
|
246
|
+
|
|
247
|
+
/** File name (audio/ id.ext) from a same-origin audio url. */
|
|
248
|
+
function historyFileIdOf(url: string): string {
|
|
249
|
+
try {
|
|
250
|
+
return decodeURIComponent(new URL(url, 'http://localhost').pathname.split('/').pop() ?? '')
|
|
251
|
+
} catch {
|
|
252
|
+
return ''
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Fill missing provenance fields from host-persisted history (which carries
|
|
258
|
+
* the resolved request snapshot) and the channel catalog. Client-supplied
|
|
259
|
+
* values win when present.
|
|
260
|
+
*/
|
|
261
|
+
async function mergeLibraryProvenance(
|
|
262
|
+
given: LibraryProvenance,
|
|
263
|
+
files: LibraryAudioInput[],
|
|
264
|
+
channels: AudioChannel[],
|
|
265
|
+
): Promise<LibraryProvenance> {
|
|
266
|
+
const wanted = new Set(files.map(file => file.file))
|
|
267
|
+
const history = await listHistory()
|
|
268
|
+
const entry = history.find(candidate => candidate.audio.some(audio => wanted.has(historyFileIdOf(audio.url))))
|
|
269
|
+
const params = entry?.params !== undefined && typeof entry.params === 'object' ? entry.params : undefined
|
|
270
|
+
const channel = channels.find(candidate => candidate.id === (entry?.channelId ?? ''))
|
|
271
|
+
return {
|
|
272
|
+
mode: entry?.mode ?? given.mode,
|
|
273
|
+
prompt: given.prompt !== '' ? given.prompt : (entry?.prompt ?? ''),
|
|
274
|
+
...(given.channel !== undefined || entry?.channel !== undefined ? { channel: given.channel ?? entry?.channel } : {}),
|
|
275
|
+
...(given.channelId !== undefined || entry?.channelId !== undefined ? { channelId: given.channelId ?? entry?.channelId } : {}),
|
|
276
|
+
...((given.apiUrl ?? channel?.apiUrl ?? '') === '' ? {} : { apiUrl: given.apiUrl ?? channel?.apiUrl }),
|
|
277
|
+
...(given.model !== undefined || entry?.model !== undefined ? { model: given.model ?? entry?.model } : {}),
|
|
278
|
+
...((given.upstream ?? (typeof params?.upstream === 'string' ? params.upstream : undefined)) !== undefined
|
|
279
|
+
? { upstream: given.upstream ?? (typeof params?.upstream === 'string' ? params.upstream : undefined) } : {}),
|
|
280
|
+
...(given.voice !== undefined || entry?.voice !== undefined ? { voice: given.voice ?? entry?.voice } : {}),
|
|
281
|
+
...(given.voiceId !== undefined || entry?.voiceId !== undefined ? { voiceId: given.voiceId ?? entry?.voiceId } : {}),
|
|
282
|
+
...(given.params !== undefined || params !== undefined ? { params: given.params ?? params } : {}),
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
205
286
|
/** Build every /api/dsh-audiogen route. */
|
|
206
287
|
export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
207
288
|
const guard = (req: IncomingMessage, res: ServerResponse, method: string): boolean => {
|
|
@@ -342,6 +423,7 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
|
342
423
|
const saved = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`)
|
|
343
424
|
generated.push({
|
|
344
425
|
id: saved.id,
|
|
426
|
+
file: saved.file,
|
|
345
427
|
b64: Buffer.from(output.data).toString('base64'),
|
|
346
428
|
mime: saved.mime,
|
|
347
429
|
bytes: saved.bytes,
|
|
@@ -349,6 +431,7 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
|
349
431
|
...(output.voiceId === undefined ? {} : { voiceId: output.voiceId }),
|
|
350
432
|
})
|
|
351
433
|
}
|
|
434
|
+
const paramsSnapshot: Record<string, unknown> = { ...request }
|
|
352
435
|
let history
|
|
353
436
|
try {
|
|
354
437
|
history = await appendHistory({
|
|
@@ -364,12 +447,33 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
|
364
447
|
audio: generated,
|
|
365
448
|
...(request.channelId === undefined ? {} : { channelId: request.channelId }),
|
|
366
449
|
...(request.channel === undefined ? {} : { channel: request.channel }),
|
|
450
|
+
params: paramsSnapshot,
|
|
367
451
|
})
|
|
368
452
|
} catch (error) {
|
|
369
453
|
writeJson(res, 200, { ok: true, outputs: generated, historyError: messageOf(error) })
|
|
370
454
|
return
|
|
371
455
|
}
|
|
372
|
-
|
|
456
|
+
// ---- 资源库:单次勾选或设置自动入库(saveToLibrary === false 显式跳过) ----
|
|
457
|
+
const wantSave = request.saveToLibrary === true || (deps.autoSave() && request.saveToLibrary !== false)
|
|
458
|
+
let resources: Array<{ id: string; name: string; type: LibraryType }> | undefined
|
|
459
|
+
if (wantSave) {
|
|
460
|
+
try {
|
|
461
|
+
const entry = await saveToLibrary({
|
|
462
|
+
audioFiles: generated.map(audio => ({
|
|
463
|
+
id: audio.id,
|
|
464
|
+
file: audio.file,
|
|
465
|
+
mime: audio.mime,
|
|
466
|
+
...(audio.voiceId === undefined ? {} : { voiceId: audio.voiceId }),
|
|
467
|
+
})),
|
|
468
|
+
type: libraryTypeOf(request.mode),
|
|
469
|
+
provenance: provenanceOf(request, channel.apiUrl),
|
|
470
|
+
})
|
|
471
|
+
resources = [{ id: entry.id, name: entry.name, type: entry.type }]
|
|
472
|
+
} catch {
|
|
473
|
+
// library-save is best-effort: generation and history already succeeded
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
writeJson(res, 200, { ok: true, outputs: generated, history, ...(resources === undefined ? {} : { resources }) })
|
|
373
477
|
} catch (error) {
|
|
374
478
|
const code = error instanceof AudioGenError ? error.code : 'generate-failed'
|
|
375
479
|
writeJson(res, 200, { ok: false, code, message: messageOf(error) })
|
|
@@ -407,6 +511,141 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
|
407
511
|
res.end(stored.data)
|
|
408
512
|
},
|
|
409
513
|
},
|
|
514
|
+
// ------------------------------------------------------- resource library
|
|
515
|
+
{
|
|
516
|
+
kind: 'exact', path: LIBRARY_API.list,
|
|
517
|
+
handler: async (req, res) => {
|
|
518
|
+
if (!guard(req, res, 'POST')) return
|
|
519
|
+
writeJson(res, 200, { ok: true, entries: await listLibrary() })
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
kind: 'exact', path: LIBRARY_API.save,
|
|
524
|
+
handler: async (req, res) => {
|
|
525
|
+
if (!guard(req, res, 'POST')) return
|
|
526
|
+
const body = await readJsonBody(req)
|
|
527
|
+
const audioFiles: LibraryAudioInput[] = Array.isArray(body?.audioFiles)
|
|
528
|
+
? body.audioFiles
|
|
529
|
+
.filter((item): item is Record<string, unknown> => typeof item === 'object' && item !== null)
|
|
530
|
+
.map(item => ({
|
|
531
|
+
id: strOf(item.id) ?? '',
|
|
532
|
+
file: strOf(item.file) ?? '',
|
|
533
|
+
mime: strOf(item.mime) ?? 'audio/mpeg',
|
|
534
|
+
...(strOf(item.voiceId) !== undefined ? { voiceId: strOf(item.voiceId)! } : {}),
|
|
535
|
+
...(typeof item.duration === 'number' && Number.isFinite(item.duration) ? { duration: item.duration } : {}),
|
|
536
|
+
}))
|
|
537
|
+
.filter(item => item.id !== '' && item.file !== '')
|
|
538
|
+
: []
|
|
539
|
+
if (audioFiles.length === 0) {
|
|
540
|
+
writeJson(res, 200, { ok: false, code: 'bad-request', message: '没有可入库的音频文件' })
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
const type = parseLibraryTypeOf(body?.type)
|
|
544
|
+
if (type === undefined) {
|
|
545
|
+
writeJson(res, 200, { ok: false, code: 'bad-request', message: '资源类型无效(voice/music/sfx/tts)' })
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
const rawProvenance = typeof body?.provenance === 'object' && body.provenance !== null ? body.provenance as Record<string, unknown> : {}
|
|
549
|
+
const provenance = await mergeLibraryProvenance({
|
|
550
|
+
mode: parseModeOf(rawProvenance.mode),
|
|
551
|
+
prompt: typeof rawProvenance.prompt === 'string' ? rawProvenance.prompt.trim() : '',
|
|
552
|
+
...(strOf(rawProvenance.channel) !== undefined ? { channel: strOf(rawProvenance.channel)! } : {}),
|
|
553
|
+
...(strOf(rawProvenance.channelId) !== undefined ? { channelId: strOf(rawProvenance.channelId)! } : {}),
|
|
554
|
+
...(strOf(rawProvenance.apiUrl) !== undefined ? { apiUrl: strOf(rawProvenance.apiUrl)! } : {}),
|
|
555
|
+
...(strOf(rawProvenance.model) !== undefined ? { model: strOf(rawProvenance.model)! } : {}),
|
|
556
|
+
...(strOf(rawProvenance.upstream) !== undefined ? { upstream: strOf(rawProvenance.upstream)! } : {}),
|
|
557
|
+
...(strOf(rawProvenance.voice) !== undefined ? { voice: strOf(rawProvenance.voice)! } : {}),
|
|
558
|
+
...(strOf(rawProvenance.voiceId) !== undefined ? { voiceId: strOf(rawProvenance.voiceId)! } : {}),
|
|
559
|
+
...(typeof rawProvenance.params === 'object' && rawProvenance.params !== null
|
|
560
|
+
? { params: rawProvenance.params as Record<string, unknown> } : {}),
|
|
561
|
+
}, audioFiles, deps.resolveChannels().channels)
|
|
562
|
+
try {
|
|
563
|
+
const entry = await saveToLibrary({
|
|
564
|
+
audioFiles,
|
|
565
|
+
type,
|
|
566
|
+
...(strOf(body?.category) !== undefined ? { category: strOf(body?.category)! } : {}),
|
|
567
|
+
...(strOf(body?.name) !== undefined ? { name: strOf(body?.name)! } : {}),
|
|
568
|
+
...(strListOf(body?.tags) !== undefined ? { tags: strListOf(body?.tags)! } : {}),
|
|
569
|
+
...(strOf(body?.note) !== undefined ? { note: strOf(body?.note)! } : {}),
|
|
570
|
+
provenance,
|
|
571
|
+
})
|
|
572
|
+
writeJson(res, 200, { ok: true, entry })
|
|
573
|
+
} catch (error) {
|
|
574
|
+
writeJson(res, 200, { ok: false, code: 'library-save-failed', message: messageOf(error) })
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
kind: 'exact', path: LIBRARY_API.update,
|
|
580
|
+
handler: async (req, res) => {
|
|
581
|
+
if (!guard(req, res, 'POST')) return
|
|
582
|
+
const body = await readJsonBody(req)
|
|
583
|
+
const id = strOf(body?.id)
|
|
584
|
+
if (id === undefined) {
|
|
585
|
+
writeJson(res, 200, { ok: false, code: 'bad-request', message: '缺少资源 id' })
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const entry = await updateLibraryEntry(id, {
|
|
590
|
+
...(strOf(body?.name) !== undefined ? { name: strOf(body?.name)! } : {}),
|
|
591
|
+
...(strListOf(body?.tags) !== undefined ? { tags: strListOf(body?.tags)! } : {}),
|
|
592
|
+
...(typeof body?.note === 'string' ? { note: body.note } : {}),
|
|
593
|
+
...(strOf(body?.category) !== undefined ? { category: strOf(body?.category)! } : {}),
|
|
594
|
+
...(parseLibraryTypeOf(body?.type) !== undefined ? { type: parseLibraryTypeOf(body?.type)! } : {}),
|
|
595
|
+
})
|
|
596
|
+
if (entry === undefined) {
|
|
597
|
+
writeJson(res, 200, { ok: false, code: 'not-found', message: '资源不存在' })
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
writeJson(res, 200, { ok: true, entry })
|
|
601
|
+
} catch (error) {
|
|
602
|
+
writeJson(res, 200, { ok: false, code: 'library-update-failed', message: messageOf(error) })
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
kind: 'exact', path: LIBRARY_API.remove,
|
|
608
|
+
handler: async (req, res) => {
|
|
609
|
+
if (!guard(req, res, 'POST')) return
|
|
610
|
+
const body = await readJsonBody(req)
|
|
611
|
+
const ids = strListOf(body?.ids) ?? []
|
|
612
|
+
if (ids.length === 0) {
|
|
613
|
+
writeJson(res, 200, { ok: false, code: 'bad-request', message: '缺少资源 id' })
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const entries = await removeLibraryEntries(ids)
|
|
618
|
+
writeJson(res, 200, { ok: true, entries })
|
|
619
|
+
} catch (error) {
|
|
620
|
+
writeJson(res, 200, { ok: false, code: 'library-remove-failed', message: messageOf(error) })
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
kind: 'prefix', path: LIBRARY_API.audio,
|
|
626
|
+
handler: async (req, res) => {
|
|
627
|
+
if (!isLoopbackRequest(req)) {
|
|
628
|
+
writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
if (req.method !== 'GET') {
|
|
632
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` })
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
const rel = audioFileFrom(req.url, LIBRARY_API.audio)
|
|
636
|
+
if (rel === undefined) {
|
|
637
|
+
writeJson(res, 400, { error: 'invalid library audio' })
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
const stored = await readLibraryFile(rel)
|
|
641
|
+
if (stored === undefined) {
|
|
642
|
+
writeJson(res, 404, { error: 'library audio not found' })
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
res.writeHead(200, { 'content-type': stored.mime, 'content-length': stored.bytes, 'cache-control': 'private, max-age=3600' })
|
|
646
|
+
res.end(stored.data)
|
|
647
|
+
},
|
|
648
|
+
},
|
|
410
649
|
// ------------------------------------------------------- history
|
|
411
650
|
{
|
|
412
651
|
kind: 'exact', path: HISTORY_API.list,
|