dsh-audiogen 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/lib/client.js +1669 -987
- package/lib/client.js.map +1 -1
- package/lib/index.js +124 -6
- package/package.json +1 -1
- package/src/agent-audio-tools.ts +11 -1
- package/src/audio-scheduler.ts +73 -0
- package/src/client/SettingsCard.tsx +18 -0
- package/src/client/api.ts +24 -25
- package/src/client/audio-panel.module.css +241 -0
- package/src/client/field-specs.ts +186 -0
- package/src/client/library-view.tsx +6 -1
- package/src/client/locales.ts +2 -0
- package/src/client/settings-scope.ts +18 -3
- package/src/client/studio-view.tsx +729 -390
- package/src/index.ts +14 -0
- package/src/protocol.ts +10 -1
- package/src/routes.ts +50 -2
package/lib/index.js
CHANGED
|
@@ -22,6 +22,8 @@ const SETTINGS_API = {
|
|
|
22
22
|
};
|
|
23
23
|
/** The audio-generation proxy route. */
|
|
24
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" };
|
|
25
27
|
/** Host-mediated built-in provider catalog (channels the user can instantiate). */
|
|
26
28
|
const PRESETS_API = "/api/dsh-audiogen/presets";
|
|
27
29
|
/** Host-mediated model/voice discovery endpoint. */
|
|
@@ -52,6 +54,63 @@ const LIBRARY_TYPES = [
|
|
|
52
54
|
"tts"
|
|
53
55
|
];
|
|
54
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
|
|
55
114
|
//#region src/audio-engine.ts
|
|
56
115
|
/** An audio generation failure with a user-presentable message. */
|
|
57
116
|
var AudioGenError = class extends Error {
|
|
@@ -1462,6 +1521,8 @@ const LIBRARY_TYPES_VALID = [
|
|
|
1462
1521
|
//#endregion
|
|
1463
1522
|
//#region src/routes.ts
|
|
1464
1523
|
const MAX_JSON_BODY_BYTES = 16 * 1024 * 1024;
|
|
1524
|
+
/** 宿主侧任务取消注册表:taskId → 该任务当前在途请求的 AbortController 集合。 */
|
|
1525
|
+
const taskAborts = /* @__PURE__ */ new Map();
|
|
1465
1526
|
function isLoopbackRequest(request) {
|
|
1466
1527
|
const address = request.socket.remoteAddress;
|
|
1467
1528
|
if (address !== "127.0.0.1" && address !== "::1" && address !== "::ffff:127.0.0.1") return false;
|
|
@@ -1865,8 +1926,21 @@ function makeRoutes(deps) {
|
|
|
1865
1926
|
}
|
|
1866
1927
|
const request = resolved.request;
|
|
1867
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
|
+
}
|
|
1868
1936
|
try {
|
|
1869
|
-
const
|
|
1937
|
+
const release = await deps.budget.acquire(controller.signal);
|
|
1938
|
+
let outputs;
|
|
1939
|
+
try {
|
|
1940
|
+
outputs = await generateAudio(channel, request, controller.signal);
|
|
1941
|
+
} finally {
|
|
1942
|
+
release();
|
|
1943
|
+
}
|
|
1870
1944
|
const generated = [];
|
|
1871
1945
|
for (const [index, output] of outputs.entries()) {
|
|
1872
1946
|
const saved = await saveAudioFile(output.data, output.mime, `generated-${index + 1}`);
|
|
@@ -1937,7 +2011,39 @@ function makeRoutes(deps) {
|
|
|
1937
2011
|
code: error instanceof AudioGenError ? error.code : "generate-failed",
|
|
1938
2012
|
message: messageOf(error)
|
|
1939
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;
|
|
1940
2037
|
}
|
|
2038
|
+
const controllers = taskAborts.get(taskId);
|
|
2039
|
+
if (controllers !== void 0) {
|
|
2040
|
+
for (const controller of controllers) controller.abort();
|
|
2041
|
+
taskAborts.delete(taskId);
|
|
2042
|
+
}
|
|
2043
|
+
writeJson(res, 200, {
|
|
2044
|
+
ok: true,
|
|
2045
|
+
aborted: controllers !== void 0 ? controllers.size : 0
|
|
2046
|
+
});
|
|
1941
2047
|
}
|
|
1942
2048
|
},
|
|
1943
2049
|
{
|
|
@@ -2620,7 +2726,13 @@ function registerAgentAudioTools(ctx, resolve) {
|
|
|
2620
2726
|
const runOne = async (picked) => {
|
|
2621
2727
|
const request = buildRequest(picked);
|
|
2622
2728
|
try {
|
|
2623
|
-
const
|
|
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
|
+
}
|
|
2624
2736
|
const audio = [];
|
|
2625
2737
|
const saved = [];
|
|
2626
2738
|
for (const [index, output] of outputs.entries()) {
|
|
@@ -2904,6 +3016,7 @@ const name = "audiogen";
|
|
|
2904
3016
|
const inject = ["webServer", "systemPrompt"];
|
|
2905
3017
|
/** The branded settings namespace of this plugin. */
|
|
2906
3018
|
const AudioGenSettingsNamespace = settingsNamespace(AUDIOGEN_SETTINGS_NAMESPACE);
|
|
3019
|
+
const DEFAULT_MAX_CONCURRENT = 5;
|
|
2907
3020
|
const Config = z.object({
|
|
2908
3021
|
enabled: z.boolean().default(true),
|
|
2909
3022
|
announceToAgent: z.boolean().default(true),
|
|
@@ -2921,7 +3034,8 @@ const Config = z.object({
|
|
|
2921
3034
|
channelSecrets: z.dict(z.string().role("secret")).default({}),
|
|
2922
3035
|
defaultChannelId: z.string().default(""),
|
|
2923
3036
|
defaultModel: z.string().default(""),
|
|
2924
|
-
autoSaveToLibrary: z.boolean().default(false)
|
|
3037
|
+
autoSaveToLibrary: z.boolean().default(false),
|
|
3038
|
+
maxConcurrentGenerations: z.number().default(DEFAULT_MAX_CONCURRENT)
|
|
2925
3039
|
});
|
|
2926
3040
|
const DEFAULT_ENABLED = true;
|
|
2927
3041
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -3014,9 +3128,11 @@ function apply(ctx, config) {
|
|
|
3014
3128
|
})),
|
|
3015
3129
|
defaultChannelId,
|
|
3016
3130
|
defaultModel: typeof value.defaultModel === "string" ? value.defaultModel.trim() : "",
|
|
3017
|
-
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
|
|
3018
3133
|
};
|
|
3019
3134
|
};
|
|
3135
|
+
const budget = createGenerationBudget(() => resolve().maxConcurrentGenerations);
|
|
3020
3136
|
const channelsView = () => {
|
|
3021
3137
|
const value = resolve();
|
|
3022
3138
|
return {
|
|
@@ -3030,7 +3146,8 @@ function apply(ctx, config) {
|
|
|
3030
3146
|
const disposers = makeRoutes({
|
|
3031
3147
|
settings: seam,
|
|
3032
3148
|
resolveChannels: channelsView,
|
|
3033
|
-
autoSave: () => resolve().autoSaveToLibrary
|
|
3149
|
+
autoSave: () => resolve().autoSaveToLibrary,
|
|
3150
|
+
budget
|
|
3034
3151
|
}).map((route) => ctx.webServer.register(route));
|
|
3035
3152
|
return () => {
|
|
3036
3153
|
for (const dispose of disposers) dispose();
|
|
@@ -3045,7 +3162,8 @@ function apply(ctx, config) {
|
|
|
3045
3162
|
allowAgentAudioGeneration: value.allowAgentAudioGeneration,
|
|
3046
3163
|
channels: value.channels,
|
|
3047
3164
|
defaultChannelId: value.defaultChannelId,
|
|
3048
|
-
autoSaveToLibrary: value.autoSaveToLibrary
|
|
3165
|
+
autoSaveToLibrary: value.autoSaveToLibrary,
|
|
3166
|
+
budget
|
|
3049
3167
|
};
|
|
3050
3168
|
}), "dsh-audiogen: agent audio tools");
|
|
3051
3169
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-audiogen",
|
|
3
3
|
"description": "AI audio generation plugin for the dsh web GUI: multi-vendor TTS/music/sound-effect channels (OpenAI-compatible, ElevenLabs, MiniMax, Stability AI and custom), per-channel model/voice catalogs, Agent tool and a sidebar AI 音频 panel.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/agent-audio-tools.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
9
9
|
import { randomUUID } from 'node:crypto'
|
|
10
10
|
import type { AudioChannel } from './audio-engine.ts'
|
|
11
11
|
import { generateAudio, AudioGenError } from './audio-engine.ts'
|
|
12
|
+
import type { GenerationBudget } from './audio-scheduler.ts'
|
|
12
13
|
import { appendHistory, saveAudioFile, saveToLibrary, listLibrary } from './audio-store.ts'
|
|
13
14
|
import type { AudioMode, GenerateAudioRequest, LibraryType } from './protocol.ts'
|
|
14
15
|
|
|
@@ -18,6 +19,8 @@ export interface AgentAudioToolConfig {
|
|
|
18
19
|
channels: AudioChannel[]
|
|
19
20
|
defaultChannelId: string
|
|
20
21
|
autoSaveToLibrary: boolean
|
|
22
|
+
/** 全局并发闸门(与面板路由共享「最大并发生成数」)。 */
|
|
23
|
+
budget?: GenerationBudget
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
interface AgentAudioRef {
|
|
@@ -300,7 +303,14 @@ export function registerAgentAudioTools(ctx: Context, resolve: () => AgentAudioT
|
|
|
300
303
|
const runOne = async (picked: { channel: AudioChannel; alias: string; upstream: string }): Promise<AgentAudioGroup> => {
|
|
301
304
|
const request = buildRequest(picked)
|
|
302
305
|
try {
|
|
303
|
-
|
|
306
|
+
// 与面板路由共享全局并发闸门(限流时排队;取消时立即出队)。
|
|
307
|
+
const release = await (config.budget?.acquire(exec.signal) ?? Promise.resolve(() => { /* 默认不限制 */ }))
|
|
308
|
+
let outputs
|
|
309
|
+
try {
|
|
310
|
+
outputs = await generateAudio(picked.channel, request, exec.signal)
|
|
311
|
+
} finally {
|
|
312
|
+
release()
|
|
313
|
+
}
|
|
304
314
|
const audio: AgentAudioRef[] = []
|
|
305
315
|
const saved: SavedAudioRef[] = []
|
|
306
316
|
for (const [index, output] of outputs.entries()) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global generation budget: a FIFO semaphore shared by every upstream audio
|
|
3
|
+
* call (panel routes and the Agent tool). The limit comes from the plugin
|
|
4
|
+
* setting 「最大并发生成数」(default 5): a 3-model compare task takes 3 slots,
|
|
5
|
+
* other tasks queue or run concurrently up to the same cap.
|
|
6
|
+
*
|
|
7
|
+
* Acquire resolves with a release function once a slot is free; aborting the
|
|
8
|
+
* signal while queued rejects immediately (no slot is occupied).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface GenerationBudget {
|
|
12
|
+
/** Wait for a free slot; resolves with the release function. */
|
|
13
|
+
acquire(signal?: AbortSignal): Promise<() => void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createGenerationBudget(limit: () => number): GenerationBudget {
|
|
17
|
+
let active = 0
|
|
18
|
+
const waiting: Array<{
|
|
19
|
+
resolve: (release: () => void) => void
|
|
20
|
+
reject: (reason: unknown) => void
|
|
21
|
+
signal?: AbortSignal
|
|
22
|
+
cleanup?: () => void
|
|
23
|
+
}> = []
|
|
24
|
+
|
|
25
|
+
const clampLimit = (): number => {
|
|
26
|
+
const raw = Number(limit())
|
|
27
|
+
if (!Number.isFinite(raw) || raw < 1) return 5
|
|
28
|
+
return Math.min(20, Math.floor(raw))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pump = (): void => {
|
|
32
|
+
const max = clampLimit()
|
|
33
|
+
while (active < max && waiting.length > 0) {
|
|
34
|
+
const entry = waiting.shift()!
|
|
35
|
+
if (entry.signal?.aborted === true) {
|
|
36
|
+
entry.reject(new DOMException('The operation was aborted.', 'AbortError'))
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
entry.cleanup?.()
|
|
40
|
+
active += 1
|
|
41
|
+
let released = false
|
|
42
|
+
entry.resolve(() => {
|
|
43
|
+
if (released) return
|
|
44
|
+
released = true
|
|
45
|
+
active = Math.max(0, active - 1)
|
|
46
|
+
pump()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const acquire = (signal?: AbortSignal): Promise<() => void> => new Promise<() => void>((resolve, reject) => {
|
|
52
|
+
const entry: { resolve: (release: () => void) => void; reject: (reason: unknown) => void; signal?: AbortSignal; cleanup?: () => void } = { resolve, reject, signal }
|
|
53
|
+
const onAbort = (): void => {
|
|
54
|
+
const index = waiting.indexOf(entry)
|
|
55
|
+
if (index < 0) return // 已在运行:由调用方的 signal 中断上游请求
|
|
56
|
+
waiting.splice(index, 1)
|
|
57
|
+
entry.cleanup = undefined
|
|
58
|
+
reject(new DOMException('The operation was aborted.', 'AbortError'))
|
|
59
|
+
}
|
|
60
|
+
entry.cleanup = () => { signal?.removeEventListener('abort', onAbort) }
|
|
61
|
+
if (signal !== undefined) {
|
|
62
|
+
if (signal.aborted === true) {
|
|
63
|
+
reject(new DOMException('The operation was aborted.', 'AbortError'))
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
67
|
+
}
|
|
68
|
+
waiting.push(entry)
|
|
69
|
+
pump()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return { acquire }
|
|
73
|
+
}
|
|
@@ -28,6 +28,7 @@ export interface AudioGenSettings {
|
|
|
28
28
|
allowAgentAudioGeneration?: boolean
|
|
29
29
|
defaultModel?: string
|
|
30
30
|
autoSaveToLibrary?: boolean
|
|
31
|
+
maxConcurrentGenerations?: number
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface AudioGenSettingsCardState extends CardShell {
|
|
@@ -37,6 +38,7 @@ export interface AudioGenSettingsCardState extends CardShell {
|
|
|
37
38
|
allowAgentAudioGeneration: CardFieldState
|
|
38
39
|
defaultModel: CardFieldState
|
|
39
40
|
autoSaveToLibrary: CardFieldState
|
|
41
|
+
maxConcurrentGenerations: CardFieldState
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface AudioGenSettingsCardFace extends CardActions {
|
|
@@ -57,6 +59,7 @@ export class AudioGenSettingsCardController {
|
|
|
57
59
|
booleanField('allowAgentAudioGeneration'),
|
|
58
60
|
textField('defaultModel'),
|
|
59
61
|
booleanField('autoSaveToLibrary'),
|
|
62
|
+
textField('maxConcurrentGenerations'),
|
|
60
63
|
])
|
|
61
64
|
this.channelsForm = new ChannelsForm(scope)
|
|
62
65
|
}
|
|
@@ -72,6 +75,7 @@ export class AudioGenSettingsCardController {
|
|
|
72
75
|
allowAgentAudioGeneration: this.form.field('allowAgentAudioGeneration'),
|
|
73
76
|
defaultModel: this.form.field('defaultModel'),
|
|
74
77
|
autoSaveToLibrary: this.form.field('autoSaveToLibrary'),
|
|
78
|
+
maxConcurrentGenerations: this.form.field('maxConcurrentGenerations'),
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -723,6 +727,20 @@ export function AudioGenSettingsCard(props: AudioGenSettingsCardProps) {
|
|
|
723
727
|
<input type="checkbox" checked={state.autoSaveToLibrary.text === 'true'} disabled={!state.writable} onChange={event => props.edit('autoSaveToLibrary', String(event.target.checked))} /> {t('settings.autoSaveLibrary')}
|
|
724
728
|
</label>
|
|
725
729
|
</div>
|
|
730
|
+
<div className={css.field}>
|
|
731
|
+
<label className={css.label}>
|
|
732
|
+
<span>{t('settings.maxConcurrent')}</span>
|
|
733
|
+
<input
|
|
734
|
+
type="number"
|
|
735
|
+
min="1"
|
|
736
|
+
max="20"
|
|
737
|
+
className={css.input}
|
|
738
|
+
value={state.maxConcurrentGenerations.text}
|
|
739
|
+
disabled={!state.writable}
|
|
740
|
+
onChange={event => props.edit('maxConcurrentGenerations', event.target.value)}
|
|
741
|
+
/>
|
|
742
|
+
</label>
|
|
743
|
+
</div>
|
|
726
744
|
<div className={css.footer}>
|
|
727
745
|
{state.failed ? <p className={css.failed}>保存失败</p> : null}
|
|
728
746
|
<button type="button" className={css.discard} disabled={!state.dirty || state.saving} onClick={() => props.discard()}>{t('settings.discard')}</button>
|
package/src/client/api.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
GENERATE_API, HISTORY_API, LIBRARY_API,
|
|
7
|
+
GENERATE_API, HISTORY_API, LIBRARY_API, TASK_API,
|
|
8
8
|
type GenerateAudioRequest, type GeneratedAudio, type HistoryEntry,
|
|
9
9
|
type LibraryEntry, type LibrarySaveRequest, type LibraryUpdateRequest,
|
|
10
10
|
} from '../protocol.ts'
|
|
@@ -20,59 +20,58 @@ export interface GenerateResponse {
|
|
|
20
20
|
message?: string
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** POST helper: the host API requires the JSON content type on every POST. */
|
|
24
|
+
function postJson(path: string, body: unknown, signal?: AbortSignal): Promise<Response> {
|
|
25
|
+
return fetch(path, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'content-type': 'application/json' },
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
...(signal === undefined ? {} : { signal }),
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export class AudiogenApi {
|
|
24
|
-
async generate(request: GenerateAudioRequest): Promise<GenerateResponse> {
|
|
25
|
-
const response = await
|
|
26
|
-
method: 'POST',
|
|
27
|
-
headers: { 'content-type': 'application/json' },
|
|
28
|
-
body: JSON.stringify(request),
|
|
29
|
-
})
|
|
34
|
+
async generate(request: GenerateAudioRequest, signal?: AbortSignal): Promise<GenerateResponse> {
|
|
35
|
+
const response = await postJson(GENERATE_API, { ...request, taskId: request.taskId }, signal)
|
|
30
36
|
const body = await response.json() as GenerateResponse
|
|
31
37
|
return body
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/** 取消进行中的任务:宿主侧中断全部在途上游调用,剩余模型跳过。 */
|
|
41
|
+
async cancelTask(taskId: string): Promise<void> {
|
|
42
|
+
await postJson(TASK_API.cancel, { taskId }).catch(() => { /* best-effort */ })
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
async history(): Promise<HistoryEntry[]> {
|
|
35
|
-
const response = await
|
|
46
|
+
const response = await postJson(HISTORY_API.list, {})
|
|
36
47
|
const body = await response.json() as { ok?: boolean; history?: HistoryEntry[] }
|
|
37
48
|
return body.ok === true ? (body.history ?? []) : []
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
async clearHistory(): Promise<void> {
|
|
41
|
-
await
|
|
52
|
+
await postJson(HISTORY_API.clear, {})
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
async libraryList(): Promise<LibraryEntry[]> {
|
|
45
|
-
const response = await
|
|
56
|
+
const response = await postJson(LIBRARY_API.list, {})
|
|
46
57
|
const body = await response.json() as { ok?: boolean; entries?: LibraryEntry[] }
|
|
47
58
|
return body.ok === true ? (body.entries ?? []) : []
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
async librarySave(request: LibrarySaveRequest): Promise<{ ok: boolean; entry?: LibraryEntry; message?: string }> {
|
|
51
|
-
const response = await
|
|
52
|
-
method: 'POST',
|
|
53
|
-
headers: { 'content-type': 'application/json' },
|
|
54
|
-
body: JSON.stringify(request),
|
|
55
|
-
})
|
|
62
|
+
const response = await postJson(LIBRARY_API.save, request)
|
|
56
63
|
const body = await response.json() as { ok?: boolean; entry?: LibraryEntry; message?: string }
|
|
57
64
|
return { ok: body.ok === true, ...(body.entry === undefined ? {} : { entry: body.entry }), ...(body.message === undefined ? {} : { message: body.message }) }
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
async libraryUpdate(request: LibraryUpdateRequest): Promise<{ ok: boolean; entry?: LibraryEntry; message?: string }> {
|
|
61
|
-
const response = await
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers: { 'content-type': 'application/json' },
|
|
64
|
-
body: JSON.stringify(request),
|
|
65
|
-
})
|
|
68
|
+
const response = await postJson(LIBRARY_API.update, request)
|
|
66
69
|
const body = await response.json() as { ok?: boolean; entry?: LibraryEntry; message?: string }
|
|
67
70
|
return { ok: body.ok === true, ...(body.entry === undefined ? {} : { entry: body.entry }), ...(body.message === undefined ? {} : { message: body.message }) }
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
async libraryRemove(ids: string[]): Promise<{ ok: boolean }> {
|
|
71
|
-
const response = await
|
|
72
|
-
method: 'POST',
|
|
73
|
-
headers: { 'content-type': 'application/json' },
|
|
74
|
-
body: JSON.stringify({ ids }),
|
|
75
|
-
})
|
|
74
|
+
const response = await postJson(LIBRARY_API.remove, { ids })
|
|
76
75
|
const body = await response.json() as { ok?: boolean }
|
|
77
76
|
return { ok: body.ok === true }
|
|
78
77
|
}
|