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/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 outputs = await generateAudio(channel, request);
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
- id: {
2390
+ model: {
2252
2391
  type: "string",
2253
2392
  required: true
2254
2393
  },
2255
- url: {
2256
- type: "string",
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
- bytes: {
2264
- type: "integer",
2265
- required: true
2399
+ resources: {
2400
+ type: "array",
2401
+ items: { type: "string" }
2266
2402
  },
2267
- voiceId: { type: "string" }
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: { type: "string" },
2467
- weight: { type: "integer" }
2468
- },
2469
- required: ["voice_id", "weight"]
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
- const picked = mode === "voice_design" ? (() => {
2508
- const usable = config.channels.filter((channel) => channel.apiUrl.trim() !== "" && channel.apiKey.trim() !== "");
2509
- const target = usable.find((channel) => channel.id === config.defaultChannelId) ?? usable[0];
2510
- if (target === void 0) throw new AudioGenError("No usable audio channel is configured for voice design.", "no-channel-available");
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
- channel: target,
2513
- alias: "",
2514
- upstream: ""
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
- try {
2566
- const outputs = await generateAudio(picked.channel, request, exec.signal);
2567
- const audio = [];
2568
- const saved = [];
2569
- for (const [index, output] of outputs.entries()) {
2570
- const stored = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`);
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 appendHistory({
2589
- id: randomUUID(),
2590
- createdAt: Date.now(),
2591
- mode: request.mode,
2592
- model: picked.alias,
2593
- prompt: request.prompt,
2594
- ...request.voice === void 0 ? {} : { voice: request.voice },
2595
- ...request.speed === void 0 ? {} : { speed: request.speed },
2596
- ...request.duration === void 0 ? {} : { duration: request.duration },
2597
- ...request.format === void 0 ? {} : { format: request.format },
2598
- audio: outputs.map((output, index) => ({
2599
- id: saved[index].id,
2600
- file: saved[index].file,
2601
- b64: Buffer.from(output.data).toString("base64"),
2602
- mime: saved[index].mime,
2603
- bytes: saved[index].bytes,
2604
- url: saved[index].url,
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
- channelId: picked.channel.id,
2608
- channel: picked.channel.name,
2609
- params: { ...request }
2610
- });
2611
- } catch {}
2612
- const wantSave = args.save_to_library === true || config.autoSaveToLibrary && args.save_to_library !== false;
2613
- let resources;
2614
- if (wantSave) try {
2615
- resources = [(await saveToLibrary({
2616
- audioFiles: saved.map((item) => ({
2617
- id: item.id,
2618
- file: item.file,
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
- upstream: picked.upstream,
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
- })).id];
2637
- } catch {}
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: "Audio generation completed. The audio files can be played/downloaded from the returned URLs.",
2641
- mode: request.mode,
2642
- model: picked.alias,
2643
- audio,
2644
- ...resources === void 0 ? {} : { resources }
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
- } catch (error) {
2647
- if (exec.signal?.aborted === true) throw error;
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
- status: "failed",
2650
- message: "Audio generation failed.",
2651
- mode: request.mode,
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
  });