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/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.1'
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
- const outputs = await generateAudio(channel, request)
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