dsh-audiogen 0.3.5 → 0.4.1
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 +7 -1
- package/lib/client.js +2639 -703
- package/lib/client.js.map +1 -1
- package/lib/index.js +1119 -149
- package/package.json +1 -1
- package/skills/design/SKILL.md +5 -0
- package/skills/music/SKILL.md +29 -0
- package/skills/sfx/SKILL.md +5 -0
- package/skills/tts/SKILL.md +5 -0
- package/src/agent-audio-tools.ts +358 -111
- package/src/audio-engine.ts +173 -11
- package/src/audio-presets.ts +4 -3
- package/src/audio-store.ts +251 -3
- package/src/client/AudioGenPanel.tsx +59 -376
- package/src/client/SettingsCard.tsx +9 -0
- package/src/client/api.ts +45 -2
- package/src/client/audio-panel.module.css +738 -78
- package/src/client/audio-player.tsx +87 -0
- package/src/client/icons.tsx +170 -0
- package/src/client/library-save-dialog.tsx +173 -0
- package/src/client/library-view.tsx +511 -0
- package/src/client/library.module.css +686 -0
- package/src/client/locales.ts +6 -0
- package/src/client/settings-scope.ts +2 -0
- package/src/client/studio-view.tsx +739 -0
- package/src/index.ts +38 -0
- package/src/protocol.ts +126 -1
- package/src/routes.ts +237 -4
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
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
7
|
+
import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
6
8
|
import os from "node:os";
|
|
7
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
10
|
//#region src/protocol.ts
|
|
@@ -34,6 +36,21 @@ const HISTORY_API = {
|
|
|
34
36
|
clear: "/api/dsh-audiogen/history/clear",
|
|
35
37
|
audio: "/api/dsh-audiogen/history/audio"
|
|
36
38
|
};
|
|
39
|
+
/** Host-persisted resource-library routes. */
|
|
40
|
+
const LIBRARY_API = {
|
|
41
|
+
list: "/api/dsh-audiogen/library/list",
|
|
42
|
+
save: "/api/dsh-audiogen/library/save",
|
|
43
|
+
update: "/api/dsh-audiogen/library/update",
|
|
44
|
+
remove: "/api/dsh-audiogen/library/remove",
|
|
45
|
+
audio: "/api/dsh-audiogen/library/audio"
|
|
46
|
+
};
|
|
47
|
+
/** All library types, for iteration and validation. */
|
|
48
|
+
const LIBRARY_TYPES = [
|
|
49
|
+
"voice",
|
|
50
|
+
"music",
|
|
51
|
+
"sfx",
|
|
52
|
+
"tts"
|
|
53
|
+
];
|
|
37
54
|
//#endregion
|
|
38
55
|
//#region src/audio-engine.ts
|
|
39
56
|
/** An audio generation failure with a user-presentable message. */
|
|
@@ -79,7 +96,12 @@ function detectAudioMime(data) {
|
|
|
79
96
|
}
|
|
80
97
|
function mimeFromContentType(value) {
|
|
81
98
|
if (value === null || value === "") return void 0;
|
|
82
|
-
|
|
99
|
+
const parts = value.split(";");
|
|
100
|
+
for (const part of parts.slice(1)) {
|
|
101
|
+
const match = /^\s*type=([^;\s]+)/i.exec(part);
|
|
102
|
+
if (match !== null) return match[1].trim().toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
return parts[0].trim().toLowerCase();
|
|
83
105
|
}
|
|
84
106
|
function audioMime(data, contentType) {
|
|
85
107
|
return detectAudioMime(data) ?? mimeFromContentType(contentType) ?? "audio/mpeg";
|
|
@@ -591,30 +613,155 @@ async function minimax(channel, request, signal) {
|
|
|
591
613
|
throw new AudioGenError(`MiniMax 渠道「${channel.name}」网关未提供原生 TTS 接口:POST ${endpoint} 返回 HTTP 404(Invalid URL,网关未路由 /v1/t2a_v2);已回退 OpenAI 兼容 ${minimaxApiBase(channel.apiUrl)}/audio/speech 仍失败:${detailText.slice(0, 300)}。请把渠道 API 地址配置为官方 https://api.minimaxi.com(配合 MiniMax 官方密钥),或确认网关已将 /v1/audio/speech 映射到 MiniMax 音色渠道。`, "audio-api-error");
|
|
592
614
|
}
|
|
593
615
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
616
|
+
/** Stability 内部信号:路由缺失(网关 404 Invalid URL),可切换另一协议重试。 */
|
|
617
|
+
var StabilityRouteMissError = class extends Error {};
|
|
618
|
+
/** 网关风格:apiUrl 形如 .../v1、.../v1/audio/speech 时优先 OpenAI 兼容 speech。 */
|
|
619
|
+
function stabilityGatewayStyle(channel) {
|
|
620
|
+
const url = channel.apiUrl.trim().toLowerCase();
|
|
621
|
+
return /\/v1(\/|$|\?)/.test(url) || /\/audio\/speech(\?|$)/.test(url);
|
|
622
|
+
}
|
|
623
|
+
function isStabilityRouteMiss(status, detail) {
|
|
624
|
+
return status === 404 && /invalid url|invalid_request_error/i.test(detail);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Stable Audio 官方 v2beta(multipart/form-data)。
|
|
628
|
+
* - stable-audio-3 → POST {base}/stable-audio/text-to-audio (202 异步 → GET /v2beta/audio/results/{id} 轮询)
|
|
629
|
+
* - stable-audio-2 / 2.5 → POST {base}/stable-audio-2/text-to-audio (200 同步返回音频/JSON base64)
|
|
630
|
+
* - 不同模型参数不同:stable-audio-3 steps 4-8、duration ≤380;2 steps 30-100、cfg_scale 默认 7;
|
|
631
|
+
* 2.5 steps 4-8、cfg_scale 默认 1;均支持 seed、output_format(hp3|wav)。
|
|
632
|
+
*/
|
|
633
|
+
async function stabilityNativeAudio(channel, request, signal) {
|
|
634
|
+
const rawBase = endpointBase(channel.apiUrl);
|
|
635
|
+
const model = (request.upstream ?? request.model) || "stable-audio-2.5";
|
|
636
|
+
const isV3 = /^stable-audio-3/i.test(model);
|
|
637
|
+
const isV2 = /^stable-audio-2(\.[05])?$/i.test(model) || /^stable-audio-2-/i.test(model);
|
|
638
|
+
const group = isV2 ? "stable-audio-2" : "stable-audio";
|
|
639
|
+
const base = /\/v2beta\/audio$/i.test(rawBase) ? rawBase : /\/v2beta$/i.test(rawBase) ? `${rawBase}/audio` : `${rawBase}/v2beta/audio`;
|
|
640
|
+
const endpoint = `${base}/${group}/text-to-audio`;
|
|
641
|
+
const form = new FormData();
|
|
642
|
+
form.set("prompt", request.prompt);
|
|
643
|
+
form.set("model", model);
|
|
644
|
+
if (request.duration !== void 0 && Number.isFinite(request.duration)) {
|
|
645
|
+
const maxDuration = isV3 ? 380 : 190;
|
|
646
|
+
form.set("duration", String(Math.min(maxDuration, Math.max(1, request.duration))));
|
|
647
|
+
}
|
|
648
|
+
if (request.seed !== void 0 && Number.isFinite(request.seed)) form.set("seed", String(Math.floor(Math.min(4294967294, Math.max(0, request.seed)))));
|
|
649
|
+
const format = request.format === "wav" ? "wav" : "mp3";
|
|
650
|
+
form.set("output_format", format);
|
|
651
|
+
if (request.steps !== void 0 && Number.isInteger(request.steps)) {
|
|
652
|
+
const minSteps = isV2 && !/2\.5/i.test(model) ? 30 : 4;
|
|
653
|
+
const maxSteps = isV2 && !/2\.5/i.test(model) ? 100 : 8;
|
|
654
|
+
form.set("steps", String(Math.min(maxSteps, Math.max(minSteps, request.steps))));
|
|
655
|
+
}
|
|
656
|
+
if (request.cfgScale !== void 0 && Number.isFinite(request.cfgScale)) form.set("cfg_scale", String(Math.min(25, Math.max(1, request.cfgScale))));
|
|
657
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
658
|
+
method: "POST",
|
|
659
|
+
redirect: "error",
|
|
660
|
+
headers: {
|
|
661
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
662
|
+
accept: "application/json"
|
|
663
|
+
},
|
|
664
|
+
body: form,
|
|
665
|
+
signal
|
|
666
|
+
}, isV3 ? 6e4 : UPSTREAM_TIMEOUT_MS);
|
|
667
|
+
if (!response.ok) {
|
|
668
|
+
const detail = await response.text().catch(() => "");
|
|
669
|
+
if (isStabilityRouteMiss(response.status, detail)) throw new StabilityRouteMissError();
|
|
670
|
+
throw new AudioGenError(`Stable Audio API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
671
|
+
}
|
|
672
|
+
if (response.status === 202) {
|
|
673
|
+
const payload = await response.json().catch(() => ({}));
|
|
674
|
+
if (payload.id === void 0 || payload.id === "") throw new AudioGenError("Stable Audio accepted the job but returned no result id", "audio-empty-result");
|
|
675
|
+
const resultUrl = `${base.replace(/\/v2beta\/audio$/i, "")}/v2beta/audio/results/${encodeURIComponent(payload.id)}`;
|
|
676
|
+
const deadline = Date.now() + UPSTREAM_TIMEOUT_MS;
|
|
677
|
+
while (Date.now() < deadline) {
|
|
678
|
+
if (signal?.aborted === true) throw new AudioGenError("Stable Audio generation was aborted", "audio-aborted");
|
|
679
|
+
const polled = await fetchWithTimeout(resultUrl, {
|
|
680
|
+
method: "GET",
|
|
681
|
+
redirect: "error",
|
|
682
|
+
headers: {
|
|
683
|
+
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
684
|
+
accept: "application/json"
|
|
685
|
+
},
|
|
686
|
+
signal
|
|
687
|
+
}, 6e4);
|
|
688
|
+
if (polled.ok) return normalizeAudioResponse(polled, {
|
|
689
|
+
apiKey: channel.apiKey,
|
|
690
|
+
fallbackMime: "audio/mpeg"
|
|
691
|
+
});
|
|
692
|
+
if (polled.status === 404 || polled.status === 202) {
|
|
693
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const detail = await polled.text().catch(() => "");
|
|
697
|
+
throw new AudioGenError(`Stable Audio result API error (HTTP ${polled.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
698
|
+
}
|
|
699
|
+
throw new AudioGenError("Stable Audio generation timed out waiting for the result", "audio-timeout");
|
|
700
|
+
}
|
|
701
|
+
return normalizeAudioResponse(response, {
|
|
702
|
+
apiKey: channel.apiKey,
|
|
703
|
+
fallbackMime: "audio/mpeg"
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Stable Audio 经 OpenAI 兼容网关(如 New API 的 /v1/audio/speech):
|
|
708
|
+
* 模型名映射到 Stable 上游,JSON 体为 {model, input, output_format, duration,
|
|
709
|
+
* seed, steps, cfg_scale} —— 与官方 v2beta 字段一一对应,网关负责转发。
|
|
710
|
+
*/
|
|
711
|
+
async function stabilityGatewayAudio(channel, request, signal) {
|
|
712
|
+
const rawBase = endpointBase(channel.apiUrl);
|
|
713
|
+
const model = (request.upstream ?? request.model) || "stable-audio-2.5";
|
|
714
|
+
const isV3 = /^stable-audio-3/i.test(model);
|
|
715
|
+
const isV2 = /^stable-audio-2(\.[05])?$/i.test(model) || /^stable-audio-2-/i.test(model);
|
|
716
|
+
const endpoint = /\/audio\/speech(\?|$)/i.test(rawBase) ? rawBase : `${rawBase}/audio/speech`;
|
|
717
|
+
const format = request.format === "wav" ? "wav" : "mp3";
|
|
597
718
|
const body = {
|
|
598
|
-
model
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
...request.
|
|
719
|
+
model,
|
|
720
|
+
input: request.prompt,
|
|
721
|
+
output_format: format,
|
|
722
|
+
...request.duration !== void 0 && Number.isFinite(request.duration) ? { duration: Math.min(isV3 ? 380 : 190, Math.max(1, request.duration)) } : {},
|
|
723
|
+
...request.seed !== void 0 && Number.isFinite(request.seed) ? { seed: Math.floor(Math.min(4294967294, Math.max(0, request.seed))) } : {},
|
|
724
|
+
...request.steps !== void 0 && Number.isInteger(request.steps) ? { steps: Math.min(isV2 && !/2\.5/i.test(model) ? 100 : 8, Math.max(isV2 && !/2\.5/i.test(model) ? 30 : 4, request.steps)) } : {},
|
|
725
|
+
...request.cfgScale !== void 0 && Number.isFinite(request.cfgScale) ? { cfg_scale: Math.min(25, Math.max(1, request.cfgScale)) } : {}
|
|
602
726
|
};
|
|
603
|
-
|
|
727
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
604
728
|
method: "POST",
|
|
605
|
-
redirect: "
|
|
729
|
+
redirect: "follow",
|
|
606
730
|
headers: {
|
|
607
731
|
authorization: `Bearer ${channel.apiKey.trim()}`,
|
|
608
|
-
|
|
609
|
-
|
|
732
|
+
accept: "audio/*",
|
|
733
|
+
"content-type": "application/json"
|
|
610
734
|
},
|
|
611
735
|
body: JSON.stringify(body),
|
|
612
736
|
signal
|
|
613
|
-
}, UPSTREAM_TIMEOUT_MS)
|
|
737
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
const detail = await response.text().catch(() => "");
|
|
740
|
+
if (isStabilityRouteMiss(response.status, detail)) throw new StabilityRouteMissError();
|
|
741
|
+
throw new AudioGenError(`Stable Audio gateway API error (HTTP ${response.status})${detail === "" ? "" : `: ${detail.slice(0, 300)}`}`, "audio-api-error");
|
|
742
|
+
}
|
|
743
|
+
return normalizeAudioResponse(response, {
|
|
614
744
|
apiKey: channel.apiKey,
|
|
615
745
|
fallbackMime: "audio/mpeg"
|
|
616
746
|
});
|
|
617
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* 稳定性入口:优先官方 v2beta(api.stability.ai / v2beta 形态),
|
|
750
|
+
* 网关形态(apiUrl 以 /v1 结尾或已含 /audio/speech)优先 OpenAI 兼容;
|
|
751
|
+
* 一方返回 404 Invalid URL(未路由)时自动换另一方重试。
|
|
752
|
+
*/
|
|
753
|
+
async function stabilityAudio(channel, request, signal) {
|
|
754
|
+
const styles = stabilityGatewayStyle(channel) ? ["gateway", "native"] : ["native", "gateway"];
|
|
755
|
+
let lastError;
|
|
756
|
+
for (const style of styles) try {
|
|
757
|
+
if (style === "gateway") return await stabilityGatewayAudio(channel, request, signal);
|
|
758
|
+
return await stabilityNativeAudio(channel, request, signal);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (!(error instanceof StabilityRouteMissError)) throw error;
|
|
761
|
+
lastError = error;
|
|
762
|
+
}
|
|
763
|
+
throw lastError ?? new AudioGenError("Stable Audio 渠道未配置或不可达", "audio-api-error");
|
|
764
|
+
}
|
|
618
765
|
async function genericAudio(channel, request, signal) {
|
|
619
766
|
const base = endpointBase(channel.apiUrl);
|
|
620
767
|
if (request.mode === "tts" && !/\/generate(\?|$)/i.test(base)) return openAITTS(channel, request, signal);
|
|
@@ -653,7 +800,7 @@ async function generateAudio(channel, request, signal) {
|
|
|
653
800
|
if (request.mode === "voice_design" && !isMiniMax$1(channel) && !isElevenLabs$1(channel)) throw new AudioGenError("音色设计当前仅支持 MiniMax(/v1/voice_design)与 ElevenLabs(/v1/text-to-voice/design)渠道", "voice-design-unsupported");
|
|
654
801
|
if (isElevenLabs$1(channel)) return elevenLabs(channel, request, signal);
|
|
655
802
|
if (isMiniMax$1(channel)) return minimax(channel, request, signal);
|
|
656
|
-
if (isStability$1(channel)) return stabilityAudio(channel, request, signal);
|
|
803
|
+
if (isStability$1(channel) || /^stable-audio-/i.test(((request.upstream ?? request.model) || "").trim())) return stabilityAudio(channel, request, signal);
|
|
657
804
|
if (isOpenAICompatible(channel, request.mode)) return openAITTS(channel, request, signal);
|
|
658
805
|
return genericAudio(channel, request, signal);
|
|
659
806
|
}
|
|
@@ -788,16 +935,24 @@ const AUDIO_PRESETS = [
|
|
|
788
935
|
name: "Stability AI(stable-audio)",
|
|
789
936
|
apiUrl: "https://api.stability.ai/v2beta/audio",
|
|
790
937
|
site: "https://stability.ai/stable-audio",
|
|
791
|
-
hint: "Stability AI 音乐 /
|
|
792
|
-
models: [
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
938
|
+
hint: "Stability AI 文本到音频(TTS 描述 / 音乐 / 音效,stable-audio 系列;stable-audio-3 为异步任务)",
|
|
939
|
+
models: [
|
|
940
|
+
{
|
|
941
|
+
alias: "stable-audio-3",
|
|
942
|
+
id: "stable-audio-3",
|
|
943
|
+
category: "music"
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
alias: "stable-audio-2.5",
|
|
947
|
+
id: "stable-audio-2.5",
|
|
948
|
+
category: "music"
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
alias: "stable-audio-2",
|
|
952
|
+
id: "stable-audio-2",
|
|
953
|
+
category: "music"
|
|
954
|
+
}
|
|
955
|
+
]
|
|
801
956
|
}
|
|
802
957
|
];
|
|
803
958
|
/** Look up one built-in provider by id. */
|
|
@@ -995,12 +1150,16 @@ function dedupe(models) {
|
|
|
995
1150
|
/**
|
|
996
1151
|
* Host-side persistence for generated audio and generation history.
|
|
997
1152
|
* Files live under ~/.dsh/dsh-audiogen/audio/; history is one JSON document.
|
|
1153
|
+
* The resource library lives under ~/.dsh/dsh-audiogen/library/ with one
|
|
1154
|
+
* index JSON plus files organized by type (voice/music/sfx/tts) and category.
|
|
998
1155
|
*/
|
|
999
1156
|
function dshHome() {
|
|
1000
1157
|
return process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
|
|
1001
1158
|
}
|
|
1002
1159
|
const AUDIO_DATA_DIR = path.join(dshHome(), "dsh-audiogen", "audio");
|
|
1003
1160
|
const HISTORY_FILE = path.join(dshHome(), "dsh-audiogen", "history.json");
|
|
1161
|
+
const LIBRARY_DATA_DIR = path.join(dshHome(), "dsh-audiogen", "library");
|
|
1162
|
+
const LIBRARY_INDEX_FILE = path.join(LIBRARY_DATA_DIR, "index.json");
|
|
1004
1163
|
async function ensureDir() {
|
|
1005
1164
|
await mkdir(AUDIO_DATA_DIR, { recursive: true });
|
|
1006
1165
|
}
|
|
@@ -1083,7 +1242,8 @@ async function appendHistory(entry) {
|
|
|
1083
1242
|
...audio.voiceId === void 0 ? {} : { voiceId: audio.voiceId }
|
|
1084
1243
|
})),
|
|
1085
1244
|
...entry.channelId === void 0 ? {} : { channelId: entry.channelId },
|
|
1086
|
-
...entry.channel === void 0 ? {} : { channel: entry.channel }
|
|
1245
|
+
...entry.channel === void 0 ? {} : { channel: entry.channel },
|
|
1246
|
+
...entry.params === void 0 ? {} : { params: entry.params }
|
|
1087
1247
|
}, ...list].slice(0, 50);
|
|
1088
1248
|
await writeHistory(next);
|
|
1089
1249
|
return next;
|
|
@@ -1100,6 +1260,205 @@ async function clearHistory() {
|
|
|
1100
1260
|
await writeHistory([]);
|
|
1101
1261
|
return [];
|
|
1102
1262
|
}
|
|
1263
|
+
/** Library type dir names (whitelisted on the audio route too). */
|
|
1264
|
+
const LIBRARY_TYPE_DIRS = {
|
|
1265
|
+
voice: "voice",
|
|
1266
|
+
music: "music",
|
|
1267
|
+
sfx: "sfx",
|
|
1268
|
+
tts: "tts"
|
|
1269
|
+
};
|
|
1270
|
+
/** Sanitize one path segment (cid or voice key). Falls back to 'default'. */
|
|
1271
|
+
function sanitizeSegment(value) {
|
|
1272
|
+
const cleaned = value.replace(/[^a-zA-Z0-9\u4e00-\u9fa5._-]+/g, "_").replace(/^[._-]+|[._-]+$/g, "").slice(0, 60);
|
|
1273
|
+
return cleaned === "" ? "default" : cleaned;
|
|
1274
|
+
}
|
|
1275
|
+
/** Infer the category for a save when the client did not provide one. */
|
|
1276
|
+
function defaultLibraryCategory(type, meta) {
|
|
1277
|
+
if (type === "voice") {
|
|
1278
|
+
const probe = `${meta.voiceId ?? ""} ${meta.voice ?? ""}`.toLowerCase();
|
|
1279
|
+
if (/female|女/.test(probe)) return "female";
|
|
1280
|
+
if (/male|男/.test(probe)) return "male";
|
|
1281
|
+
return "custom";
|
|
1282
|
+
}
|
|
1283
|
+
if (type === "tts") return sanitizeSegment(meta.voice ?? meta.voiceId ?? "default");
|
|
1284
|
+
}
|
|
1285
|
+
/** Default resource name from the prompt. */
|
|
1286
|
+
function defaultLibraryName(prompt) {
|
|
1287
|
+
const flat = prompt.replace(/\s+/g, " ").trim();
|
|
1288
|
+
return flat === "" ? "未命名音频" : flat.length > 40 ? `${flat.slice(0, 40)}…` : flat;
|
|
1289
|
+
}
|
|
1290
|
+
async function readLibraryIndex() {
|
|
1291
|
+
try {
|
|
1292
|
+
const text = await readFile(LIBRARY_INDEX_FILE, "utf8");
|
|
1293
|
+
const parsed = JSON.parse(text);
|
|
1294
|
+
if (!Array.isArray(parsed)) return [];
|
|
1295
|
+
return parsed.filter(isLibraryEntry);
|
|
1296
|
+
} catch {
|
|
1297
|
+
return [];
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function isLibraryEntry(value) {
|
|
1301
|
+
if (value === null || typeof value !== "object") return false;
|
|
1302
|
+
const raw = value;
|
|
1303
|
+
return typeof raw.id === "string" && typeof raw.name === "string" && (raw.type === "voice" || raw.type === "music" || raw.type === "sfx" || raw.type === "tts") && Array.isArray(raw.files) && typeof raw.createdAt === "number" && typeof raw.provenance === "object";
|
|
1304
|
+
}
|
|
1305
|
+
async function writeLibraryIndex(entries) {
|
|
1306
|
+
await mkdir(LIBRARY_DATA_DIR, { recursive: true });
|
|
1307
|
+
await writeFile(LIBRARY_INDEX_FILE, JSON.stringify(entries, null, 2));
|
|
1308
|
+
}
|
|
1309
|
+
/** Same-origin URL for a library-relative file path. */
|
|
1310
|
+
function libraryUrlOf(rel) {
|
|
1311
|
+
return `${LIBRARY_API.audio}/${rel.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
1312
|
+
}
|
|
1313
|
+
/** Merge-library-entry: copy one audio/ file into library/<type>/<category>/. */
|
|
1314
|
+
async function copyIntoLibrary(input, typeDir, category) {
|
|
1315
|
+
const stored = await readAudioFile(input.file);
|
|
1316
|
+
if (stored === void 0) throw new Error(`音频文件不存在:${input.file}(请重新生成后再入库)`);
|
|
1317
|
+
const ext = path.extname(input.file).replace(".", "") || (stored.mime.split("/")[1]?.replace("mpeg", "mp3") ?? "bin");
|
|
1318
|
+
const rel = `${typeDir}/${category}/${input.id}.${ext}`;
|
|
1319
|
+
const target = path.join(LIBRARY_DATA_DIR, ...rel.split("/"));
|
|
1320
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
1321
|
+
await writeFile(target, stored.data);
|
|
1322
|
+
return {
|
|
1323
|
+
url: libraryUrlOf(rel),
|
|
1324
|
+
rel,
|
|
1325
|
+
mime: stored.mime,
|
|
1326
|
+
bytes: stored.bytes,
|
|
1327
|
+
...input.duration === void 0 ? {} : { duration: input.duration },
|
|
1328
|
+
...input.voiceId === void 0 ? {} : { voiceId: input.voiceId }
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Save one curated library entry: copies the referenced audio files into
|
|
1333
|
+
* library/<type>/<category>/ (audio/ files stay untouched) and appends the
|
|
1334
|
+
* entry to the index.
|
|
1335
|
+
*/
|
|
1336
|
+
async function saveToLibrary(input) {
|
|
1337
|
+
if (input.audioFiles.length === 0) throw new Error("没有可入库的音频文件");
|
|
1338
|
+
const typeDir = LIBRARY_TYPE_DIRS[input.type];
|
|
1339
|
+
const category = input.category !== void 0 && input.category.trim() !== "" ? sanitizeSegment(input.category.trim()) : defaultLibraryCategory(input.type, {
|
|
1340
|
+
voice: input.provenance.voice,
|
|
1341
|
+
voiceId: input.provenance.voiceId ?? input.audioFiles.find((file) => file.voiceId !== void 0)?.voiceId
|
|
1342
|
+
}) ?? "default";
|
|
1343
|
+
const files = await Promise.all(input.audioFiles.map((file) => copyIntoLibrary(file, typeDir, category)));
|
|
1344
|
+
const rawName = (input.name ?? "").trim();
|
|
1345
|
+
const entry = {
|
|
1346
|
+
id: randomUUID(),
|
|
1347
|
+
createdAt: Date.now(),
|
|
1348
|
+
type: input.type,
|
|
1349
|
+
category: category === "default" && input.type !== "voice" && input.type !== "tts" ? void 0 : category,
|
|
1350
|
+
name: rawName === "" ? defaultLibraryName(input.provenance.prompt) : rawName,
|
|
1351
|
+
tags: Array.isArray(input.tags) ? [...new Set(input.tags.map((tag) => tag.trim()).filter((tag) => tag !== ""))].slice(0, 20) : [],
|
|
1352
|
+
...input.note !== void 0 && input.note.trim() !== "" ? { note: input.note.trim() } : {},
|
|
1353
|
+
files,
|
|
1354
|
+
provenance: input.provenance
|
|
1355
|
+
};
|
|
1356
|
+
const entries = await readLibraryIndex();
|
|
1357
|
+
entries.unshift(entry);
|
|
1358
|
+
await writeLibraryIndex(entries);
|
|
1359
|
+
return entry;
|
|
1360
|
+
}
|
|
1361
|
+
/** Read library entries (newest first). */
|
|
1362
|
+
async function listLibrary() {
|
|
1363
|
+
return [...await readLibraryIndex()].sort((a, b) => b.createdAt - a.createdAt);
|
|
1364
|
+
}
|
|
1365
|
+
/** Move one library-relative file to a new rel path (same volume rename, else copy). */
|
|
1366
|
+
async function moveLibraryFile(fromRel, toRel) {
|
|
1367
|
+
const from = path.join(LIBRARY_DATA_DIR, ...fromRel.split("/"));
|
|
1368
|
+
const to = path.join(LIBRARY_DATA_DIR, ...toRel.split("/"));
|
|
1369
|
+
await mkdir(path.dirname(to), { recursive: true });
|
|
1370
|
+
try {
|
|
1371
|
+
await rename(from, to);
|
|
1372
|
+
} catch {
|
|
1373
|
+
await writeFile(to, await readFile(from));
|
|
1374
|
+
await unlink(from);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/** Patch name/tags/note/type/category; moving type/category relocates files. */
|
|
1378
|
+
async function updateLibraryEntry(id, patch) {
|
|
1379
|
+
const entries = await readLibraryIndex();
|
|
1380
|
+
const index = entries.findIndex((entry) => entry.id === id);
|
|
1381
|
+
if (index < 0) return void 0;
|
|
1382
|
+
const entry = {
|
|
1383
|
+
...entries[index],
|
|
1384
|
+
files: [...entries[index].files]
|
|
1385
|
+
};
|
|
1386
|
+
if (patch.type !== void 0 && LIBRARY_TYPES_VALID.includes(patch.type)) entry.type = patch.type;
|
|
1387
|
+
if (patch.name !== void 0) entry.name = patch.name.trim() === "" ? defaultLibraryName(entry.provenance.prompt) : patch.name.trim();
|
|
1388
|
+
if (patch.tags !== void 0) entry.tags = [...new Set(patch.tags.map((tag) => tag.trim()).filter((tag) => tag !== ""))].slice(0, 20);
|
|
1389
|
+
if (patch.note !== void 0) entry.note = patch.note.trim() === "" ? void 0 : patch.note.trim();
|
|
1390
|
+
if (patch.category !== void 0 && patch.category.trim() !== "") {
|
|
1391
|
+
const next = sanitizeSegment(patch.category.trim());
|
|
1392
|
+
if (entry.type === "voice" || entry.type === "tts") entry.category = next;
|
|
1393
|
+
}
|
|
1394
|
+
const oldCat = entries[index].category ?? "default";
|
|
1395
|
+
const newCat = entry.category ?? "default";
|
|
1396
|
+
if (entries[index].type !== entry.type || oldCat !== newCat) {
|
|
1397
|
+
const moved = [];
|
|
1398
|
+
for (const file of entry.files) {
|
|
1399
|
+
const fileName = file.rel.split("/").pop() ?? "";
|
|
1400
|
+
const fromRel = `${LIBRARY_TYPE_DIRS[entries[index].type]}/${oldCat}/${fileName}`;
|
|
1401
|
+
const toRel = `${LIBRARY_TYPE_DIRS[entry.type]}/${newCat}/${fileName}`;
|
|
1402
|
+
if (fromRel !== toRel) await moveLibraryFile(fromRel, toRel);
|
|
1403
|
+
moved.push({
|
|
1404
|
+
...file,
|
|
1405
|
+
rel: toRel,
|
|
1406
|
+
url: libraryUrlOf(toRel)
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
entry.files = moved;
|
|
1410
|
+
}
|
|
1411
|
+
entries[index] = entry;
|
|
1412
|
+
await writeLibraryIndex(entries);
|
|
1413
|
+
return entry;
|
|
1414
|
+
}
|
|
1415
|
+
/** Remove entries and their audio files; best-effort prune empty dirs. */
|
|
1416
|
+
async function removeLibraryEntries(ids) {
|
|
1417
|
+
const entries = await readLibraryIndex();
|
|
1418
|
+
const doomed = new Set(ids);
|
|
1419
|
+
const kept = entries.filter((entry) => !doomed.has(entry.id));
|
|
1420
|
+
for (const entry of entries) {
|
|
1421
|
+
if (!doomed.has(entry.id)) continue;
|
|
1422
|
+
for (const file of entry.files) try {
|
|
1423
|
+
await unlink(path.join(LIBRARY_DATA_DIR, ...file.rel.split("/")));
|
|
1424
|
+
} catch {}
|
|
1425
|
+
}
|
|
1426
|
+
for (const entry of entries) {
|
|
1427
|
+
if (!doomed.has(entry.id)) continue;
|
|
1428
|
+
try {
|
|
1429
|
+
await rmdir(path.dirname(path.join(LIBRARY_DATA_DIR, ...entry.files[0].rel.split("/"))), { recursive: false });
|
|
1430
|
+
} catch {}
|
|
1431
|
+
}
|
|
1432
|
+
await writeLibraryIndex(kept);
|
|
1433
|
+
return kept;
|
|
1434
|
+
}
|
|
1435
|
+
/** Read one library file by its rel path (whitelisted, traversal-safe). */
|
|
1436
|
+
async function readLibraryFile(rel) {
|
|
1437
|
+
const segments = rel.split("/").filter((segment) => segment !== "");
|
|
1438
|
+
if (segments.length < 2 || segments.length > 3) return void 0;
|
|
1439
|
+
const [typeDir, category, fileName] = segments;
|
|
1440
|
+
if (typeDir === void 0 || !Object.values(LIBRARY_TYPE_DIRS).includes(typeDir)) return void 0;
|
|
1441
|
+
if (category === void 0 || sanitizeSegment(category) !== category || category.length > 60) return void 0;
|
|
1442
|
+
if (fileName === void 0 || !/^[0-9a-f-]{36}\.[a-z0-9]{2,5}$/i.test(fileName)) return void 0;
|
|
1443
|
+
const full = path.join(LIBRARY_DATA_DIR, typeDir, category, fileName);
|
|
1444
|
+
if (!full.startsWith(path.join(LIBRARY_DATA_DIR, typeDir, category) + path.sep)) return void 0;
|
|
1445
|
+
try {
|
|
1446
|
+
const data = await readFile(full);
|
|
1447
|
+
return {
|
|
1448
|
+
data,
|
|
1449
|
+
mime: mimeFromFile(fileName),
|
|
1450
|
+
bytes: data.byteLength
|
|
1451
|
+
};
|
|
1452
|
+
} catch {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const LIBRARY_TYPES_VALID = [
|
|
1457
|
+
"voice",
|
|
1458
|
+
"music",
|
|
1459
|
+
"sfx",
|
|
1460
|
+
"tts"
|
|
1461
|
+
];
|
|
1103
1462
|
//#endregion
|
|
1104
1463
|
//#region src/routes.ts
|
|
1105
1464
|
const MAX_JSON_BODY_BYTES = 16 * 1024 * 1024;
|
|
@@ -1182,6 +1541,9 @@ function parseGenerateRequest(body) {
|
|
|
1182
1541
|
...typeof body.isInstrumental === "boolean" ? { isInstrumental: body.isInstrumental } : {},
|
|
1183
1542
|
...typeof body.loop === "boolean" ? { loop: body.loop } : {},
|
|
1184
1543
|
...num(body.promptInfluence) !== void 0 ? { promptInfluence: num(body.promptInfluence) } : {},
|
|
1544
|
+
...num(body.seed) !== void 0 ? { seed: num(body.seed) } : {},
|
|
1545
|
+
...num(body.steps) !== void 0 ? { steps: num(body.steps) } : {},
|
|
1546
|
+
...num(body.cfgScale) !== void 0 ? { cfgScale: num(body.cfgScale) } : {},
|
|
1185
1547
|
...typeof body.format === "string" && body.format.trim() !== "" ? { format: body.format.trim() } : {},
|
|
1186
1548
|
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
1187
1549
|
...str(body.emotion) !== void 0 ? { emotion: str(body.emotion) } : {},
|
|
@@ -1198,7 +1560,8 @@ function parseGenerateRequest(body) {
|
|
|
1198
1560
|
...flag(body.aigcWatermark) !== void 0 ? { aigcWatermark: flag(body.aigcWatermark) } : {},
|
|
1199
1561
|
...str(body.languageBoost) !== void 0 ? { languageBoost: str(body.languageBoost) } : {},
|
|
1200
1562
|
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
1201
|
-
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
1563
|
+
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {},
|
|
1564
|
+
...flag(body.saveToLibrary) !== void 0 ? { saveToLibrary: flag(body.saveToLibrary) } : {}
|
|
1202
1565
|
};
|
|
1203
1566
|
}
|
|
1204
1567
|
function toView(descriptor) {
|
|
@@ -1295,6 +1658,60 @@ function resolveChannelRequest(request, view) {
|
|
|
1295
1658
|
}
|
|
1296
1659
|
};
|
|
1297
1660
|
}
|
|
1661
|
+
/** Build the library type from a generation mode (voice_design → voice). */
|
|
1662
|
+
function libraryTypeOf$1(mode) {
|
|
1663
|
+
if (mode === "voice_design") return "voice";
|
|
1664
|
+
return mode;
|
|
1665
|
+
}
|
|
1666
|
+
/** Provenance snapshot straight from a resolved generate request. */
|
|
1667
|
+
function provenanceOf(request, apiUrl) {
|
|
1668
|
+
return {
|
|
1669
|
+
mode: request.mode,
|
|
1670
|
+
prompt: request.prompt,
|
|
1671
|
+
...request.channel === void 0 ? {} : { channel: request.channel },
|
|
1672
|
+
...request.channelId === void 0 ? {} : { channelId: request.channelId },
|
|
1673
|
+
...apiUrl === "" ? {} : { apiUrl },
|
|
1674
|
+
...request.model === void 0 || request.model === "" ? {} : { model: request.model },
|
|
1675
|
+
...request.upstream === void 0 || request.upstream === "" ? {} : { upstream: request.upstream },
|
|
1676
|
+
...request.voice === void 0 ? {} : { voice: request.voice },
|
|
1677
|
+
params: { ...request }
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const strOf = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1681
|
+
const strListOf = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()) : void 0;
|
|
1682
|
+
const parseModeOf = (value) => value === "music" ? "music" : value === "sfx" ? "sfx" : value === "voice_design" ? "voice_design" : "tts";
|
|
1683
|
+
const parseLibraryTypeOf = (value) => LIBRARY_TYPES.includes(value) ? value : void 0;
|
|
1684
|
+
/** File name (audio/ id.ext) from a same-origin audio url. */
|
|
1685
|
+
function historyFileIdOf(url) {
|
|
1686
|
+
try {
|
|
1687
|
+
return decodeURIComponent(new URL(url, "http://localhost").pathname.split("/").pop() ?? "");
|
|
1688
|
+
} catch {
|
|
1689
|
+
return "";
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Fill missing provenance fields from host-persisted history (which carries
|
|
1694
|
+
* the resolved request snapshot) and the channel catalog. Client-supplied
|
|
1695
|
+
* values win when present.
|
|
1696
|
+
*/
|
|
1697
|
+
async function mergeLibraryProvenance(given, files, channels) {
|
|
1698
|
+
const wanted = new Set(files.map((file) => file.file));
|
|
1699
|
+
const entry = (await listHistory()).find((candidate) => candidate.audio.some((audio) => wanted.has(historyFileIdOf(audio.url))));
|
|
1700
|
+
const params = entry?.params !== void 0 && typeof entry.params === "object" ? entry.params : void 0;
|
|
1701
|
+
const channel = channels.find((candidate) => candidate.id === (entry?.channelId ?? ""));
|
|
1702
|
+
return {
|
|
1703
|
+
mode: entry?.mode ?? given.mode,
|
|
1704
|
+
prompt: given.prompt !== "" ? given.prompt : entry?.prompt ?? "",
|
|
1705
|
+
...given.channel !== void 0 || entry?.channel !== void 0 ? { channel: given.channel ?? entry?.channel } : {},
|
|
1706
|
+
...given.channelId !== void 0 || entry?.channelId !== void 0 ? { channelId: given.channelId ?? entry?.channelId } : {},
|
|
1707
|
+
...(given.apiUrl ?? channel?.apiUrl ?? "") === "" ? {} : { apiUrl: given.apiUrl ?? channel?.apiUrl },
|
|
1708
|
+
...given.model !== void 0 || entry?.model !== void 0 ? { model: given.model ?? entry?.model } : {},
|
|
1709
|
+
...(given.upstream ?? (typeof params?.upstream === "string" ? params.upstream : void 0)) !== void 0 ? { upstream: given.upstream ?? (typeof params?.upstream === "string" ? params.upstream : void 0) } : {},
|
|
1710
|
+
...given.voice !== void 0 || entry?.voice !== void 0 ? { voice: given.voice ?? entry?.voice } : {},
|
|
1711
|
+
...given.voiceId !== void 0 || entry?.voiceId !== void 0 ? { voiceId: given.voiceId ?? entry?.voiceId } : {},
|
|
1712
|
+
...given.params !== void 0 || params !== void 0 ? { params: given.params ?? params } : {}
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1298
1715
|
/** Build every /api/dsh-audiogen route. */
|
|
1299
1716
|
function makeRoutes(deps) {
|
|
1300
1717
|
const guard = (req, res, method) => {
|
|
@@ -1455,6 +1872,7 @@ function makeRoutes(deps) {
|
|
|
1455
1872
|
const saved = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`);
|
|
1456
1873
|
generated.push({
|
|
1457
1874
|
id: saved.id,
|
|
1875
|
+
file: saved.file,
|
|
1458
1876
|
b64: Buffer.from(output.data).toString("base64"),
|
|
1459
1877
|
mime: saved.mime,
|
|
1460
1878
|
bytes: saved.bytes,
|
|
@@ -1462,6 +1880,7 @@ function makeRoutes(deps) {
|
|
|
1462
1880
|
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
1463
1881
|
});
|
|
1464
1882
|
}
|
|
1883
|
+
const paramsSnapshot = { ...request };
|
|
1465
1884
|
let history;
|
|
1466
1885
|
try {
|
|
1467
1886
|
history = await appendHistory({
|
|
@@ -1476,7 +1895,8 @@ function makeRoutes(deps) {
|
|
|
1476
1895
|
...request.format === void 0 ? {} : { format: request.format },
|
|
1477
1896
|
audio: generated,
|
|
1478
1897
|
...request.channelId === void 0 ? {} : { channelId: request.channelId },
|
|
1479
|
-
...request.channel === void 0 ? {} : { channel: request.channel }
|
|
1898
|
+
...request.channel === void 0 ? {} : { channel: request.channel },
|
|
1899
|
+
params: paramsSnapshot
|
|
1480
1900
|
});
|
|
1481
1901
|
} catch (error) {
|
|
1482
1902
|
writeJson(res, 200, {
|
|
@@ -1486,10 +1906,30 @@ function makeRoutes(deps) {
|
|
|
1486
1906
|
});
|
|
1487
1907
|
return;
|
|
1488
1908
|
}
|
|
1909
|
+
const wantSave = request.saveToLibrary === true || deps.autoSave() && request.saveToLibrary !== false;
|
|
1910
|
+
let resources;
|
|
1911
|
+
if (wantSave) try {
|
|
1912
|
+
const entry = await saveToLibrary({
|
|
1913
|
+
audioFiles: generated.map((audio) => ({
|
|
1914
|
+
id: audio.id,
|
|
1915
|
+
file: audio.file,
|
|
1916
|
+
mime: audio.mime,
|
|
1917
|
+
...audio.voiceId === void 0 ? {} : { voiceId: audio.voiceId }
|
|
1918
|
+
})),
|
|
1919
|
+
type: libraryTypeOf$1(request.mode),
|
|
1920
|
+
provenance: provenanceOf(request, channel.apiUrl)
|
|
1921
|
+
});
|
|
1922
|
+
resources = [{
|
|
1923
|
+
id: entry.id,
|
|
1924
|
+
name: entry.name,
|
|
1925
|
+
type: entry.type
|
|
1926
|
+
}];
|
|
1927
|
+
} catch {}
|
|
1489
1928
|
writeJson(res, 200, {
|
|
1490
1929
|
ok: true,
|
|
1491
1930
|
outputs: generated,
|
|
1492
|
-
history
|
|
1931
|
+
history,
|
|
1932
|
+
...resources === void 0 ? {} : { resources }
|
|
1493
1933
|
});
|
|
1494
1934
|
} catch (error) {
|
|
1495
1935
|
writeJson(res, 200, {
|
|
@@ -1530,6 +1970,185 @@ function makeRoutes(deps) {
|
|
|
1530
1970
|
res.end(stored.data);
|
|
1531
1971
|
}
|
|
1532
1972
|
},
|
|
1973
|
+
{
|
|
1974
|
+
kind: "exact",
|
|
1975
|
+
path: LIBRARY_API.list,
|
|
1976
|
+
handler: async (req, res) => {
|
|
1977
|
+
if (!guard(req, res, "POST")) return;
|
|
1978
|
+
writeJson(res, 200, {
|
|
1979
|
+
ok: true,
|
|
1980
|
+
entries: await listLibrary()
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
kind: "exact",
|
|
1986
|
+
path: LIBRARY_API.save,
|
|
1987
|
+
handler: async (req, res) => {
|
|
1988
|
+
if (!guard(req, res, "POST")) return;
|
|
1989
|
+
const body = await readJsonBody(req);
|
|
1990
|
+
const audioFiles = Array.isArray(body?.audioFiles) ? body.audioFiles.filter((item) => typeof item === "object" && item !== null).map((item) => ({
|
|
1991
|
+
id: strOf(item.id) ?? "",
|
|
1992
|
+
file: strOf(item.file) ?? "",
|
|
1993
|
+
mime: strOf(item.mime) ?? "audio/mpeg",
|
|
1994
|
+
...strOf(item.voiceId) !== void 0 ? { voiceId: strOf(item.voiceId) } : {},
|
|
1995
|
+
...typeof item.duration === "number" && Number.isFinite(item.duration) ? { duration: item.duration } : {}
|
|
1996
|
+
})).filter((item) => item.id !== "" && item.file !== "") : [];
|
|
1997
|
+
if (audioFiles.length === 0) {
|
|
1998
|
+
writeJson(res, 200, {
|
|
1999
|
+
ok: false,
|
|
2000
|
+
code: "bad-request",
|
|
2001
|
+
message: "没有可入库的音频文件"
|
|
2002
|
+
});
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
const type = parseLibraryTypeOf(body?.type);
|
|
2006
|
+
if (type === void 0) {
|
|
2007
|
+
writeJson(res, 200, {
|
|
2008
|
+
ok: false,
|
|
2009
|
+
code: "bad-request",
|
|
2010
|
+
message: "资源类型无效(voice/music/sfx/tts)"
|
|
2011
|
+
});
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const rawProvenance = typeof body?.provenance === "object" && body.provenance !== null ? body.provenance : {};
|
|
2015
|
+
const provenance = await mergeLibraryProvenance({
|
|
2016
|
+
mode: parseModeOf(rawProvenance.mode),
|
|
2017
|
+
prompt: typeof rawProvenance.prompt === "string" ? rawProvenance.prompt.trim() : "",
|
|
2018
|
+
...strOf(rawProvenance.channel) !== void 0 ? { channel: strOf(rawProvenance.channel) } : {},
|
|
2019
|
+
...strOf(rawProvenance.channelId) !== void 0 ? { channelId: strOf(rawProvenance.channelId) } : {},
|
|
2020
|
+
...strOf(rawProvenance.apiUrl) !== void 0 ? { apiUrl: strOf(rawProvenance.apiUrl) } : {},
|
|
2021
|
+
...strOf(rawProvenance.model) !== void 0 ? { model: strOf(rawProvenance.model) } : {},
|
|
2022
|
+
...strOf(rawProvenance.upstream) !== void 0 ? { upstream: strOf(rawProvenance.upstream) } : {},
|
|
2023
|
+
...strOf(rawProvenance.voice) !== void 0 ? { voice: strOf(rawProvenance.voice) } : {},
|
|
2024
|
+
...strOf(rawProvenance.voiceId) !== void 0 ? { voiceId: strOf(rawProvenance.voiceId) } : {},
|
|
2025
|
+
...typeof rawProvenance.params === "object" && rawProvenance.params !== null ? { params: rawProvenance.params } : {}
|
|
2026
|
+
}, audioFiles, deps.resolveChannels().channels);
|
|
2027
|
+
try {
|
|
2028
|
+
writeJson(res, 200, {
|
|
2029
|
+
ok: true,
|
|
2030
|
+
entry: await saveToLibrary({
|
|
2031
|
+
audioFiles,
|
|
2032
|
+
type,
|
|
2033
|
+
...strOf(body?.category) !== void 0 ? { category: strOf(body?.category) } : {},
|
|
2034
|
+
...strOf(body?.name) !== void 0 ? { name: strOf(body?.name) } : {},
|
|
2035
|
+
...strListOf(body?.tags) !== void 0 ? { tags: strListOf(body?.tags) } : {},
|
|
2036
|
+
...strOf(body?.note) !== void 0 ? { note: strOf(body?.note) } : {},
|
|
2037
|
+
provenance
|
|
2038
|
+
})
|
|
2039
|
+
});
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
writeJson(res, 200, {
|
|
2042
|
+
ok: false,
|
|
2043
|
+
code: "library-save-failed",
|
|
2044
|
+
message: messageOf(error)
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
kind: "exact",
|
|
2051
|
+
path: LIBRARY_API.update,
|
|
2052
|
+
handler: async (req, res) => {
|
|
2053
|
+
if (!guard(req, res, "POST")) return;
|
|
2054
|
+
const body = await readJsonBody(req);
|
|
2055
|
+
const id = strOf(body?.id);
|
|
2056
|
+
if (id === void 0) {
|
|
2057
|
+
writeJson(res, 200, {
|
|
2058
|
+
ok: false,
|
|
2059
|
+
code: "bad-request",
|
|
2060
|
+
message: "缺少资源 id"
|
|
2061
|
+
});
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const entry = await updateLibraryEntry(id, {
|
|
2066
|
+
...strOf(body?.name) !== void 0 ? { name: strOf(body?.name) } : {},
|
|
2067
|
+
...strListOf(body?.tags) !== void 0 ? { tags: strListOf(body?.tags) } : {},
|
|
2068
|
+
...typeof body?.note === "string" ? { note: body.note } : {},
|
|
2069
|
+
...strOf(body?.category) !== void 0 ? { category: strOf(body?.category) } : {},
|
|
2070
|
+
...parseLibraryTypeOf(body?.type) !== void 0 ? { type: parseLibraryTypeOf(body?.type) } : {}
|
|
2071
|
+
});
|
|
2072
|
+
if (entry === void 0) {
|
|
2073
|
+
writeJson(res, 200, {
|
|
2074
|
+
ok: false,
|
|
2075
|
+
code: "not-found",
|
|
2076
|
+
message: "资源不存在"
|
|
2077
|
+
});
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
writeJson(res, 200, {
|
|
2081
|
+
ok: true,
|
|
2082
|
+
entry
|
|
2083
|
+
});
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
writeJson(res, 200, {
|
|
2086
|
+
ok: false,
|
|
2087
|
+
code: "library-update-failed",
|
|
2088
|
+
message: messageOf(error)
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
kind: "exact",
|
|
2095
|
+
path: LIBRARY_API.remove,
|
|
2096
|
+
handler: async (req, res) => {
|
|
2097
|
+
if (!guard(req, res, "POST")) return;
|
|
2098
|
+
const body = await readJsonBody(req);
|
|
2099
|
+
const ids = strListOf(body?.ids) ?? [];
|
|
2100
|
+
if (ids.length === 0) {
|
|
2101
|
+
writeJson(res, 200, {
|
|
2102
|
+
ok: false,
|
|
2103
|
+
code: "bad-request",
|
|
2104
|
+
message: "缺少资源 id"
|
|
2105
|
+
});
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
try {
|
|
2109
|
+
writeJson(res, 200, {
|
|
2110
|
+
ok: true,
|
|
2111
|
+
entries: await removeLibraryEntries(ids)
|
|
2112
|
+
});
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
writeJson(res, 200, {
|
|
2115
|
+
ok: false,
|
|
2116
|
+
code: "library-remove-failed",
|
|
2117
|
+
message: messageOf(error)
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
kind: "prefix",
|
|
2124
|
+
path: LIBRARY_API.audio,
|
|
2125
|
+
handler: async (req, res) => {
|
|
2126
|
+
if (!isLoopbackRequest(req)) {
|
|
2127
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
if (req.method !== "GET") {
|
|
2131
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
const rel = audioFileFrom(req.url, LIBRARY_API.audio);
|
|
2135
|
+
if (rel === void 0) {
|
|
2136
|
+
writeJson(res, 400, { error: "invalid library audio" });
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const stored = await readLibraryFile(rel);
|
|
2140
|
+
if (stored === void 0) {
|
|
2141
|
+
writeJson(res, 404, { error: "library audio not found" });
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
res.writeHead(200, {
|
|
2145
|
+
"content-type": stored.mime,
|
|
2146
|
+
"content-length": stored.bytes,
|
|
2147
|
+
"cache-control": "private, max-age=3600"
|
|
2148
|
+
});
|
|
2149
|
+
res.end(stored.data);
|
|
2150
|
+
}
|
|
2151
|
+
},
|
|
1533
2152
|
{
|
|
1534
2153
|
kind: "exact",
|
|
1535
2154
|
path: HISTORY_API.list,
|
|
@@ -1598,6 +2217,29 @@ function makeRoutes(deps) {
|
|
|
1598
2217
|
}
|
|
1599
2218
|
//#endregion
|
|
1600
2219
|
//#region src/agent-audio-tools.ts
|
|
2220
|
+
const audioRefSchema = {
|
|
2221
|
+
type: "object",
|
|
2222
|
+
additionalProperties: false,
|
|
2223
|
+
properties: {
|
|
2224
|
+
id: {
|
|
2225
|
+
type: "string",
|
|
2226
|
+
required: true
|
|
2227
|
+
},
|
|
2228
|
+
url: {
|
|
2229
|
+
type: "string",
|
|
2230
|
+
required: true
|
|
2231
|
+
},
|
|
2232
|
+
mime: {
|
|
2233
|
+
type: "string",
|
|
2234
|
+
required: true
|
|
2235
|
+
},
|
|
2236
|
+
bytes: {
|
|
2237
|
+
type: "integer",
|
|
2238
|
+
required: true
|
|
2239
|
+
},
|
|
2240
|
+
voiceId: { type: "string" }
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
1601
2243
|
const resultSchema = {
|
|
1602
2244
|
type: "object",
|
|
1603
2245
|
additionalProperties: false,
|
|
@@ -1627,27 +2269,32 @@ const resultSchema = {
|
|
|
1627
2269
|
audio: {
|
|
1628
2270
|
type: "array",
|
|
1629
2271
|
required: true,
|
|
2272
|
+
items: audioRefSchema
|
|
2273
|
+
},
|
|
2274
|
+
resources: {
|
|
2275
|
+
type: "array",
|
|
2276
|
+
items: { type: "string" }
|
|
2277
|
+
},
|
|
2278
|
+
groups: {
|
|
2279
|
+
type: "array",
|
|
1630
2280
|
items: {
|
|
1631
2281
|
type: "object",
|
|
1632
2282
|
additionalProperties: false,
|
|
1633
2283
|
properties: {
|
|
1634
|
-
|
|
1635
|
-
type: "string",
|
|
1636
|
-
required: true
|
|
1637
|
-
},
|
|
1638
|
-
url: {
|
|
2284
|
+
model: {
|
|
1639
2285
|
type: "string",
|
|
1640
2286
|
required: true
|
|
1641
2287
|
},
|
|
1642
|
-
|
|
1643
|
-
type: "
|
|
1644
|
-
required: true
|
|
2288
|
+
audio: {
|
|
2289
|
+
type: "array",
|
|
2290
|
+
required: true,
|
|
2291
|
+
items: audioRefSchema
|
|
1645
2292
|
},
|
|
1646
|
-
|
|
1647
|
-
type: "
|
|
1648
|
-
|
|
2293
|
+
resources: {
|
|
2294
|
+
type: "array",
|
|
2295
|
+
items: { type: "string" }
|
|
1649
2296
|
},
|
|
1650
|
-
|
|
2297
|
+
error: { type: "string" }
|
|
1651
2298
|
}
|
|
1652
2299
|
}
|
|
1653
2300
|
},
|
|
@@ -1681,9 +2328,15 @@ function ensureConfigured(config) {
|
|
|
1681
2328
|
if (!config.allowAgentAudioGeneration) throw new AudioGenError("Agent audio generation is disabled in Settings > Plugins > AI Audio.", "agent-generation-disabled");
|
|
1682
2329
|
if (!config.channels.some((channel) => channel.apiUrl.trim() !== "" && channel.apiKey.trim() !== "")) throw new AudioGenError("Audio API credentials are not configured. Open Settings > Plugins > AI Audio, add a channel and fill its API URL and API key.", "audio-api-not-configured");
|
|
1683
2330
|
}
|
|
2331
|
+
/** Library type from the generation mode, with an explicit override. */
|
|
2332
|
+
function libraryTypeOf(mode, override) {
|
|
2333
|
+
if (override === "voice" || override === "music" || override === "sfx" || override === "tts") return override;
|
|
2334
|
+
if (mode === "voice_design") return "voice";
|
|
2335
|
+
return mode;
|
|
2336
|
+
}
|
|
1684
2337
|
/** Register the Agent audio tool. */
|
|
1685
2338
|
function registerAgentAudioTools(ctx, resolve) {
|
|
1686
|
-
|
|
2339
|
+
const disposer = ctx.tools.register(defineTool({
|
|
1687
2340
|
name: "generate_audio",
|
|
1688
2341
|
description: "Generate audio with the configured audio provider. Supports text-to-speech, music generation, sound effects and voice design (MiniMax /v1/voice_design, ElevenLabs /v1/text-to-voice/design). The tool call waits for the upstream result and returns same-origin audio URLs; pass those URLs to the user for playback or download. If multiple models are configured, first ask the user which one to use or pass model explicitly.",
|
|
1689
2342
|
parameters: {
|
|
@@ -1706,6 +2359,16 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1706
2359
|
type: "string",
|
|
1707
2360
|
description: "One of the configured audio models/voices. Defaults to the first configured model."
|
|
1708
2361
|
},
|
|
2362
|
+
models: {
|
|
2363
|
+
type: "array",
|
|
2364
|
+
items: { type: "string" },
|
|
2365
|
+
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."
|
|
2366
|
+
},
|
|
2367
|
+
model_params: {
|
|
2368
|
+
type: "object",
|
|
2369
|
+
additionalProperties: true,
|
|
2370
|
+
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."
|
|
2371
|
+
},
|
|
1709
2372
|
voice: {
|
|
1710
2373
|
type: "string",
|
|
1711
2374
|
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."
|
|
@@ -1738,6 +2401,18 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1738
2401
|
type: "number",
|
|
1739
2402
|
description: "Sound effect prompt influence 0-1 (ElevenLabs prompt_influence, default 0.3): higher follows the prompt more closely, lower is more variable."
|
|
1740
2403
|
},
|
|
2404
|
+
seed: {
|
|
2405
|
+
type: "integer",
|
|
2406
|
+
description: "Stable Audio random seed 0-4294967294 (default 0 = random); same seed yields reproducible audio."
|
|
2407
|
+
},
|
|
2408
|
+
steps: {
|
|
2409
|
+
type: "integer",
|
|
2410
|
+
description: "Stable Audio sampling steps, model-dependent: stable-audio-2 30-100, stable-audio-2.5/3 4-8 (out-of-range auto-clamped)."
|
|
2411
|
+
},
|
|
2412
|
+
cfg_scale: {
|
|
2413
|
+
type: "number",
|
|
2414
|
+
description: "Stable Audio prompt adherence 1-25 (stable-audio-2 default 7, 2.5/3 default 1); higher follows the prompt more strictly."
|
|
2415
|
+
},
|
|
1741
2416
|
format: {
|
|
1742
2417
|
type: "string",
|
|
1743
2418
|
description: "Output format such as mp3 or wav. MiniMax music supports mp3/wav/pcm."
|
|
@@ -1824,12 +2499,40 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1824
2499
|
type: "object",
|
|
1825
2500
|
additionalProperties: false,
|
|
1826
2501
|
properties: {
|
|
1827
|
-
voice_id: {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2502
|
+
voice_id: {
|
|
2503
|
+
type: "string",
|
|
2504
|
+
required: true
|
|
2505
|
+
},
|
|
2506
|
+
weight: {
|
|
2507
|
+
type: "integer",
|
|
2508
|
+
required: true
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
1831
2511
|
},
|
|
1832
2512
|
description: "MiniMax TTS dual-voice blend weights (timbre_weights)."
|
|
2513
|
+
},
|
|
2514
|
+
save_to_library: {
|
|
2515
|
+
type: "boolean",
|
|
2516
|
+
description: "Save the generated audio into the local resource library after success. Also enabled globally by the \"auto save to library\" setting; pass false to skip a single run."
|
|
2517
|
+
},
|
|
2518
|
+
library_name: {
|
|
2519
|
+
type: "string",
|
|
2520
|
+
description: "Resource name in the library. Defaults to the prompt."
|
|
2521
|
+
},
|
|
2522
|
+
library_type: {
|
|
2523
|
+
type: "string",
|
|
2524
|
+
enum: [
|
|
2525
|
+
"voice",
|
|
2526
|
+
"music",
|
|
2527
|
+
"sfx",
|
|
2528
|
+
"tts"
|
|
2529
|
+
],
|
|
2530
|
+
description: "Resource type in the library. Defaults to the generation mode (voice_design → voice)."
|
|
2531
|
+
},
|
|
2532
|
+
library_tags: {
|
|
2533
|
+
type: "array",
|
|
2534
|
+
items: { type: "string" },
|
|
2535
|
+
description: "Tags for the library resource."
|
|
1833
2536
|
}
|
|
1834
2537
|
},
|
|
1835
2538
|
output: {
|
|
@@ -1842,117 +2545,356 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
1842
2545
|
const config = resolve();
|
|
1843
2546
|
ensureConfigured(config);
|
|
1844
2547
|
const mode = args.mode === "music" ? "music" : args.mode === "sfx" ? "sfx" : args.mode === "voice_design" ? "voice_design" : "tts";
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const
|
|
1848
|
-
|
|
2548
|
+
/** 把生成参数(snake_case 入参或 model_params 片段)映射为请求字段。 */
|
|
2549
|
+
const mapParams = (raw) => {
|
|
2550
|
+
const voiceModify = typeof raw.voice_modify === "object" && raw.voice_modify !== null ? (() => {
|
|
2551
|
+
const src = raw.voice_modify;
|
|
2552
|
+
const out = {};
|
|
2553
|
+
if (typeof src.pitch === "number") out.pitch = src.pitch;
|
|
2554
|
+
if (typeof src.intensity === "number") out.intensity = src.intensity;
|
|
2555
|
+
if (typeof src.timbre === "number") out.timbre = src.timbre;
|
|
2556
|
+
if (typeof src.sound_effects === "string" && src.sound_effects.trim() !== "") out.soundEffects = src.sound_effects.trim();
|
|
2557
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2558
|
+
})() : void 0;
|
|
2559
|
+
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) => ({
|
|
2560
|
+
voiceId: item.voice_id.trim(),
|
|
2561
|
+
weight: item.weight
|
|
2562
|
+
})).filter((item) => item.voiceId !== "") : void 0;
|
|
2563
|
+
const stringOrEmpty = (key) => {
|
|
2564
|
+
const value = raw[key];
|
|
2565
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
2566
|
+
};
|
|
2567
|
+
const finiteOrUndefined = (key) => {
|
|
2568
|
+
const value = raw[key];
|
|
2569
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2570
|
+
};
|
|
1849
2571
|
return {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2572
|
+
...stringOrEmpty("voice") !== void 0 ? { voice: stringOrEmpty("voice") } : {},
|
|
2573
|
+
...stringOrEmpty("preview_text") !== void 0 ? { previewText: stringOrEmpty("preview_text") } : {},
|
|
2574
|
+
...finiteOrUndefined("speed") !== void 0 ? { speed: finiteOrUndefined("speed") } : {},
|
|
2575
|
+
...finiteOrUndefined("duration") !== void 0 ? { duration: finiteOrUndefined("duration") } : {},
|
|
2576
|
+
...stringOrEmpty("lyrics") !== void 0 ? { lyrics: stringOrEmpty("lyrics") } : {},
|
|
2577
|
+
...typeof raw.is_instrumental === "boolean" ? { isInstrumental: raw.is_instrumental } : {},
|
|
2578
|
+
...typeof raw.loop === "boolean" ? { loop: raw.loop } : {},
|
|
2579
|
+
...finiteOrUndefined("prompt_influence") !== void 0 ? { promptInfluence: finiteOrUndefined("prompt_influence") } : {},
|
|
2580
|
+
...finiteOrUndefined("seed") !== void 0 ? { seed: finiteOrUndefined("seed") } : {},
|
|
2581
|
+
...finiteOrUndefined("steps") !== void 0 ? { steps: finiteOrUndefined("steps") } : {},
|
|
2582
|
+
...finiteOrUndefined("cfg_scale") !== void 0 ? { cfgScale: finiteOrUndefined("cfg_scale") } : {},
|
|
2583
|
+
...stringOrEmpty("format") !== void 0 ? { format: stringOrEmpty("format") } : {},
|
|
2584
|
+
...stringOrEmpty("emotion") !== void 0 ? { emotion: stringOrEmpty("emotion") } : {},
|
|
2585
|
+
...finiteOrUndefined("vol") !== void 0 ? { vol: finiteOrUndefined("vol") } : {},
|
|
2586
|
+
...finiteOrUndefined("pitch") !== void 0 ? { pitch: finiteOrUndefined("pitch") } : {},
|
|
2587
|
+
...typeof raw.text_normalization === "boolean" ? { textNormalization: raw.text_normalization } : {},
|
|
2588
|
+
...typeof raw.latex_read === "boolean" ? { latexRead: raw.latex_read } : {},
|
|
2589
|
+
...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()) } : {},
|
|
2590
|
+
...finiteOrUndefined("sample_rate") !== void 0 ? { sampleRate: finiteOrUndefined("sample_rate") } : {},
|
|
2591
|
+
...finiteOrUndefined("bitrate") !== void 0 ? { bitrate: finiteOrUndefined("bitrate") } : {},
|
|
2592
|
+
...finiteOrUndefined("channel") !== void 0 ? { audioChannel: finiteOrUndefined("channel") } : {},
|
|
2593
|
+
...typeof raw.force_cbr === "boolean" ? { forceCbr: raw.force_cbr } : {},
|
|
2594
|
+
...typeof raw.subtitle_enable === "boolean" ? { subtitleEnable: raw.subtitle_enable } : {},
|
|
2595
|
+
...typeof raw.aigc_watermark === "boolean" ? { aigcWatermark: raw.aigc_watermark } : {},
|
|
2596
|
+
...stringOrEmpty("language_boost") !== void 0 ? { languageBoost: stringOrEmpty("language_boost") } : {},
|
|
2597
|
+
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
2598
|
+
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
1853
2599
|
};
|
|
1854
|
-
})() : resolveModel(config, args.model);
|
|
1855
|
-
const voiceModify = typeof args.voice_modify === "object" && args.voice_modify !== null ? (() => {
|
|
1856
|
-
const raw = args.voice_modify;
|
|
1857
|
-
const out = {};
|
|
1858
|
-
if (typeof raw.pitch === "number") out.pitch = raw.pitch;
|
|
1859
|
-
if (typeof raw.intensity === "number") out.intensity = raw.intensity;
|
|
1860
|
-
if (typeof raw.timbre === "number") out.timbre = raw.timbre;
|
|
1861
|
-
if (typeof raw.sound_effects === "string" && raw.sound_effects.trim() !== "") out.soundEffects = raw.sound_effects.trim();
|
|
1862
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
1863
|
-
})() : void 0;
|
|
1864
|
-
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) => ({
|
|
1865
|
-
voiceId: item.voice_id.trim(),
|
|
1866
|
-
weight: item.weight
|
|
1867
|
-
})).filter((item) => item.voiceId !== "") : void 0;
|
|
1868
|
-
const request = {
|
|
1869
|
-
mode,
|
|
1870
|
-
model: picked.alias,
|
|
1871
|
-
upstream: picked.upstream,
|
|
1872
|
-
channelId: picked.channel.id,
|
|
1873
|
-
channel: picked.channel.name,
|
|
1874
|
-
prompt: args.prompt.trim(),
|
|
1875
|
-
...typeof args.voice === "string" && args.voice.trim() !== "" ? { voice: args.voice.trim() } : {},
|
|
1876
|
-
...typeof args.preview_text === "string" && args.preview_text.trim() !== "" ? { previewText: args.preview_text.trim() } : {},
|
|
1877
|
-
...typeof args.speed === "number" ? { speed: args.speed } : {},
|
|
1878
|
-
...typeof args.duration === "number" ? { duration: args.duration } : {},
|
|
1879
|
-
...typeof args.lyrics === "string" && args.lyrics.trim() !== "" ? { lyrics: args.lyrics.trim() } : {},
|
|
1880
|
-
...typeof args.is_instrumental === "boolean" ? { isInstrumental: args.is_instrumental } : {},
|
|
1881
|
-
...typeof args.loop === "boolean" ? { loop: args.loop } : {},
|
|
1882
|
-
...typeof args.prompt_influence === "number" && Number.isFinite(args.prompt_influence) ? { promptInfluence: args.prompt_influence } : {},
|
|
1883
|
-
...typeof args.format === "string" && args.format.trim() !== "" ? { format: args.format.trim() } : {},
|
|
1884
|
-
...typeof args.emotion === "string" && args.emotion.trim() !== "" ? { emotion: args.emotion.trim() } : {},
|
|
1885
|
-
...typeof args.vol === "number" && Number.isFinite(args.vol) ? { vol: args.vol } : {},
|
|
1886
|
-
...typeof args.pitch === "number" && Number.isFinite(args.pitch) ? { pitch: args.pitch } : {},
|
|
1887
|
-
...typeof args.text_normalization === "boolean" ? { textNormalization: args.text_normalization } : {},
|
|
1888
|
-
...typeof args.latex_read === "boolean" ? { latexRead: args.latex_read } : {},
|
|
1889
|
-
...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()) } : {},
|
|
1890
|
-
...typeof args.sample_rate === "number" && Number.isFinite(args.sample_rate) ? { sampleRate: args.sample_rate } : {},
|
|
1891
|
-
...typeof args.bitrate === "number" && Number.isFinite(args.bitrate) ? { bitrate: args.bitrate } : {},
|
|
1892
|
-
...typeof args.channel === "number" && Number.isFinite(args.channel) ? { audioChannel: args.channel } : {},
|
|
1893
|
-
...typeof args.force_cbr === "boolean" ? { forceCbr: args.force_cbr } : {},
|
|
1894
|
-
...typeof args.subtitle_enable === "boolean" ? { subtitleEnable: args.subtitle_enable } : {},
|
|
1895
|
-
...typeof args.aigc_watermark === "boolean" ? { aigcWatermark: args.aigc_watermark } : {},
|
|
1896
|
-
...typeof args.language_boost === "string" && args.language_boost.trim() !== "" ? { languageBoost: args.language_boost.trim() } : {},
|
|
1897
|
-
...voiceModify !== void 0 ? { voiceModify } : {},
|
|
1898
|
-
...timbreWeights !== void 0 && timbreWeights.length > 0 ? { timbreWeights } : {}
|
|
1899
2600
|
};
|
|
1900
|
-
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
id: saved.id,
|
|
1907
|
-
url: `/api/dsh-audiogen/audio/${encodeURIComponent(saved.file)}`,
|
|
1908
|
-
mime: saved.mime,
|
|
1909
|
-
bytes: saved.bytes,
|
|
1910
|
-
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
1911
|
-
});
|
|
2601
|
+
const buildRequest = (picked) => {
|
|
2602
|
+
const base = mapParams(args);
|
|
2603
|
+
let override = {};
|
|
2604
|
+
if (typeof args.model_params === "object" && args.model_params !== null) {
|
|
2605
|
+
const perModel = args.model_params[picked.alias];
|
|
2606
|
+
if (typeof perModel === "object" && perModel !== null) override = mapParams(perModel);
|
|
1912
2607
|
}
|
|
2608
|
+
return {
|
|
2609
|
+
mode,
|
|
2610
|
+
model: picked.alias,
|
|
2611
|
+
upstream: picked.upstream,
|
|
2612
|
+
channelId: picked.channel.id,
|
|
2613
|
+
channel: picked.channel.name,
|
|
2614
|
+
prompt: typeof args.prompt === "string" ? args.prompt.trim() : "",
|
|
2615
|
+
...base,
|
|
2616
|
+
...override
|
|
2617
|
+
};
|
|
2618
|
+
};
|
|
2619
|
+
/** 单模型执行:生成 + 保存文件 + 历史 + 可选资源库;错误收敛为分组结果。 */
|
|
2620
|
+
const runOne = async (picked) => {
|
|
2621
|
+
const request = buildRequest(picked);
|
|
1913
2622
|
try {
|
|
1914
|
-
await
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
id: audio[index].id,
|
|
1926
|
-
b64: Buffer.from(output.data).toString("base64"),
|
|
1927
|
-
mime: audio[index].mime,
|
|
1928
|
-
bytes: audio[index].bytes,
|
|
1929
|
-
url: audio[index].url,
|
|
2623
|
+
const outputs = await generateAudio(picked.channel, request, exec.signal);
|
|
2624
|
+
const audio = [];
|
|
2625
|
+
const saved = [];
|
|
2626
|
+
for (const [index, output] of outputs.entries()) {
|
|
2627
|
+
const stored = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`);
|
|
2628
|
+
saved.push({
|
|
2629
|
+
id: stored.id,
|
|
2630
|
+
url: `/api/dsh-audiogen/audio/${encodeURIComponent(stored.file)}`,
|
|
2631
|
+
file: stored.file,
|
|
2632
|
+
mime: stored.mime,
|
|
2633
|
+
bytes: stored.bytes,
|
|
1930
2634
|
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
1931
|
-
})
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2635
|
+
});
|
|
2636
|
+
audio.push({
|
|
2637
|
+
id: stored.id,
|
|
2638
|
+
url: `/api/dsh-audiogen/audio/${encodeURIComponent(stored.file)}`,
|
|
2639
|
+
mime: stored.mime,
|
|
2640
|
+
bytes: stored.bytes,
|
|
2641
|
+
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
try {
|
|
2645
|
+
await appendHistory({
|
|
2646
|
+
id: randomUUID(),
|
|
2647
|
+
createdAt: Date.now(),
|
|
2648
|
+
mode: request.mode,
|
|
2649
|
+
model: picked.alias,
|
|
2650
|
+
prompt: request.prompt,
|
|
2651
|
+
...request.voice === void 0 ? {} : { voice: request.voice },
|
|
2652
|
+
...request.speed === void 0 ? {} : { speed: request.speed },
|
|
2653
|
+
...request.duration === void 0 ? {} : { duration: request.duration },
|
|
2654
|
+
...request.format === void 0 ? {} : { format: request.format },
|
|
2655
|
+
audio: outputs.map((output, index) => ({
|
|
2656
|
+
id: saved[index].id,
|
|
2657
|
+
file: saved[index].file,
|
|
2658
|
+
b64: Buffer.from(output.data).toString("base64"),
|
|
2659
|
+
mime: saved[index].mime,
|
|
2660
|
+
bytes: saved[index].bytes,
|
|
2661
|
+
url: saved[index].url,
|
|
2662
|
+
...output.voiceId === void 0 ? {} : { voiceId: output.voiceId }
|
|
2663
|
+
})),
|
|
2664
|
+
channelId: picked.channel.id,
|
|
2665
|
+
channel: picked.channel.name,
|
|
2666
|
+
params: { ...request }
|
|
2667
|
+
});
|
|
2668
|
+
} catch {}
|
|
2669
|
+
const wantSave = args.save_to_library === true || config.autoSaveToLibrary && args.save_to_library !== false;
|
|
2670
|
+
let resources;
|
|
2671
|
+
if (wantSave) try {
|
|
2672
|
+
resources = [(await saveToLibrary({
|
|
2673
|
+
audioFiles: saved.map((item) => ({
|
|
2674
|
+
id: item.id,
|
|
2675
|
+
file: item.file,
|
|
2676
|
+
mime: item.mime,
|
|
2677
|
+
...item.voiceId === void 0 ? {} : { voiceId: item.voiceId }
|
|
2678
|
+
})),
|
|
2679
|
+
type: libraryTypeOf(request.mode, args.library_type),
|
|
2680
|
+
...typeof args.library_name === "string" && args.library_name.trim() !== "" ? { name: args.library_name.trim() } : {},
|
|
2681
|
+
...Array.isArray(args.library_tags) ? { tags: args.library_tags.filter((tag) => typeof tag === "string" && tag.trim() !== "").map((tag) => tag.trim()) } : {},
|
|
2682
|
+
provenance: {
|
|
2683
|
+
mode: request.mode,
|
|
2684
|
+
prompt: request.prompt,
|
|
2685
|
+
channel: picked.channel.name,
|
|
2686
|
+
channelId: picked.channel.id,
|
|
2687
|
+
apiUrl: picked.channel.apiUrl,
|
|
2688
|
+
model: picked.alias,
|
|
2689
|
+
upstream: picked.upstream,
|
|
2690
|
+
...request.voice === void 0 ? {} : { voice: request.voice },
|
|
2691
|
+
params: { ...request }
|
|
2692
|
+
}
|
|
2693
|
+
})).id];
|
|
2694
|
+
} catch {}
|
|
2695
|
+
return {
|
|
2696
|
+
model: picked.alias,
|
|
2697
|
+
audio,
|
|
2698
|
+
...resources === void 0 ? {} : { resources }
|
|
2699
|
+
};
|
|
2700
|
+
} catch (error) {
|
|
2701
|
+
if (exec.signal?.aborted === true) throw error;
|
|
2702
|
+
return {
|
|
2703
|
+
model: picked.alias,
|
|
2704
|
+
audio: [],
|
|
2705
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
const requestedModels = Array.isArray(args.models) ? [...new Set(args.models.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()))] : [];
|
|
2710
|
+
if (requestedModels.length > 0 && mode !== "voice_design") {
|
|
2711
|
+
const groups = [];
|
|
2712
|
+
let succeeded = 0;
|
|
2713
|
+
for (const alias of requestedModels) {
|
|
2714
|
+
let picked;
|
|
2715
|
+
try {
|
|
2716
|
+
picked = resolveModel(config, alias);
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
groups.push({
|
|
2719
|
+
model: alias,
|
|
2720
|
+
audio: [],
|
|
2721
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2722
|
+
});
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
const group = await runOne(picked);
|
|
2726
|
+
groups.push(group);
|
|
2727
|
+
if (group.error === void 0) succeeded++;
|
|
2728
|
+
}
|
|
1936
2729
|
return {
|
|
1937
|
-
status: "completed",
|
|
1938
|
-
message:
|
|
1939
|
-
mode
|
|
1940
|
-
model:
|
|
1941
|
-
audio
|
|
2730
|
+
status: succeeded > 0 ? "completed" : "failed",
|
|
2731
|
+
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.",
|
|
2732
|
+
mode,
|
|
2733
|
+
model: groups[0]?.model ?? requestedModels[0],
|
|
2734
|
+
audio: groups.flatMap((group) => group.audio),
|
|
2735
|
+
groups,
|
|
2736
|
+
...succeeded === 0 ? { error: groups.map((group) => `${group.model}: ${group.error ?? ""}`).filter((item) => !item.endsWith(": ")).join(";") } : {}
|
|
1942
2737
|
};
|
|
1943
|
-
}
|
|
1944
|
-
|
|
2738
|
+
}
|
|
2739
|
+
const one = await runOne(mode === "voice_design" ? (() => {
|
|
2740
|
+
const usable = config.channels.filter((channel) => channel.apiUrl.trim() !== "" && channel.apiKey.trim() !== "");
|
|
2741
|
+
const target = usable.find((channel) => channel.id === config.defaultChannelId) ?? usable[0];
|
|
2742
|
+
if (target === void 0) throw new AudioGenError("No usable audio channel is configured for voice design.", "no-channel-available");
|
|
1945
2743
|
return {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
model: picked.alias,
|
|
1950
|
-
audio: [],
|
|
1951
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2744
|
+
channel: target,
|
|
2745
|
+
alias: "",
|
|
2746
|
+
upstream: ""
|
|
1952
2747
|
};
|
|
2748
|
+
})() : resolveModel(config, args.model));
|
|
2749
|
+
if (one.error !== void 0) return {
|
|
2750
|
+
status: "failed",
|
|
2751
|
+
message: "Audio generation failed.",
|
|
2752
|
+
mode,
|
|
2753
|
+
model: one.model,
|
|
2754
|
+
audio: [],
|
|
2755
|
+
error: one.error
|
|
2756
|
+
};
|
|
2757
|
+
return {
|
|
2758
|
+
status: "completed",
|
|
2759
|
+
message: "Audio generation completed. The audio files can be played/downloaded from the returned URLs.",
|
|
2760
|
+
mode,
|
|
2761
|
+
model: one.model,
|
|
2762
|
+
audio: one.audio,
|
|
2763
|
+
...one.resources === void 0 ? {} : { resources: one.resources }
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
}));
|
|
2767
|
+
const searchDisposer = ctx.tools.register(defineTool({
|
|
2768
|
+
name: "search_audio_library",
|
|
2769
|
+
description: "Search curated audio resources in the local resource library (voice / music / sfx / tts). Returns matching resources with type, category, name, tags, full provenance (channel, model, voiceId, prompt) and same-origin audio URLs the user can play. Use it before generating to reuse an existing voice, music bed or sound effect instead of generating a new one.",
|
|
2770
|
+
parameters: {
|
|
2771
|
+
type: {
|
|
2772
|
+
type: "string",
|
|
2773
|
+
enum: [
|
|
2774
|
+
"voice",
|
|
2775
|
+
"music",
|
|
2776
|
+
"sfx",
|
|
2777
|
+
"tts"
|
|
2778
|
+
],
|
|
2779
|
+
description: "Filter by resource type."
|
|
2780
|
+
},
|
|
2781
|
+
category: {
|
|
2782
|
+
type: "string",
|
|
2783
|
+
description: "Filter by category (voice: male/female/custom; tts: the speaking voice key)."
|
|
2784
|
+
},
|
|
2785
|
+
keyword: {
|
|
2786
|
+
type: "string",
|
|
2787
|
+
description: "Search name, tags, prompt and model."
|
|
1953
2788
|
}
|
|
2789
|
+
},
|
|
2790
|
+
output: {
|
|
2791
|
+
schema: {
|
|
2792
|
+
type: "object",
|
|
2793
|
+
additionalProperties: false,
|
|
2794
|
+
properties: {
|
|
2795
|
+
status: {
|
|
2796
|
+
type: "string",
|
|
2797
|
+
required: true,
|
|
2798
|
+
enum: ["ok"]
|
|
2799
|
+
},
|
|
2800
|
+
count: {
|
|
2801
|
+
type: "integer",
|
|
2802
|
+
required: true
|
|
2803
|
+
},
|
|
2804
|
+
entries: {
|
|
2805
|
+
type: "array",
|
|
2806
|
+
required: true,
|
|
2807
|
+
items: {
|
|
2808
|
+
type: "object",
|
|
2809
|
+
additionalProperties: false,
|
|
2810
|
+
properties: {
|
|
2811
|
+
id: {
|
|
2812
|
+
type: "string",
|
|
2813
|
+
required: true
|
|
2814
|
+
},
|
|
2815
|
+
name: {
|
|
2816
|
+
type: "string",
|
|
2817
|
+
required: true
|
|
2818
|
+
},
|
|
2819
|
+
type: {
|
|
2820
|
+
type: "string",
|
|
2821
|
+
required: true,
|
|
2822
|
+
enum: [
|
|
2823
|
+
"voice",
|
|
2824
|
+
"music",
|
|
2825
|
+
"sfx",
|
|
2826
|
+
"tts"
|
|
2827
|
+
]
|
|
2828
|
+
},
|
|
2829
|
+
category: { type: "string" },
|
|
2830
|
+
tags: {
|
|
2831
|
+
type: "array",
|
|
2832
|
+
items: { type: "string" },
|
|
2833
|
+
required: true
|
|
2834
|
+
},
|
|
2835
|
+
prompt: {
|
|
2836
|
+
type: "string",
|
|
2837
|
+
required: true
|
|
2838
|
+
},
|
|
2839
|
+
model: { type: "string" },
|
|
2840
|
+
channel: { type: "string" },
|
|
2841
|
+
voiceId: { type: "string" },
|
|
2842
|
+
urls: {
|
|
2843
|
+
type: "array",
|
|
2844
|
+
items: { type: "string" },
|
|
2845
|
+
required: true
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
},
|
|
2852
|
+
render: (_args, value) => [{
|
|
2853
|
+
type: "text",
|
|
2854
|
+
text: JSON.stringify(value)
|
|
2855
|
+
}]
|
|
2856
|
+
},
|
|
2857
|
+
isConcurrencySafe: () => true,
|
|
2858
|
+
async execute(args) {
|
|
2859
|
+
const keyword = typeof args.keyword === "string" ? args.keyword.trim().toLowerCase() : "";
|
|
2860
|
+
const wantedType = args.type === "voice" || args.type === "music" || args.type === "sfx" || args.type === "tts" ? args.type : void 0;
|
|
2861
|
+
const wantedCategory = typeof args.category === "string" && args.category.trim() !== "" ? args.category.trim() : void 0;
|
|
2862
|
+
const entries = (await listLibrary()).filter((entry) => {
|
|
2863
|
+
if (wantedType !== void 0 && entry.type !== wantedType) return false;
|
|
2864
|
+
if (wantedCategory !== void 0 && (entry.category ?? "") !== wantedCategory) return false;
|
|
2865
|
+
if (keyword !== "") {
|
|
2866
|
+
if (![
|
|
2867
|
+
entry.name,
|
|
2868
|
+
...entry.tags,
|
|
2869
|
+
entry.provenance.prompt,
|
|
2870
|
+
entry.provenance.model ?? "",
|
|
2871
|
+
entry.provenance.channel ?? ""
|
|
2872
|
+
].join(" ").toLowerCase().includes(keyword)) return false;
|
|
2873
|
+
}
|
|
2874
|
+
return true;
|
|
2875
|
+
}).slice(0, 30).map((entry) => ({
|
|
2876
|
+
id: entry.id,
|
|
2877
|
+
name: entry.name,
|
|
2878
|
+
type: entry.type,
|
|
2879
|
+
...entry.category === void 0 ? {} : { category: entry.category },
|
|
2880
|
+
tags: entry.tags,
|
|
2881
|
+
prompt: entry.provenance.prompt,
|
|
2882
|
+
...entry.provenance.model === void 0 ? {} : { model: entry.provenance.model },
|
|
2883
|
+
...entry.provenance.channel === void 0 ? {} : { channel: entry.provenance.channel },
|
|
2884
|
+
...entry.provenance.voiceId === void 0 ? {} : { voiceId: entry.provenance.voiceId },
|
|
2885
|
+
urls: entry.files.map((file) => file.url)
|
|
2886
|
+
}));
|
|
2887
|
+
return {
|
|
2888
|
+
status: "ok",
|
|
2889
|
+
count: entries.length,
|
|
2890
|
+
entries
|
|
2891
|
+
};
|
|
1954
2892
|
}
|
|
1955
2893
|
}));
|
|
2894
|
+
return () => {
|
|
2895
|
+
disposer();
|
|
2896
|
+
searchDisposer();
|
|
2897
|
+
};
|
|
1956
2898
|
}
|
|
1957
2899
|
//#endregion
|
|
1958
2900
|
//#region src/index.ts
|
|
@@ -1978,7 +2920,8 @@ const Config = z.object({
|
|
|
1978
2920
|
})).default([]),
|
|
1979
2921
|
channelSecrets: z.dict(z.string().role("secret")).default({}),
|
|
1980
2922
|
defaultChannelId: z.string().default(""),
|
|
1981
|
-
defaultModel: z.string().default("")
|
|
2923
|
+
defaultModel: z.string().default(""),
|
|
2924
|
+
autoSaveToLibrary: z.boolean().default(false)
|
|
1982
2925
|
});
|
|
1983
2926
|
const DEFAULT_ENABLED = true;
|
|
1984
2927
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -1996,6 +2939,29 @@ function guidanceFor(channels, defaultChannelId) {
|
|
|
1996
2939
|
}).join(";");
|
|
1997
2940
|
return `${AUDIOGEN_GUIDANCE} 当前渠道与模型:${table}。`;
|
|
1998
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* 把随包分发的技能(skills/<id>/SKILL.md,含 frontmatter)同步到 DSH 用户技能根
|
|
2944
|
+
* `~/.dsh/skills/<id>/SKILL.md` —— DSH web 会话的 skill-filesystem(standard 等
|
|
2945
|
+
* preset 行)会扫描用户根,使会话可直接触发这些技能。仅创建缺失文件,绝不覆盖
|
|
2946
|
+
* 用户已有内容;任何失败仅告警,不影响插件本身。
|
|
2947
|
+
*/
|
|
2948
|
+
function syncBundledSkills() {
|
|
2949
|
+
try {
|
|
2950
|
+
const sourceRoot = join(dirname(dirname(fileURLToPath(import.meta.url))), "skills");
|
|
2951
|
+
if (existsSync(sourceRoot) !== true) return;
|
|
2952
|
+
const targetRoot = join(process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh"), "skills");
|
|
2953
|
+
for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
2954
|
+
if (entry.isDirectory() !== true) continue;
|
|
2955
|
+
const sourceFile = join(sourceRoot, entry.name, "SKILL.md");
|
|
2956
|
+
if (existsSync(sourceFile) !== true) continue;
|
|
2957
|
+
const targetDir = join(targetRoot, entry.name);
|
|
2958
|
+
const targetFile = join(targetDir, "SKILL.md");
|
|
2959
|
+
if (existsSync(targetFile)) continue;
|
|
2960
|
+
mkdirSync(targetDir, { recursive: true });
|
|
2961
|
+
copyFileSync(sourceFile, targetFile);
|
|
2962
|
+
}
|
|
2963
|
+
} catch {}
|
|
2964
|
+
}
|
|
1999
2965
|
function normalizeChannels(value) {
|
|
2000
2966
|
if (!Array.isArray(value)) return [];
|
|
2001
2967
|
const out = [];
|
|
@@ -2027,6 +2993,7 @@ function normalizeChannels(value) {
|
|
|
2027
2993
|
return out;
|
|
2028
2994
|
}
|
|
2029
2995
|
function apply(ctx, config) {
|
|
2996
|
+
syncBundledSkills();
|
|
2030
2997
|
let current = () => config ?? {};
|
|
2031
2998
|
const resolve = () => {
|
|
2032
2999
|
const value = current() ?? {};
|
|
@@ -2046,7 +3013,8 @@ function apply(ctx, config) {
|
|
|
2046
3013
|
apiKey: typeof secrets[channel.id] === "string" ? secrets[channel.id] : ""
|
|
2047
3014
|
})),
|
|
2048
3015
|
defaultChannelId,
|
|
2049
|
-
defaultModel: typeof value.defaultModel === "string" ? value.defaultModel.trim() : ""
|
|
3016
|
+
defaultModel: typeof value.defaultModel === "string" ? value.defaultModel.trim() : "",
|
|
3017
|
+
autoSaveToLibrary: value.autoSaveToLibrary === true
|
|
2050
3018
|
};
|
|
2051
3019
|
};
|
|
2052
3020
|
const channelsView = () => {
|
|
@@ -2061,7 +3029,8 @@ function apply(ctx, config) {
|
|
|
2061
3029
|
sctx.effect(() => {
|
|
2062
3030
|
const disposers = makeRoutes({
|
|
2063
3031
|
settings: seam,
|
|
2064
|
-
resolveChannels: channelsView
|
|
3032
|
+
resolveChannels: channelsView,
|
|
3033
|
+
autoSave: () => resolve().autoSaveToLibrary
|
|
2065
3034
|
}).map((route) => ctx.webServer.register(route));
|
|
2066
3035
|
return () => {
|
|
2067
3036
|
for (const dispose of disposers) dispose();
|
|
@@ -2075,7 +3044,8 @@ function apply(ctx, config) {
|
|
|
2075
3044
|
enabled: value.enabled,
|
|
2076
3045
|
allowAgentAudioGeneration: value.allowAgentAudioGeneration,
|
|
2077
3046
|
channels: value.channels,
|
|
2078
|
-
defaultChannelId: value.defaultChannelId
|
|
3047
|
+
defaultChannelId: value.defaultChannelId,
|
|
3048
|
+
autoSaveToLibrary: value.autoSaveToLibrary
|
|
2079
3049
|
};
|
|
2080
3050
|
}), "dsh-audiogen: agent audio tools");
|
|
2081
3051
|
});
|