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/lib/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { SettingsConflictError, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path, { dirname, join } from "node:path";
|
|
2
5
|
import z from "schemastery";
|
|
3
6
|
import { randomUUID } from "node:crypto";
|
|
4
7
|
import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
8
|
import os from "node:os";
|
|
7
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
10
|
//#region src/protocol.ts
|
|
@@ -20,6 +22,8 @@ const SETTINGS_API = {
|
|
|
20
22
|
};
|
|
21
23
|
/** The audio-generation proxy route. */
|
|
22
24
|
const GENERATE_API = "/api/dsh-audiogen/generate";
|
|
25
|
+
/** Loopback-only task cancellation route (aborts the host-side upstream call). */
|
|
26
|
+
const TASK_API = { cancel: "/api/dsh-audiogen/task/cancel" };
|
|
23
27
|
/** Host-mediated built-in provider catalog (channels the user can instantiate). */
|
|
24
28
|
const PRESETS_API = "/api/dsh-audiogen/presets";
|
|
25
29
|
/** Host-mediated model/voice discovery endpoint. */
|
|
@@ -50,6 +54,63 @@ const LIBRARY_TYPES = [
|
|
|
50
54
|
"tts"
|
|
51
55
|
];
|
|
52
56
|
//#endregion
|
|
57
|
+
//#region src/audio-scheduler.ts
|
|
58
|
+
function createGenerationBudget(limit) {
|
|
59
|
+
let active = 0;
|
|
60
|
+
const waiting = [];
|
|
61
|
+
const clampLimit = () => {
|
|
62
|
+
const raw = Number(limit());
|
|
63
|
+
if (!Number.isFinite(raw) || raw < 1) return 5;
|
|
64
|
+
return Math.min(20, Math.floor(raw));
|
|
65
|
+
};
|
|
66
|
+
const pump = () => {
|
|
67
|
+
const max = clampLimit();
|
|
68
|
+
while (active < max && waiting.length > 0) {
|
|
69
|
+
const entry = waiting.shift();
|
|
70
|
+
if (entry.signal?.aborted === true) {
|
|
71
|
+
entry.reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
entry.cleanup?.();
|
|
75
|
+
active += 1;
|
|
76
|
+
let released = false;
|
|
77
|
+
entry.resolve(() => {
|
|
78
|
+
if (released) return;
|
|
79
|
+
released = true;
|
|
80
|
+
active = Math.max(0, active - 1);
|
|
81
|
+
pump();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const acquire = (signal) => new Promise((resolve, reject) => {
|
|
86
|
+
const entry = {
|
|
87
|
+
resolve,
|
|
88
|
+
reject,
|
|
89
|
+
signal
|
|
90
|
+
};
|
|
91
|
+
const onAbort = () => {
|
|
92
|
+
const index = waiting.indexOf(entry);
|
|
93
|
+
if (index < 0) return;
|
|
94
|
+
waiting.splice(index, 1);
|
|
95
|
+
entry.cleanup = void 0;
|
|
96
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
97
|
+
};
|
|
98
|
+
entry.cleanup = () => {
|
|
99
|
+
signal?.removeEventListener("abort", onAbort);
|
|
100
|
+
};
|
|
101
|
+
if (signal !== void 0) {
|
|
102
|
+
if (signal.aborted === true) {
|
|
103
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
107
|
+
}
|
|
108
|
+
waiting.push(entry);
|
|
109
|
+
pump();
|
|
110
|
+
});
|
|
111
|
+
return { acquire };
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
53
114
|
//#region src/audio-engine.ts
|
|
54
115
|
/** An audio generation failure with a user-presentable message. */
|
|
55
116
|
var AudioGenError = class extends Error {
|
|
@@ -1460,6 +1521,8 @@ const LIBRARY_TYPES_VALID = [
|
|
|
1460
1521
|
//#endregion
|
|
1461
1522
|
//#region src/routes.ts
|
|
1462
1523
|
const MAX_JSON_BODY_BYTES = 16 * 1024 * 1024;
|
|
1524
|
+
/** 宿主侧任务取消注册表:taskId → 该任务当前在途请求的 AbortController 集合。 */
|
|
1525
|
+
const taskAborts = /* @__PURE__ */ new Map();
|
|
1463
1526
|
function isLoopbackRequest(request) {
|
|
1464
1527
|
const address = request.socket.remoteAddress;
|
|
1465
1528
|
if (address !== "127.0.0.1" && address !== "::1" && address !== "::ffff:127.0.0.1") return false;
|
|
@@ -1863,8 +1926,21 @@ function makeRoutes(deps) {
|
|
|
1863
1926
|
}
|
|
1864
1927
|
const request = resolved.request;
|
|
1865
1928
|
const channel = view.channels.find((candidate) => candidate.id === request.channelId);
|
|
1929
|
+
const taskId = typeof body?.taskId === "string" && body.taskId.trim() !== "" ? body.taskId.trim() : "";
|
|
1930
|
+
const controller = new AbortController();
|
|
1931
|
+
if (taskId !== "") {
|
|
1932
|
+
const set = taskAborts.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
1933
|
+
set.add(controller);
|
|
1934
|
+
taskAborts.set(taskId, set);
|
|
1935
|
+
}
|
|
1866
1936
|
try {
|
|
1867
|
-
const
|
|
1937
|
+
const release = await deps.budget.acquire(controller.signal);
|
|
1938
|
+
let outputs;
|
|
1939
|
+
try {
|
|
1940
|
+
outputs = await generateAudio(channel, request, controller.signal);
|
|
1941
|
+
} finally {
|
|
1942
|
+
release();
|
|
1943
|
+
}
|
|
1868
1944
|
const generated = [];
|
|
1869
1945
|
for (const [index, output] of outputs.entries()) {
|
|
1870
1946
|
const saved = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`);
|
|
@@ -1935,7 +2011,39 @@ function makeRoutes(deps) {
|
|
|
1935
2011
|
code: error instanceof AudioGenError ? error.code : "generate-failed",
|
|
1936
2012
|
message: messageOf(error)
|
|
1937
2013
|
});
|
|
2014
|
+
} finally {
|
|
2015
|
+
if (taskId !== "") {
|
|
2016
|
+
const set = taskAborts.get(taskId);
|
|
2017
|
+
set?.delete(controller);
|
|
2018
|
+
if (set !== void 0 && set.size === 0) taskAborts.delete(taskId);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
kind: "exact",
|
|
2025
|
+
path: TASK_API.cancel,
|
|
2026
|
+
handler: async (req, res) => {
|
|
2027
|
+
if (!guard(req, res, "POST")) return;
|
|
2028
|
+
const body = await readJsonBody(req);
|
|
2029
|
+
const taskId = typeof body?.taskId === "string" ? body.taskId.trim() : "";
|
|
2030
|
+
if (taskId === "") {
|
|
2031
|
+
writeJson(res, 200, {
|
|
2032
|
+
ok: false,
|
|
2033
|
+
code: "bad-request",
|
|
2034
|
+
message: "taskId is required"
|
|
2035
|
+
});
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const controllers = taskAborts.get(taskId);
|
|
2039
|
+
if (controllers !== void 0) {
|
|
2040
|
+
for (const controller of controllers) controller.abort();
|
|
2041
|
+
taskAborts.delete(taskId);
|
|
1938
2042
|
}
|
|
2043
|
+
writeJson(res, 200, {
|
|
2044
|
+
ok: true,
|
|
2045
|
+
aborted: controllers !== void 0 ? controllers.size : 0
|
|
2046
|
+
});
|
|
1939
2047
|
}
|
|
1940
2048
|
},
|
|
1941
2049
|
{
|
|
@@ -2215,6 +2323,29 @@ function makeRoutes(deps) {
|
|
|
2215
2323
|
}
|
|
2216
2324
|
//#endregion
|
|
2217
2325
|
//#region src/agent-audio-tools.ts
|
|
2326
|
+
const audioRefSchema = {
|
|
2327
|
+
type: "object",
|
|
2328
|
+
additionalProperties: false,
|
|
2329
|
+
properties: {
|
|
2330
|
+
id: {
|
|
2331
|
+
type: "string",
|
|
2332
|
+
required: true
|
|
2333
|
+
},
|
|
2334
|
+
url: {
|
|
2335
|
+
type: "string",
|
|
2336
|
+
required: true
|
|
2337
|
+
},
|
|
2338
|
+
mime: {
|
|
2339
|
+
type: "string",
|
|
2340
|
+
required: true
|
|
2341
|
+
},
|
|
2342
|
+
bytes: {
|
|
2343
|
+
type: "integer",
|
|
2344
|
+
required: true
|
|
2345
|
+
},
|
|
2346
|
+
voiceId: { type: "string" }
|
|
2347
|
+
}
|
|
2348
|
+
};
|
|
2218
2349
|
const resultSchema = {
|
|
2219
2350
|
type: "object",
|
|
2220
2351
|
additionalProperties: false,
|
|
@@ -2244,34 +2375,35 @@ const resultSchema = {
|
|
|
2244
2375
|
audio: {
|
|
2245
2376
|
type: "array",
|
|
2246
2377
|
required: true,
|
|
2378
|
+
items: audioRefSchema
|
|
2379
|
+
},
|
|
2380
|
+
resources: {
|
|
2381
|
+
type: "array",
|
|
2382
|
+
items: { type: "string" }
|
|
2383
|
+
},
|
|
2384
|
+
groups: {
|
|
2385
|
+
type: "array",
|
|
2247
2386
|
items: {
|
|
2248
2387
|
type: "object",
|
|
2249
2388
|
additionalProperties: false,
|
|
2250
2389
|
properties: {
|
|
2251
|
-
|
|
2390
|
+
model: {
|
|
2252
2391
|
type: "string",
|
|
2253
2392
|
required: true
|
|
2254
2393
|
},
|
|
2255
|
-
|
|
2256
|
-
type: "
|
|
2257
|
-
required: true
|
|
2258
|
-
|
|
2259
|
-
mime: {
|
|
2260
|
-
type: "string",
|
|
2261
|
-
required: true
|
|
2394
|
+
audio: {
|
|
2395
|
+
type: "array",
|
|
2396
|
+
required: true,
|
|
2397
|
+
items: audioRefSchema
|
|
2262
2398
|
},
|
|
2263
|
-
|
|
2264
|
-
type: "
|
|
2265
|
-
|
|
2399
|
+
resources: {
|
|
2400
|
+
type: "array",
|
|
2401
|
+
items: { type: "string" }
|
|
2266
2402
|
},
|
|
2267
|
-
|
|
2403
|
+
error: { type: "string" }
|
|
2268
2404
|
}
|
|
2269
2405
|
}
|
|
2270
2406
|
},
|
|
2271
|
-
resources: {
|
|
2272
|
-
type: "array",
|
|
2273
|
-
items: { type: "string" }
|
|
2274
|
-
},
|
|
2275
2407
|
error: { type: "string" }
|
|
2276
2408
|
}
|
|
2277
2409
|
};
|
|
@@ -2333,6 +2465,16 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
2333
2465
|
type: "string",
|
|
2334
2466
|
description: "One of the configured audio models/voices. Defaults to the first configured model."
|
|
2335
2467
|
},
|
|
2468
|
+
models: {
|
|
2469
|
+
type: "array",
|
|
2470
|
+
items: { type: "string" },
|
|
2471
|
+
description: "Optional: several configured model aliases to generate the SAME prompt with each one, sequentially, for comparison (e.g. [\"speech-2.8-hd\",\"speech-2.6-hd\"]). Cannot be combined with model; when present, models wins."
|
|
2472
|
+
},
|
|
2473
|
+
model_params: {
|
|
2474
|
+
type: "object",
|
|
2475
|
+
additionalProperties: true,
|
|
2476
|
+
description: "Optional per-model parameter overrides used with \"models\" (automatic by default = all models share the global params). Keys are model aliases; values are partial param objects using the same param names (format, duration, voice, speed, emotion, vol, pitch, sample_rate, bitrate, lyrics, is_instrumental, loop, prompt_influence, seed, steps, cfg_scale, subtitle_enable, aigc_watermark, language_boost, pronunciation_tone, voice_modify, timbre_weights). Unset fields fall back to the global values."
|
|
2477
|
+
},
|
|
2336
2478
|
voice: {
|
|
2337
2479
|
type: "string",
|
|
2338
2480
|
description: "Optional voice id/name for TTS providers. Required for MiniMax TTS (e.g. male-qn-qingse, female-shaonv); fetch the account voices in Settings > Plugins > AI Audio."
|
|
@@ -2463,10 +2605,15 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
2463
2605
|
type: "object",
|
|
2464
2606
|
additionalProperties: false,
|
|
2465
2607
|
properties: {
|
|
2466
|
-
voice_id: {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2608
|
+
voice_id: {
|
|
2609
|
+
type: "string",
|
|
2610
|
+
required: true
|
|
2611
|
+
},
|
|
2612
|
+
weight: {
|
|
2613
|
+
type: "integer",
|
|
2614
|
+
required: true
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2470
2617
|
},
|
|
2471
2618
|
description: "MiniMax TTS dual-voice blend weights (timbre_weights)."
|
|
2472
2619
|
},
|
|
@@ -2504,156 +2651,229 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
2504
2651
|
const config = resolve();
|
|
2505
2652
|
ensureConfigured(config);
|
|
2506
2653
|
const mode = args.mode === "music" ? "music" : args.mode === "sfx" ? "sfx" : args.mode === "voice_design" ? "voice_design" : "tts";
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const
|
|
2510
|
-
|
|
2654
|
+
/** 把生成参数(snake_case 入参或 model_params 片段)映射为请求字段。 */
|
|
2655
|
+
const mapParams = (raw) => {
|
|
2656
|
+
const voiceModify = typeof raw.voice_modify === "object" && raw.voice_modify !== null ? (() => {
|
|
2657
|
+
const src = raw.voice_modify;
|
|
2658
|
+
const out = {};
|
|
2659
|
+
if (typeof src.pitch === "number") out.pitch = src.pitch;
|
|
2660
|
+
if (typeof src.intensity === "number") out.intensity = src.intensity;
|
|
2661
|
+
if (typeof src.timbre === "number") out.timbre = src.timbre;
|
|
2662
|
+
if (typeof src.sound_effects === "string" && src.sound_effects.trim() !== "") out.soundEffects = src.sound_effects.trim();
|
|
2663
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2664
|
+
})() : void 0;
|
|
2665
|
+
const timbreWeights = Array.isArray(raw.timbre_weights) ? raw.timbre_weights.filter((item) => typeof item === "object" && item !== null && typeof item.voice_id === "string" && typeof item.weight === "number").map((item) => ({
|
|
2666
|
+
voiceId: item.voice_id.trim(),
|
|
2667
|
+
weight: item.weight
|
|
2668
|
+
})).filter((item) => item.voiceId !== "") : void 0;
|
|
2669
|
+
const stringOrEmpty = (key) => {
|
|
2670
|
+
const value = raw[key];
|
|
2671
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
2672
|
+
};
|
|
2673
|
+
const finiteOrUndefined = (key) => {
|
|
2674
|
+
const value = raw[key];
|
|
2675
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2676
|
+
};
|
|
2511
2677
|
return {
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2678
|
+
...stringOrEmpty("voice") !== void 0 ? { voice: stringOrEmpty("voice") } : {},
|
|
2679
|
+
...stringOrEmpty("preview_text") !== void 0 ? { previewText: stringOrEmpty("preview_text") } : {},
|
|
2680
|
+
...finiteOrUndefined("speed") !== void 0 ? { speed: finiteOrUndefined("speed") } : {},
|
|
2681
|
+
...finiteOrUndefined("duration") !== void 0 ? { duration: finiteOrUndefined("duration") } : {},
|
|
2682
|
+
...stringOrEmpty("lyrics") !== void 0 ? { lyrics: stringOrEmpty("lyrics") } : {},
|
|
2683
|
+
...typeof raw.is_instrumental === "boolean" ? { isInstrumental: raw.is_instrumental } : {},
|
|
2684
|
+
...typeof raw.loop === "boolean" ? { loop: raw.loop } : {},
|
|
2685
|
+
...finiteOrUndefined("prompt_influence") !== void 0 ? { promptInfluence: finiteOrUndefined("prompt_influence") } : {},
|
|
2686
|
+
...finiteOrUndefined("seed") !== void 0 ? { seed: finiteOrUndefined("seed") } : {},
|
|
2687
|
+
...finiteOrUndefined("steps") !== void 0 ? { steps: finiteOrUndefined("steps") } : {},
|
|
2688
|
+
...finiteOrUndefined("cfg_scale") !== void 0 ? { cfgScale: finiteOrUndefined("cfg_scale") } : {},
|
|
2689
|
+
...stringOrEmpty("format") !== void 0 ? { format: stringOrEmpty("format") } : {},
|
|
2690
|
+
...stringOrEmpty("emotion") !== void 0 ? { emotion: stringOrEmpty("emotion") } : {},
|
|
2691
|
+
...finiteOrUndefined("vol") !== void 0 ? { vol: finiteOrUndefined("vol") } : {},
|
|
2692
|
+
...finiteOrUndefined("pitch") !== void 0 ? { pitch: finiteOrUndefined("pitch") } : {},
|
|
2693
|
+
...typeof raw.text_normalization === "boolean" ? { textNormalization: raw.text_normalization } : {},
|
|
2694
|
+
...typeof raw.latex_read === "boolean" ? { latexRead: raw.latex_read } : {},
|
|
2695
|
+
...Array.isArray(raw.pronunciation_tone) && raw.pronunciation_tone.length > 0 ? { pronunciationTone: raw.pronunciation_tone.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()) } : {},
|
|
2696
|
+
...finiteOrUndefined("sample_rate") !== void 0 ? { sampleRate: finiteOrUndefined("sample_rate") } : {},
|
|
2697
|
+
...finiteOrUndefined("bitrate") !== void 0 ? { bitrate: finiteOrUndefined("bitrate") } : {},
|
|
2698
|
+
...finiteOrUndefined("channel") !== void 0 ? { audioChannel: finiteOrUndefined("channel") } : {},
|
|
2699
|
+
...typeof raw.force_cbr === "boolean" ? { forceCbr: raw.force_cbr } : {},
|
|
2700
|
+
...typeof raw.subtitle_enable === "boolean" ? { subtitleEnable: raw.subtitle_enable } : {},
|
|
2701
|
+
...typeof raw.aigc_watermark === "boolean" ? { aigcWatermark: raw.aigc_watermark } : {},
|
|
2702
|
+
...stringOrEmpty("language_boost") !== void 0 ? { languageBoost: stringOrEmpty("language_boost") } : {},
|
|
2703
|
+
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
2704
|
+
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
2515
2705
|
};
|
|
2516
|
-
})() : resolveModel(config, args.model);
|
|
2517
|
-
const voiceModify = typeof args.voice_modify === "object" && args.voice_modify !== null ? (() => {
|
|
2518
|
-
const raw = args.voice_modify;
|
|
2519
|
-
const out = {};
|
|
2520
|
-
if (typeof raw.pitch === "number") out.pitch = raw.pitch;
|
|
2521
|
-
if (typeof raw.intensity === "number") out.intensity = raw.intensity;
|
|
2522
|
-
if (typeof raw.timbre === "number") out.timbre = raw.timbre;
|
|
2523
|
-
if (typeof raw.sound_effects === "string" && raw.sound_effects.trim() !== "") out.soundEffects = raw.sound_effects.trim();
|
|
2524
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
2525
|
-
})() : void 0;
|
|
2526
|
-
const timbreWeights = Array.isArray(args.timbre_weights) ? args.timbre_weights.filter((item) => typeof item === "object" && item !== null && typeof item.voice_id === "string" && typeof item.weight === "number").map((item) => ({
|
|
2527
|
-
voiceId: item.voice_id.trim(),
|
|
2528
|
-
weight: item.weight
|
|
2529
|
-
})).filter((item) => item.voiceId !== "") : void 0;
|
|
2530
|
-
const request = {
|
|
2531
|
-
mode,
|
|
2532
|
-
model: picked.alias,
|
|
2533
|
-
upstream: picked.upstream,
|
|
2534
|
-
channelId: picked.channel.id,
|
|
2535
|
-
channel: picked.channel.name,
|
|
2536
|
-
prompt: args.prompt.trim(),
|
|
2537
|
-
...typeof args.voice === "string" && args.voice.trim() !== "" ? { voice: args.voice.trim() } : {},
|
|
2538
|
-
...typeof args.preview_text === "string" && args.preview_text.trim() !== "" ? { previewText: args.preview_text.trim() } : {},
|
|
2539
|
-
...typeof args.speed === "number" ? { speed: args.speed } : {},
|
|
2540
|
-
...typeof args.duration === "number" ? { duration: args.duration } : {},
|
|
2541
|
-
...typeof args.lyrics === "string" && args.lyrics.trim() !== "" ? { lyrics: args.lyrics.trim() } : {},
|
|
2542
|
-
...typeof args.is_instrumental === "boolean" ? { isInstrumental: args.is_instrumental } : {},
|
|
2543
|
-
...typeof args.loop === "boolean" ? { loop: args.loop } : {},
|
|
2544
|
-
...typeof args.prompt_influence === "number" && Number.isFinite(args.prompt_influence) ? { promptInfluence: args.prompt_influence } : {},
|
|
2545
|
-
...typeof args.seed === "number" && Number.isFinite(args.seed) ? { seed: args.seed } : {},
|
|
2546
|
-
...typeof args.steps === "number" && Number.isFinite(args.steps) ? { steps: args.steps } : {},
|
|
2547
|
-
...typeof args.cfg_scale === "number" && Number.isFinite(args.cfg_scale) ? { cfgScale: args.cfg_scale } : {},
|
|
2548
|
-
...typeof args.format === "string" && args.format.trim() !== "" ? { format: args.format.trim() } : {},
|
|
2549
|
-
...typeof args.emotion === "string" && args.emotion.trim() !== "" ? { emotion: args.emotion.trim() } : {},
|
|
2550
|
-
...typeof args.vol === "number" && Number.isFinite(args.vol) ? { vol: args.vol } : {},
|
|
2551
|
-
...typeof args.pitch === "number" && Number.isFinite(args.pitch) ? { pitch: args.pitch } : {},
|
|
2552
|
-
...typeof args.text_normalization === "boolean" ? { textNormalization: args.text_normalization } : {},
|
|
2553
|
-
...typeof args.latex_read === "boolean" ? { latexRead: args.latex_read } : {},
|
|
2554
|
-
...Array.isArray(args.pronunciation_tone) && args.pronunciation_tone.length > 0 ? { pronunciationTone: args.pronunciation_tone.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()) } : {},
|
|
2555
|
-
...typeof args.sample_rate === "number" && Number.isFinite(args.sample_rate) ? { sampleRate: args.sample_rate } : {},
|
|
2556
|
-
...typeof args.bitrate === "number" && Number.isFinite(args.bitrate) ? { bitrate: args.bitrate } : {},
|
|
2557
|
-
...typeof args.channel === "number" && Number.isFinite(args.channel) ? { audioChannel: args.channel } : {},
|
|
2558
|
-
...typeof args.force_cbr === "boolean" ? { forceCbr: args.force_cbr } : {},
|
|
2559
|
-
...typeof args.subtitle_enable === "boolean" ? { subtitleEnable: args.subtitle_enable } : {},
|
|
2560
|
-
...typeof args.aigc_watermark === "boolean" ? { aigcWatermark: args.aigc_watermark } : {},
|
|
2561
|
-
...typeof args.language_boost === "string" && args.language_boost.trim() !== "" ? { languageBoost: args.language_boost.trim() } : {},
|
|
2562
|
-
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
2563
|
-
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
2564
2706
|
};
|
|
2565
|
-
|
|
2566
|
-
const
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
saved.push({
|
|
2572
|
-
id: stored.id,
|
|
2573
|
-
url: `/api/dsh-audiogen/audio/${encodeURIComponent(stored.file)}`,
|
|
2574
|
-
file: stored.file,
|
|
2575
|
-
mime: stored.mime,
|
|
2576
|
-
bytes: stored.bytes,
|
|
2577
|
-
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
2578
|
-
});
|
|
2579
|
-
audio.push({
|
|
2580
|
-
id: stored.id,
|
|
2581
|
-
url: `/api/dsh-audiogen/audio/${encodeURIComponent(stored.file)}`,
|
|
2582
|
-
mime: stored.mime,
|
|
2583
|
-
bytes: stored.bytes,
|
|
2584
|
-
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
2585
|
-
});
|
|
2707
|
+
const buildRequest = (picked) => {
|
|
2708
|
+
const base = mapParams(args);
|
|
2709
|
+
let override = {};
|
|
2710
|
+
if (typeof args.model_params === "object" && args.model_params !== null) {
|
|
2711
|
+
const perModel = args.model_params[picked.alias];
|
|
2712
|
+
if (typeof perModel === "object" && perModel !== null) override = mapParams(perModel);
|
|
2586
2713
|
}
|
|
2714
|
+
return {
|
|
2715
|
+
mode,
|
|
2716
|
+
model: picked.alias,
|
|
2717
|
+
upstream: picked.upstream,
|
|
2718
|
+
channelId: picked.channel.id,
|
|
2719
|
+
channel: picked.channel.name,
|
|
2720
|
+
prompt: typeof args.prompt === "string" ? args.prompt.trim() : "",
|
|
2721
|
+
...base,
|
|
2722
|
+
...override
|
|
2723
|
+
};
|
|
2724
|
+
};
|
|
2725
|
+
/** 单模型执行:生成 + 保存文件 + 历史 + 可选资源库;错误收敛为分组结果。 */
|
|
2726
|
+
const runOne = async (picked) => {
|
|
2727
|
+
const request = buildRequest(picked);
|
|
2587
2728
|
try {
|
|
2588
|
-
await
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2729
|
+
const release = await (config.budget?.acquire(exec.signal) ?? Promise.resolve(() => {}));
|
|
2730
|
+
let outputs;
|
|
2731
|
+
try {
|
|
2732
|
+
outputs = await generateAudio(picked.channel, request, exec.signal);
|
|
2733
|
+
} finally {
|
|
2734
|
+
release();
|
|
2735
|
+
}
|
|
2736
|
+
const audio = [];
|
|
2737
|
+
const saved = [];
|
|
2738
|
+
for (const [index, output] of outputs.entries()) {
|
|
2739
|
+
const stored = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`);
|
|
2740
|
+
saved.push({
|
|
2741
|
+
id: stored.id,
|
|
2742
|
+
url: `/api/dsh-audiogen/audio/${encodeURIComponent(stored.file)}`,
|
|
2743
|
+
file: stored.file,
|
|
2744
|
+
mime: stored.mime,
|
|
2745
|
+
bytes: stored.bytes,
|
|
2605
2746
|
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
2606
|
-
})
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
id:
|
|
2618
|
-
|
|
2619
|
-
mime: item.mime,
|
|
2620
|
-
...item.voiceId === void 0 ? {} : { voiceId: item.voiceId }
|
|
2621
|
-
})),
|
|
2622
|
-
type: libraryTypeOf(request.mode, args.library_type),
|
|
2623
|
-
...typeof args.library_name === "string" && args.library_name.trim() !== "" ? { name: args.library_name.trim() } : {},
|
|
2624
|
-
...Array.isArray(args.library_tags) ? { tags: args.library_tags.filter((tag) => typeof tag === "string" && tag.trim() !== "").map((tag) => tag.trim()) } : {},
|
|
2625
|
-
provenance: {
|
|
2747
|
+
});
|
|
2748
|
+
audio.push({
|
|
2749
|
+
id: stored.id,
|
|
2750
|
+
url: `/api/dsh-audiogen/audio/${encodeURIComponent(stored.file)}`,
|
|
2751
|
+
mime: stored.mime,
|
|
2752
|
+
bytes: stored.bytes,
|
|
2753
|
+
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
try {
|
|
2757
|
+
await appendHistory({
|
|
2758
|
+
id: randomUUID(),
|
|
2759
|
+
createdAt: Date.now(),
|
|
2626
2760
|
mode: request.mode,
|
|
2627
|
-
prompt: request.prompt,
|
|
2628
|
-
channel: picked.channel.name,
|
|
2629
|
-
channelId: picked.channel.id,
|
|
2630
|
-
apiUrl: picked.channel.apiUrl,
|
|
2631
2761
|
model: picked.alias,
|
|
2632
|
-
|
|
2762
|
+
prompt: request.prompt,
|
|
2633
2763
|
...request.voice === void 0 ? {} : { voice: request.voice },
|
|
2764
|
+
...request.speed === void 0 ? {} : { speed: request.speed },
|
|
2765
|
+
...request.duration === void 0 ? {} : { duration: request.duration },
|
|
2766
|
+
...request.format === void 0 ? {} : { format: request.format },
|
|
2767
|
+
audio: outputs.map((output, index) => ({
|
|
2768
|
+
id: saved[index].id,
|
|
2769
|
+
file: saved[index].file,
|
|
2770
|
+
b64: Buffer.from(output.data).toString("base64"),
|
|
2771
|
+
mime: saved[index].mime,
|
|
2772
|
+
bytes: saved[index].bytes,
|
|
2773
|
+
url: saved[index].url,
|
|
2774
|
+
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
2775
|
+
})),
|
|
2776
|
+
channelId: picked.channel.id,
|
|
2777
|
+
channel: picked.channel.name,
|
|
2634
2778
|
params: { ...request }
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2779
|
+
});
|
|
2780
|
+
} catch {}
|
|
2781
|
+
const wantSave = args.save_to_library === true || config.autoSaveToLibrary && args.save_to_library !== false;
|
|
2782
|
+
let resources;
|
|
2783
|
+
if (wantSave) try {
|
|
2784
|
+
resources = [(await saveToLibrary({
|
|
2785
|
+
audioFiles: saved.map((item) => ({
|
|
2786
|
+
id: item.id,
|
|
2787
|
+
file: item.file,
|
|
2788
|
+
mime: item.mime,
|
|
2789
|
+
...item.voiceId === void 0 ? {} : { voiceId: item.voiceId }
|
|
2790
|
+
})),
|
|
2791
|
+
type: libraryTypeOf(request.mode, args.library_type),
|
|
2792
|
+
...typeof args.library_name === "string" && args.library_name.trim() !== "" ? { name: args.library_name.trim() } : {},
|
|
2793
|
+
...Array.isArray(args.library_tags) ? { tags: args.library_tags.filter((tag) => typeof tag === "string" && tag.trim() !== "").map((tag) => tag.trim()) } : {},
|
|
2794
|
+
provenance: {
|
|
2795
|
+
mode: request.mode,
|
|
2796
|
+
prompt: request.prompt,
|
|
2797
|
+
channel: picked.channel.name,
|
|
2798
|
+
channelId: picked.channel.id,
|
|
2799
|
+
apiUrl: picked.channel.apiUrl,
|
|
2800
|
+
model: picked.alias,
|
|
2801
|
+
upstream: picked.upstream,
|
|
2802
|
+
...request.voice === void 0 ? {} : { voice: request.voice },
|
|
2803
|
+
params: { ...request }
|
|
2804
|
+
}
|
|
2805
|
+
})).id];
|
|
2806
|
+
} catch {}
|
|
2807
|
+
return {
|
|
2808
|
+
model: picked.alias,
|
|
2809
|
+
audio,
|
|
2810
|
+
...resources === void 0 ? {} : { resources }
|
|
2811
|
+
};
|
|
2812
|
+
} catch (error) {
|
|
2813
|
+
if (exec.signal?.aborted === true) throw error;
|
|
2814
|
+
return {
|
|
2815
|
+
model: picked.alias,
|
|
2816
|
+
audio: [],
|
|
2817
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
const requestedModels = Array.isArray(args.models) ? [...new Set(args.models.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()))] : [];
|
|
2822
|
+
if (requestedModels.length > 0 && mode !== "voice_design") {
|
|
2823
|
+
const groups = [];
|
|
2824
|
+
let succeeded = 0;
|
|
2825
|
+
for (const alias of requestedModels) {
|
|
2826
|
+
let picked;
|
|
2827
|
+
try {
|
|
2828
|
+
picked = resolveModel(config, alias);
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
groups.push({
|
|
2831
|
+
model: alias,
|
|
2832
|
+
audio: [],
|
|
2833
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2834
|
+
});
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2837
|
+
const group = await runOne(picked);
|
|
2838
|
+
groups.push(group);
|
|
2839
|
+
if (group.error === void 0) succeeded++;
|
|
2840
|
+
}
|
|
2638
2841
|
return {
|
|
2639
|
-
status: "completed",
|
|
2640
|
-
message:
|
|
2641
|
-
mode
|
|
2642
|
-
model:
|
|
2643
|
-
audio,
|
|
2644
|
-
|
|
2842
|
+
status: succeeded > 0 ? "completed" : "failed",
|
|
2843
|
+
message: succeeded > 0 ? `Generated ${succeeded}/${groups.length} model(s) with the same prompt for comparison. The audio files can be played/downloaded from the returned URLs.` : "All model generations failed.",
|
|
2844
|
+
mode,
|
|
2845
|
+
model: groups[0]?.model ?? requestedModels[0],
|
|
2846
|
+
audio: groups.flatMap((group) => group.audio),
|
|
2847
|
+
groups,
|
|
2848
|
+
...succeeded === 0 ? { error: groups.map((group) => `${group.model}: ${group.error ?? ""}`).filter((item) => !item.endsWith(": ")).join(";") } : {}
|
|
2645
2849
|
};
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2850
|
+
}
|
|
2851
|
+
const one = await runOne(mode === "voice_design" ? (() => {
|
|
2852
|
+
const usable = config.channels.filter((channel) => channel.apiUrl.trim() !== "" && channel.apiKey.trim() !== "");
|
|
2853
|
+
const target = usable.find((channel) => channel.id === config.defaultChannelId) ?? usable[0];
|
|
2854
|
+
if (target === void 0) throw new AudioGenError("No usable audio channel is configured for voice design.", "no-channel-available");
|
|
2648
2855
|
return {
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
model: picked.alias,
|
|
2653
|
-
audio: [],
|
|
2654
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2856
|
+
channel: target,
|
|
2857
|
+
alias: "",
|
|
2858
|
+
upstream: ""
|
|
2655
2859
|
};
|
|
2656
|
-
}
|
|
2860
|
+
})() : resolveModel(config, args.model));
|
|
2861
|
+
if (one.error !== void 0) return {
|
|
2862
|
+
status: "failed",
|
|
2863
|
+
message: "Audio generation failed.",
|
|
2864
|
+
mode,
|
|
2865
|
+
model: one.model,
|
|
2866
|
+
audio: [],
|
|
2867
|
+
error: one.error
|
|
2868
|
+
};
|
|
2869
|
+
return {
|
|
2870
|
+
status: "completed",
|
|
2871
|
+
message: "Audio generation completed. The audio files can be played/downloaded from the returned URLs.",
|
|
2872
|
+
mode,
|
|
2873
|
+
model: one.model,
|
|
2874
|
+
audio: one.audio,
|
|
2875
|
+
...one.resources === void 0 ? {} : { resources: one.resources }
|
|
2876
|
+
};
|
|
2657
2877
|
}
|
|
2658
2878
|
}));
|
|
2659
2879
|
const searchDisposer = ctx.tools.register(defineTool({
|
|
@@ -2796,6 +3016,7 @@ const name = "audiogen";
|
|
|
2796
3016
|
const inject = ["webServer", "systemPrompt"];
|
|
2797
3017
|
/** The branded settings namespace of this plugin. */
|
|
2798
3018
|
const AudioGenSettingsNamespace = settingsNamespace(AUDIOGEN_SETTINGS_NAMESPACE);
|
|
3019
|
+
const DEFAULT_MAX_CONCURRENT = 5;
|
|
2799
3020
|
const Config = z.object({
|
|
2800
3021
|
enabled: z.boolean().default(true),
|
|
2801
3022
|
announceToAgent: z.boolean().default(true),
|
|
@@ -2813,7 +3034,8 @@ const Config = z.object({
|
|
|
2813
3034
|
channelSecrets: z.dict(z.string().role("secret")).default({}),
|
|
2814
3035
|
defaultChannelId: z.string().default(""),
|
|
2815
3036
|
defaultModel: z.string().default(""),
|
|
2816
|
-
autoSaveToLibrary: z.boolean().default(false)
|
|
3037
|
+
autoSaveToLibrary: z.boolean().default(false),
|
|
3038
|
+
maxConcurrentGenerations: z.number().default(DEFAULT_MAX_CONCURRENT)
|
|
2817
3039
|
});
|
|
2818
3040
|
const DEFAULT_ENABLED = true;
|
|
2819
3041
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -2831,6 +3053,29 @@ function guidanceFor(channels, defaultChannelId) {
|
|
|
2831
3053
|
}).join(";");
|
|
2832
3054
|
return `${AUDIOGEN_GUIDANCE} 当前渠道与模型:${table}。`;
|
|
2833
3055
|
}
|
|
3056
|
+
/**
|
|
3057
|
+
* 把随包分发的技能(skills/<id>/SKILL.md,含 frontmatter)同步到 DSH 用户技能根
|
|
3058
|
+
* `~/.dsh/skills/<id>/SKILL.md` —— DSH web 会话的 skill-filesystem(standard 等
|
|
3059
|
+
* preset 行)会扫描用户根,使会话可直接触发这些技能。仅创建缺失文件,绝不覆盖
|
|
3060
|
+
* 用户已有内容;任何失败仅告警,不影响插件本身。
|
|
3061
|
+
*/
|
|
3062
|
+
function syncBundledSkills() {
|
|
3063
|
+
try {
|
|
3064
|
+
const sourceRoot = join(dirname(dirname(fileURLToPath(import.meta.url))), "skills");
|
|
3065
|
+
if (existsSync(sourceRoot) !== true) return;
|
|
3066
|
+
const targetRoot = join(process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh"), "skills");
|
|
3067
|
+
for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
3068
|
+
if (entry.isDirectory() !== true) continue;
|
|
3069
|
+
const sourceFile = join(sourceRoot, entry.name, "SKILL.md");
|
|
3070
|
+
if (existsSync(sourceFile) !== true) continue;
|
|
3071
|
+
const targetDir = join(targetRoot, entry.name);
|
|
3072
|
+
const targetFile = join(targetDir, "SKILL.md");
|
|
3073
|
+
if (existsSync(targetFile)) continue;
|
|
3074
|
+
mkdirSync(targetDir, { recursive: true });
|
|
3075
|
+
copyFileSync(sourceFile, targetFile);
|
|
3076
|
+
}
|
|
3077
|
+
} catch {}
|
|
3078
|
+
}
|
|
2834
3079
|
function normalizeChannels(value) {
|
|
2835
3080
|
if (!Array.isArray(value)) return [];
|
|
2836
3081
|
const out = [];
|
|
@@ -2862,6 +3107,7 @@ function normalizeChannels(value) {
|
|
|
2862
3107
|
return out;
|
|
2863
3108
|
}
|
|
2864
3109
|
function apply(ctx, config) {
|
|
3110
|
+
syncBundledSkills();
|
|
2865
3111
|
let current = () => config ?? {};
|
|
2866
3112
|
const resolve = () => {
|
|
2867
3113
|
const value = current() ?? {};
|
|
@@ -2882,9 +3128,11 @@ function apply(ctx, config) {
|
|
|
2882
3128
|
})),
|
|
2883
3129
|
defaultChannelId,
|
|
2884
3130
|
defaultModel: typeof value.defaultModel === "string" ? value.defaultModel.trim() : "",
|
|
2885
|
-
autoSaveToLibrary: value.autoSaveToLibrary === true
|
|
3131
|
+
autoSaveToLibrary: value.autoSaveToLibrary === true,
|
|
3132
|
+
maxConcurrentGenerations: typeof value.maxConcurrentGenerations === "number" && Number.isFinite(value.maxConcurrentGenerations) ? Math.max(1, Math.min(20, Math.floor(value.maxConcurrentGenerations))) : DEFAULT_MAX_CONCURRENT
|
|
2886
3133
|
};
|
|
2887
3134
|
};
|
|
3135
|
+
const budget = createGenerationBudget(() => resolve().maxConcurrentGenerations);
|
|
2888
3136
|
const channelsView = () => {
|
|
2889
3137
|
const value = resolve();
|
|
2890
3138
|
return {
|
|
@@ -2898,7 +3146,8 @@ function apply(ctx, config) {
|
|
|
2898
3146
|
const disposers = makeRoutes({
|
|
2899
3147
|
settings: seam,
|
|
2900
3148
|
resolveChannels: channelsView,
|
|
2901
|
-
autoSave: () => resolve().autoSaveToLibrary
|
|
3149
|
+
autoSave: () => resolve().autoSaveToLibrary,
|
|
3150
|
+
budget
|
|
2902
3151
|
}).map((route) => ctx.webServer.register(route));
|
|
2903
3152
|
return () => {
|
|
2904
3153
|
for (const dispose of disposers) dispose();
|
|
@@ -2913,7 +3162,8 @@ function apply(ctx, config) {
|
|
|
2913
3162
|
allowAgentAudioGeneration: value.allowAgentAudioGeneration,
|
|
2914
3163
|
channels: value.channels,
|
|
2915
3164
|
defaultChannelId: value.defaultChannelId,
|
|
2916
|
-
autoSaveToLibrary: value.autoSaveToLibrary
|
|
3165
|
+
autoSaveToLibrary: value.autoSaveToLibrary,
|
|
3166
|
+
budget
|
|
2917
3167
|
};
|
|
2918
3168
|
}), "dsh-audiogen: agent audio tools");
|
|
2919
3169
|
});
|