dsh-audiogen 0.4.1 → 0.4.3
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 +1675 -988
- package/lib/client.js.map +1 -1
- package/lib/index.js +128 -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 +23 -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 +17 -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,12 @@ export interface Config {
|
|
|
40
41
|
defaultChannelId?: string
|
|
41
42
|
defaultModel?: string
|
|
42
43
|
autoSaveToLibrary?: boolean
|
|
44
|
+
/** 设置卡按文本编辑,保存值可能是数字或数字字符串。 */
|
|
45
|
+
maxConcurrentGenerations?: number | string
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
const DEFAULT_MAX_CONCURRENT = 5
|
|
49
|
+
|
|
45
50
|
export const Config: z<Config> = z.object({
|
|
46
51
|
enabled: z.boolean().default(true),
|
|
47
52
|
announceToAgent: z.boolean().default(true),
|
|
@@ -60,6 +65,7 @@ export const Config: z<Config> = z.object({
|
|
|
60
65
|
defaultChannelId: z.string().default(''),
|
|
61
66
|
defaultModel: z.string().default(''),
|
|
62
67
|
autoSaveToLibrary: z.boolean().default(false),
|
|
68
|
+
maxConcurrentGenerations: z.union([z.number(), z.string()]).default(DEFAULT_MAX_CONCURRENT),
|
|
63
69
|
})
|
|
64
70
|
|
|
65
71
|
const DEFAULT_ENABLED = true
|
|
@@ -150,6 +156,7 @@ export interface EffectiveConfig {
|
|
|
150
156
|
defaultChannelId: string
|
|
151
157
|
defaultModel: string
|
|
152
158
|
autoSaveToLibrary: boolean
|
|
159
|
+
maxConcurrentGenerations: number
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
export function apply(ctx: Context, config?: Config): void {
|
|
@@ -178,9 +185,17 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
178
185
|
defaultChannelId,
|
|
179
186
|
defaultModel: typeof value.defaultModel === 'string' ? value.defaultModel.trim() : '',
|
|
180
187
|
autoSaveToLibrary: value.autoSaveToLibrary === true,
|
|
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
|
+
})(),
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
195
|
|
|
196
|
+
// 全局并发闸门:所有上游调用(面板路由 + Agent 工具)共享「最大并发生成数」。
|
|
197
|
+
const budget = createGenerationBudget(() => resolve().maxConcurrentGenerations)
|
|
198
|
+
|
|
184
199
|
const channelsView = (): ChannelsView => {
|
|
185
200
|
const value = resolve()
|
|
186
201
|
return { channels: value.channels, defaultChannelId: value.defaultChannelId }
|
|
@@ -193,6 +208,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
193
208
|
settings: seam,
|
|
194
209
|
resolveChannels: channelsView,
|
|
195
210
|
autoSave: () => resolve().autoSaveToLibrary,
|
|
211
|
+
budget,
|
|
196
212
|
})
|
|
197
213
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
198
214
|
return () => { for (const dispose of disposers) dispose() }
|
|
@@ -208,6 +224,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
208
224
|
channels: value.channels,
|
|
209
225
|
defaultChannelId: value.defaultChannelId,
|
|
210
226
|
autoSaveToLibrary: value.autoSaveToLibrary,
|
|
227
|
+
budget,
|
|
211
228
|
}
|
|
212
229
|
}), 'dsh-audiogen: agent audio tools')
|
|
213
230
|
})
|
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.3'
|
|
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
|