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.
- package/README.md +2 -0
- package/lib/client.js +1669 -987
- package/lib/client.js.map +1 -1
- package/lib/index.js +124 -6
- package/package.json +1 -1
- package/src/agent-audio-tools.ts +11 -1
- package/src/audio-scheduler.ts +73 -0
- package/src/client/SettingsCard.tsx +18 -0
- package/src/client/api.ts +24 -25
- package/src/client/audio-panel.module.css +241 -0
- package/src/client/field-specs.ts +186 -0
- package/src/client/library-view.tsx +6 -1
- package/src/client/locales.ts +2 -0
- package/src/client/settings-scope.ts +18 -3
- package/src/client/studio-view.tsx +729 -390
- package/src/index.ts +14 -0
- package/src/protocol.ts +10 -1
- package/src/routes.ts +50 -2
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
|
17
17
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
18
18
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
19
19
|
import { AUDIOGEN_SETTINGS_NAMESPACE, type ChannelConfig, type ModelMapping } from './protocol.ts'
|
|
20
|
+
import { createGenerationBudget } from './audio-scheduler.ts'
|
|
20
21
|
import { makeRoutes, type ChannelsView, type SettingsSeam } from './routes.ts'
|
|
21
22
|
import type { AudioChannel } from './audio-engine.ts'
|
|
22
23
|
import { registerAgentAudioTools, type AgentAudioToolConfig } from './agent-audio-tools.ts'
|
|
@@ -40,8 +41,11 @@ export interface Config {
|
|
|
40
41
|
defaultChannelId?: string
|
|
41
42
|
defaultModel?: string
|
|
42
43
|
autoSaveToLibrary?: boolean
|
|
44
|
+
maxConcurrentGenerations?: number
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
const DEFAULT_MAX_CONCURRENT = 5
|
|
48
|
+
|
|
45
49
|
export const Config: z<Config> = z.object({
|
|
46
50
|
enabled: z.boolean().default(true),
|
|
47
51
|
announceToAgent: z.boolean().default(true),
|
|
@@ -60,6 +64,7 @@ export const Config: z<Config> = z.object({
|
|
|
60
64
|
defaultChannelId: z.string().default(''),
|
|
61
65
|
defaultModel: z.string().default(''),
|
|
62
66
|
autoSaveToLibrary: z.boolean().default(false),
|
|
67
|
+
maxConcurrentGenerations: z.number().default(DEFAULT_MAX_CONCURRENT),
|
|
63
68
|
})
|
|
64
69
|
|
|
65
70
|
const DEFAULT_ENABLED = true
|
|
@@ -150,6 +155,7 @@ export interface EffectiveConfig {
|
|
|
150
155
|
defaultChannelId: string
|
|
151
156
|
defaultModel: string
|
|
152
157
|
autoSaveToLibrary: boolean
|
|
158
|
+
maxConcurrentGenerations: number
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
export function apply(ctx: Context, config?: Config): void {
|
|
@@ -178,9 +184,15 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
178
184
|
defaultChannelId,
|
|
179
185
|
defaultModel: typeof value.defaultModel === 'string' ? value.defaultModel.trim() : '',
|
|
180
186
|
autoSaveToLibrary: value.autoSaveToLibrary === true,
|
|
187
|
+
maxConcurrentGenerations: typeof value.maxConcurrentGenerations === 'number' && Number.isFinite(value.maxConcurrentGenerations)
|
|
188
|
+
? Math.max(1, Math.min(20, Math.floor(value.maxConcurrentGenerations)))
|
|
189
|
+
: DEFAULT_MAX_CONCURRENT,
|
|
181
190
|
}
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
// 全局并发闸门:所有上游调用(面板路由 + Agent 工具)共享「最大并发生成数」。
|
|
194
|
+
const budget = createGenerationBudget(() => resolve().maxConcurrentGenerations)
|
|
195
|
+
|
|
184
196
|
const channelsView = (): ChannelsView => {
|
|
185
197
|
const value = resolve()
|
|
186
198
|
return { channels: value.channels, defaultChannelId: value.defaultChannelId }
|
|
@@ -193,6 +205,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
193
205
|
settings: seam,
|
|
194
206
|
resolveChannels: channelsView,
|
|
195
207
|
autoSave: () => resolve().autoSaveToLibrary,
|
|
208
|
+
budget,
|
|
196
209
|
})
|
|
197
210
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
198
211
|
return () => { for (const dispose of disposers) dispose() }
|
|
@@ -208,6 +221,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
208
221
|
channels: value.channels,
|
|
209
222
|
defaultChannelId: value.defaultChannelId,
|
|
210
223
|
autoSaveToLibrary: value.autoSaveToLibrary,
|
|
224
|
+
budget,
|
|
211
225
|
}
|
|
212
226
|
}), 'dsh-audiogen: agent audio tools')
|
|
213
227
|
})
|
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.2'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
|
|
14
14
|
export const SETTINGS_API = {
|
|
@@ -19,6 +19,11 @@ export const SETTINGS_API = {
|
|
|
19
19
|
/** The audio-generation proxy route. */
|
|
20
20
|
export const GENERATE_API = '/api/dsh-audiogen/generate' as const
|
|
21
21
|
|
|
22
|
+
/** Loopback-only task cancellation route (aborts the host-side upstream call). */
|
|
23
|
+
export const TASK_API = {
|
|
24
|
+
cancel: '/api/dsh-audiogen/task/cancel',
|
|
25
|
+
} as const
|
|
26
|
+
|
|
22
27
|
/** Host-mediated built-in provider catalog (channels the user can instantiate). */
|
|
23
28
|
export const PRESETS_API = '/api/dsh-audiogen/presets' as const
|
|
24
29
|
|
|
@@ -154,6 +159,8 @@ export interface GenerateAudioRequest {
|
|
|
154
159
|
channel?: string
|
|
155
160
|
/** Upstream model id actually sent (host-filled from alias mapping). */
|
|
156
161
|
upstream?: string
|
|
162
|
+
/** 客户端任务 id:宿主按 taskId 聚合 AbortController 支持真取消。 */
|
|
163
|
+
taskId?: string
|
|
157
164
|
|
|
158
165
|
// ---- MiniMax TTS 专属字段(其他厂商渠道忽略)——————
|
|
159
166
|
/** MiniMax 音色情绪,如 happy/sad/angry/nervous/fearful/bored;默认按音色自身。 */
|
|
@@ -372,4 +379,6 @@ export interface AudiogenConfig {
|
|
|
372
379
|
defaultModel?: string
|
|
373
380
|
/** 生成完成后自动加入资源库(面板与 Agent 生成均生效;单次可取消勾选)。 */
|
|
374
381
|
autoSaveToLibrary?: boolean
|
|
382
|
+
/** 最大并发生成数(同时打到上游的请求数,含对比任务内多模型)。默认 5。 */
|
|
383
|
+
maxConcurrentGenerations?: number
|
|
375
384
|
}
|
package/src/routes.ts
CHANGED
|
@@ -11,17 +11,21 @@ import { randomUUID } from 'node:crypto'
|
|
|
11
11
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
12
12
|
import { SettingsConflictError, settingsNamespace, type SettingsDescriptor } from '@deepseek-ai/dsh-settings'
|
|
13
13
|
import { generateAudio, AudioGenError, type AudioChannel } from './audio-engine.ts'
|
|
14
|
+
import type { GenerationBudget } from './audio-scheduler.ts'
|
|
14
15
|
import { discoverAudioModels } from './audio-models.ts'
|
|
15
16
|
import { AUDIO_PRESETS } from './audio-presets.ts'
|
|
16
17
|
import { appendHistory, clearHistory, listHistory, readAudioFile, removeHistory, saveAudioFile, listLibrary, saveToLibrary, updateLibraryEntry, removeLibraryEntries, readLibraryFile } from './audio-store.ts'
|
|
17
18
|
import {
|
|
18
|
-
AUDIO_API, AUDIOGEN_SETTINGS_NAMESPACE, GENERATE_API, HISTORY_API, LIBRARY_API, MODEL_API, PRESETS_API, SETTINGS_API,
|
|
19
|
+
AUDIO_API, AUDIOGEN_SETTINGS_NAMESPACE, GENERATE_API, HISTORY_API, LIBRARY_API, MODEL_API, PRESETS_API, SETTINGS_API, TASK_API,
|
|
19
20
|
LIBRARY_TYPES,
|
|
20
21
|
type GenerateAudioRequest, type GeneratedAudio, type HistoryEntryInput, type LibraryAudioInput, type LibraryProvenance, type LibraryType,
|
|
21
22
|
} from './protocol.ts'
|
|
22
23
|
|
|
23
24
|
const MAX_JSON_BODY_BYTES = 16 * 1024 * 1024
|
|
24
25
|
|
|
26
|
+
/** 宿主侧任务取消注册表:taskId → 该任务当前在途请求的 AbortController 集合。 */
|
|
27
|
+
const taskAborts = new Map<string, Set<AbortController>>()
|
|
28
|
+
|
|
25
29
|
/** Settings seam face the bridge needs. */
|
|
26
30
|
export interface SettingsSeam {
|
|
27
31
|
describe(options?: { redactSecrets?: boolean }): SettingsDescriptor[]
|
|
@@ -41,6 +45,8 @@ export interface AudiogenRoutesDeps {
|
|
|
41
45
|
resolveChannels: () => ChannelsView
|
|
42
46
|
/** Whether the auto-save-to-library setting is on. */
|
|
43
47
|
autoSave: () => boolean
|
|
48
|
+
/** Global upstream concurrency gate (maxConcurrentGenerations). */
|
|
49
|
+
budget: GenerationBudget
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
@@ -416,8 +422,24 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
|
416
422
|
}
|
|
417
423
|
const request = resolved.request
|
|
418
424
|
const channel = view.channels.find(candidate => candidate.id === request.channelId)!
|
|
425
|
+
// 任务取消支持:同一 taskId 可能对应多个并行子请求(对比任务),
|
|
426
|
+
// 宿主侧各自持有 AbortController,取消端点统一 abort。
|
|
427
|
+
const taskId = typeof body?.taskId === 'string' && body.taskId.trim() !== '' ? body.taskId.trim() : ''
|
|
428
|
+
const controller = new AbortController()
|
|
429
|
+
if (taskId !== '') {
|
|
430
|
+
const set = taskAborts.get(taskId) ?? new Set<AbortController>()
|
|
431
|
+
set.add(controller)
|
|
432
|
+
taskAborts.set(taskId, set)
|
|
433
|
+
}
|
|
419
434
|
try {
|
|
420
|
-
|
|
435
|
+
// 全局并发闸门:与 Agent 工具共享「最大并发生成数」上限。
|
|
436
|
+
const release = await deps.budget.acquire(controller.signal)
|
|
437
|
+
let outputs
|
|
438
|
+
try {
|
|
439
|
+
outputs = await generateAudio(channel, request, controller.signal)
|
|
440
|
+
} finally {
|
|
441
|
+
release()
|
|
442
|
+
}
|
|
421
443
|
const generated: GeneratedAudio[] = []
|
|
422
444
|
for (const [index, output] of outputs.entries()) {
|
|
423
445
|
const saved = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`)
|
|
@@ -477,7 +499,33 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
|
|
|
477
499
|
} catch (error) {
|
|
478
500
|
const code = error instanceof AudioGenError ? error.code : 'generate-failed'
|
|
479
501
|
writeJson(res, 200, { ok: false, code, message: messageOf(error) })
|
|
502
|
+
} finally {
|
|
503
|
+
if (taskId !== '') {
|
|
504
|
+
const set = taskAborts.get(taskId)
|
|
505
|
+
set?.delete(controller)
|
|
506
|
+
if (set !== undefined && set.size === 0) taskAborts.delete(taskId)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
// ---------------------------------------------------------- task cancel
|
|
512
|
+
{
|
|
513
|
+
kind: 'exact',
|
|
514
|
+
path: TASK_API.cancel,
|
|
515
|
+
handler: async (req, res) => {
|
|
516
|
+
if (!guard(req, res, 'POST')) return
|
|
517
|
+
const body = await readJsonBody(req)
|
|
518
|
+
const taskId = typeof body?.taskId === 'string' ? body.taskId.trim() : ''
|
|
519
|
+
if (taskId === '') {
|
|
520
|
+
writeJson(res, 200, { ok: false, code: 'bad-request', message: 'taskId is required' })
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
const controllers = taskAborts.get(taskId)
|
|
524
|
+
if (controllers !== undefined) {
|
|
525
|
+
for (const controller of controllers) controller.abort()
|
|
526
|
+
taskAborts.delete(taskId)
|
|
480
527
|
}
|
|
528
|
+
writeJson(res, 200, { ok: true, aborted: controllers !== undefined ? controllers.size : 0 })
|
|
481
529
|
},
|
|
482
530
|
},
|
|
483
531
|
// ----------------------------------------------------------- audio file
|