dsh-audiogen 0.4.0 → 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 +1700 -760
- package/lib/client.js.map +1 -1
- package/lib/index.js +412 -162
- package/package.json +1 -1
- package/skills/design/SKILL.md +5 -0
- package/skills/music/SKILL.md +5 -0
- package/skills/sfx/SKILL.md +5 -0
- package/skills/tts/SKILL.md +5 -0
- package/src/agent-audio-tools.ts +265 -149
- 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 +328 -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 +772 -254
- package/src/index.ts +46 -0
- package/src/protocol.ts +10 -1
- package/src/routes.ts +50 -2
package/src/index.ts
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
11
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, copyFileSync } from 'node:fs'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
12
15
|
import z from 'schemastery'
|
|
13
16
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
14
17
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
15
18
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
16
19
|
import { AUDIOGEN_SETTINGS_NAMESPACE, type ChannelConfig, type ModelMapping } from './protocol.ts'
|
|
20
|
+
import { createGenerationBudget } from './audio-scheduler.ts'
|
|
17
21
|
import { makeRoutes, type ChannelsView, type SettingsSeam } from './routes.ts'
|
|
18
22
|
import type { AudioChannel } from './audio-engine.ts'
|
|
19
23
|
import { registerAgentAudioTools, type AgentAudioToolConfig } from './agent-audio-tools.ts'
|
|
@@ -37,8 +41,11 @@ export interface Config {
|
|
|
37
41
|
defaultChannelId?: string
|
|
38
42
|
defaultModel?: string
|
|
39
43
|
autoSaveToLibrary?: boolean
|
|
44
|
+
maxConcurrentGenerations?: number
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
const DEFAULT_MAX_CONCURRENT = 5
|
|
48
|
+
|
|
42
49
|
export const Config: z<Config> = z.object({
|
|
43
50
|
enabled: z.boolean().default(true),
|
|
44
51
|
announceToAgent: z.boolean().default(true),
|
|
@@ -57,6 +64,7 @@ export const Config: z<Config> = z.object({
|
|
|
57
64
|
defaultChannelId: z.string().default(''),
|
|
58
65
|
defaultModel: z.string().default(''),
|
|
59
66
|
autoSaveToLibrary: z.boolean().default(false),
|
|
67
|
+
maxConcurrentGenerations: z.number().default(DEFAULT_MAX_CONCURRENT),
|
|
60
68
|
})
|
|
61
69
|
|
|
62
70
|
const DEFAULT_ENABLED = true
|
|
@@ -81,6 +89,34 @@ function guidanceFor(channels: AudioChannel[], defaultChannelId: string): string
|
|
|
81
89
|
return `${AUDIOGEN_GUIDANCE} 当前渠道与模型:${table}。`
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 把随包分发的技能(skills/<id>/SKILL.md,含 frontmatter)同步到 DSH 用户技能根
|
|
94
|
+
* `~/.dsh/skills/<id>/SKILL.md` —— DSH web 会话的 skill-filesystem(standard 等
|
|
95
|
+
* preset 行)会扫描用户根,使会话可直接触发这些技能。仅创建缺失文件,绝不覆盖
|
|
96
|
+
* 用户已有内容;任何失败仅告警,不影响插件本身。
|
|
97
|
+
*/
|
|
98
|
+
function syncBundledSkills(): void {
|
|
99
|
+
try {
|
|
100
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
101
|
+
const sourceRoot = join(packageRoot, 'skills')
|
|
102
|
+
if (existsSync(sourceRoot) !== true) return
|
|
103
|
+
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? '', '.dsh')
|
|
104
|
+
const targetRoot = join(dshHome, 'skills')
|
|
105
|
+
for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
106
|
+
if (entry.isDirectory() !== true) continue
|
|
107
|
+
const sourceFile = join(sourceRoot, entry.name, 'SKILL.md')
|
|
108
|
+
if (existsSync(sourceFile) !== true) continue
|
|
109
|
+
const targetDir = join(targetRoot, entry.name)
|
|
110
|
+
const targetFile = join(targetDir, 'SKILL.md')
|
|
111
|
+
if (existsSync(targetFile)) continue
|
|
112
|
+
mkdirSync(targetDir, { recursive: true })
|
|
113
|
+
copyFileSync(sourceFile, targetFile)
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// 技能同步为最佳努力:失败不阻断插件启动。
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
84
120
|
function normalizeChannels(value: unknown): ChannelConfig[] {
|
|
85
121
|
if (!Array.isArray(value)) return []
|
|
86
122
|
const out: ChannelConfig[] = []
|
|
@@ -119,9 +155,11 @@ export interface EffectiveConfig {
|
|
|
119
155
|
defaultChannelId: string
|
|
120
156
|
defaultModel: string
|
|
121
157
|
autoSaveToLibrary: boolean
|
|
158
|
+
maxConcurrentGenerations: number
|
|
122
159
|
}
|
|
123
160
|
|
|
124
161
|
export function apply(ctx: Context, config?: Config): void {
|
|
162
|
+
syncBundledSkills()
|
|
125
163
|
let current: () => Config = () => config ?? {}
|
|
126
164
|
|
|
127
165
|
const resolve = (): EffectiveConfig => {
|
|
@@ -146,9 +184,15 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
146
184
|
defaultChannelId,
|
|
147
185
|
defaultModel: typeof value.defaultModel === 'string' ? value.defaultModel.trim() : '',
|
|
148
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,
|
|
149
190
|
}
|
|
150
191
|
}
|
|
151
192
|
|
|
193
|
+
// 全局并发闸门:所有上游调用(面板路由 + Agent 工具)共享「最大并发生成数」。
|
|
194
|
+
const budget = createGenerationBudget(() => resolve().maxConcurrentGenerations)
|
|
195
|
+
|
|
152
196
|
const channelsView = (): ChannelsView => {
|
|
153
197
|
const value = resolve()
|
|
154
198
|
return { channels: value.channels, defaultChannelId: value.defaultChannelId }
|
|
@@ -161,6 +205,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
161
205
|
settings: seam,
|
|
162
206
|
resolveChannels: channelsView,
|
|
163
207
|
autoSave: () => resolve().autoSaveToLibrary,
|
|
208
|
+
budget,
|
|
164
209
|
})
|
|
165
210
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
166
211
|
return () => { for (const dispose of disposers) dispose() }
|
|
@@ -176,6 +221,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
176
221
|
channels: value.channels,
|
|
177
222
|
defaultChannelId: value.defaultChannelId,
|
|
178
223
|
autoSaveToLibrary: value.autoSaveToLibrary,
|
|
224
|
+
budget,
|
|
179
225
|
}
|
|
180
226
|
}), 'dsh-audiogen: agent audio tools')
|
|
181
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
|