runline 0.31.0 → 0.33.0
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/dist/plugin/schema.d.ts +2 -4
- package/dist/plugin/schema.js +47 -12
- package/dist/plugins/_shared/{imageFile.js → mediaFile.js} +33 -12
- package/dist/plugins/_shared/provider.js +22 -16
- package/dist/plugins/_shared/shiftCloud.js +34 -10
- package/dist/plugins/_shared/shiftUpload.js +1 -1
- package/dist/plugins/elevenlabs/src/index.js +277 -0
- package/dist/plugins/elevenlabs/src/schemas.js +115 -0
- package/dist/plugins/elevenlabs/src/shared.js +272 -0
- package/dist/plugins/elevenlabs/src/workflows.js +191 -0
- package/dist/plugins/fal/src/index.js +181 -0
- package/dist/plugins/fal/src/shared.js +303 -0
- package/dist/plugins/googleImage/src/index.js +1 -1
- package/dist/plugins/openai/src/index.js +1 -1
- package/dist/plugins/recraft/src/index.js +1 -1
- package/dist/plugins/replicate/src/index.js +5 -6
- package/dist/plugins/shiftBwm/src/collections.js +201 -0
- package/dist/plugins/shiftBwm/src/index.js +33 -0
- package/dist/plugins/shiftBwm/src/records.js +180 -0
- package/dist/plugins/shiftBwm/src/shared.js +143 -0
- package/dist/plugins/shiftBwm/src/vendor/client.js +227 -0
- package/dist/plugins/shiftBwm/src/vendor/contracts.js +59 -0
- package/dist/plugins/shiftBwm/src/vendor/models.js +525 -0
- package/dist/plugins/shiftObjects/src/objects.js +1 -2
- package/dist/plugins/shiftOcr/src/index.js +3 -1
- package/dist/plugins/shiftTranscription/src/shared.js +1 -1
- package/dist/plugins/shiftTranscription/src/transcription.js +4 -3
- package/dist/plugins/together/src/index.js +1 -1
- package/dist/plugins/xai/src/index.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
export const STRICT = { additionalProperties: false };
|
|
3
|
+
export const text = t.String({ minLength: 1, pattern: "\\S" });
|
|
4
|
+
export const timeout = t.Integer({
|
|
5
|
+
minimum: 1000,
|
|
6
|
+
maximum: 3_600_000,
|
|
7
|
+
default: 300_000,
|
|
8
|
+
description: "Request deadline including the response body; does not cancel provider work.",
|
|
9
|
+
});
|
|
10
|
+
export const fileOptions = {
|
|
11
|
+
saveDir: t.Optional(text),
|
|
12
|
+
timeoutMs: t.Optional(timeout),
|
|
13
|
+
};
|
|
14
|
+
export const seed = t.Integer({ minimum: 0, maximum: 4294967295 });
|
|
15
|
+
export const normalization = t.Union([
|
|
16
|
+
t.Literal("auto"),
|
|
17
|
+
t.Literal("on"),
|
|
18
|
+
t.Literal("off"),
|
|
19
|
+
]);
|
|
20
|
+
export const dictionaries = t.Array(t.Object({ pronunciation_dictionary_id: text, version_id: t.Optional(text) }, STRICT), { maxItems: 3 });
|
|
21
|
+
const rates = [8000, 16000, 22050, 24000, 32000, 44100, 48000];
|
|
22
|
+
const mp3 = [
|
|
23
|
+
"mp3_22050_32",
|
|
24
|
+
"mp3_24000_48",
|
|
25
|
+
...[32, 64, 96, 128, 192].map((rate) => `mp3_44100_${rate}`),
|
|
26
|
+
];
|
|
27
|
+
const compressedAndRaw = [
|
|
28
|
+
...mp3,
|
|
29
|
+
...rates.map((rate) => `pcm_${rate}`),
|
|
30
|
+
...[32, 64, 96, 128, 192].map((rate) => `opus_48000_${rate}`),
|
|
31
|
+
];
|
|
32
|
+
export const formatSchema = (kind) => t.Union([
|
|
33
|
+
...compressedAndRaw,
|
|
34
|
+
...(kind === "speech" ? rates.map((rate) => `wav_${rate}`) : []),
|
|
35
|
+
...(kind === "music"
|
|
36
|
+
? ["auto", ...[128, 192, 240, 320].map((rate) => `mp3_48000_${rate}`)]
|
|
37
|
+
: []),
|
|
38
|
+
].map((value) => t.Literal(value)), {
|
|
39
|
+
default: kind === "music" ? "auto" : "mp3_44100_128",
|
|
40
|
+
description: "Provider output format. PCM is raw audio without a WAV header; retain the sample rate and endpoint-specific channel layout. Higher-quality formats may require a paid tier.",
|
|
41
|
+
});
|
|
42
|
+
export const audioOptions = {
|
|
43
|
+
...fileOptions,
|
|
44
|
+
outputFormat: t.Optional(formatSchema("speech")),
|
|
45
|
+
};
|
|
46
|
+
export const voiceSettings = t.Object({
|
|
47
|
+
stability: t.Optional(t.Number({ minimum: 0, maximum: 1 })),
|
|
48
|
+
similarity_boost: t.Optional(t.Number({ minimum: 0, maximum: 1 })),
|
|
49
|
+
style: t.Optional(t.Number({ minimum: 0, maximum: 1 })),
|
|
50
|
+
use_speaker_boost: t.Optional(t.Boolean()),
|
|
51
|
+
speed: t.Optional(t.Number({ minimum: 0.7, maximum: 1.2 })),
|
|
52
|
+
}, STRICT);
|
|
53
|
+
export const speechControls = {
|
|
54
|
+
seed: t.Optional(seed),
|
|
55
|
+
pronunciationDictionaries: t.Optional(dictionaries),
|
|
56
|
+
normalization: t.Optional(normalization),
|
|
57
|
+
languageNormalization: t.Optional(t.Boolean({
|
|
58
|
+
description: "Language-specific normalization; currently Japanese only, may increase latency.",
|
|
59
|
+
})),
|
|
60
|
+
previousText: t.Optional(t.String()),
|
|
61
|
+
nextText: t.Optional(t.String()),
|
|
62
|
+
previousRequestIds: t.Optional(t.Array(text, { maxItems: 3 })),
|
|
63
|
+
nextRequestIds: t.Optional(t.Array(text, { maxItems: 3 })),
|
|
64
|
+
};
|
|
65
|
+
const styles = t.Array(t.String(), { maxItems: 50 });
|
|
66
|
+
const duration = t.Integer({ minimum: 3000, maximum: 120000 });
|
|
67
|
+
const v1Plan = t.Object({
|
|
68
|
+
positive_global_styles: styles,
|
|
69
|
+
negative_global_styles: styles,
|
|
70
|
+
sections: t.Array(t.Object({
|
|
71
|
+
section_name: t.String({ minLength: 1, maxLength: 100 }),
|
|
72
|
+
positive_local_styles: styles,
|
|
73
|
+
negative_local_styles: styles,
|
|
74
|
+
duration_ms: duration,
|
|
75
|
+
lines: t.Array(t.String({ maxLength: 200 }), { maxItems: 30 }),
|
|
76
|
+
}, STRICT), { minItems: 1, maxItems: 30 }),
|
|
77
|
+
}, STRICT);
|
|
78
|
+
const v2Plan = t.Object({
|
|
79
|
+
chunks: t.Array(t.Object({
|
|
80
|
+
text: t.String({ maxLength: 6132 }),
|
|
81
|
+
duration_ms: duration,
|
|
82
|
+
positive_styles: styles,
|
|
83
|
+
negative_styles: t.Optional(styles),
|
|
84
|
+
context_adherence: t.Optional(t.Union([t.Literal("low"), t.Literal("medium"), t.Literal("high")])),
|
|
85
|
+
}, STRICT), { minItems: 1, maxItems: 30 }),
|
|
86
|
+
}, STRICT);
|
|
87
|
+
export const musicSchema = t.Union([
|
|
88
|
+
t.Object({
|
|
89
|
+
...fileOptions,
|
|
90
|
+
outputFormat: t.Optional(formatSchema("music")),
|
|
91
|
+
prompt: t.String({ minLength: 1, maxLength: 4100, pattern: "\\S" }),
|
|
92
|
+
durationMs: t.Integer({ minimum: 3000, maximum: 600000 }),
|
|
93
|
+
model: t.Optional(t.Union([
|
|
94
|
+
t.Literal("music_v1"),
|
|
95
|
+
t.Literal("music_v2"),
|
|
96
|
+
t.Literal("music_v2_5"),
|
|
97
|
+
])),
|
|
98
|
+
instrumental: t.Optional(t.Boolean()),
|
|
99
|
+
}, STRICT),
|
|
100
|
+
t.Object({
|
|
101
|
+
...fileOptions,
|
|
102
|
+
outputFormat: t.Optional(formatSchema("music")),
|
|
103
|
+
model: t.Literal("music_v1"),
|
|
104
|
+
compositionPlan: v1Plan,
|
|
105
|
+
seed: t.Optional(t.Integer({ minimum: 0, maximum: 2147483647 })),
|
|
106
|
+
respectSectionsDurations: t.Optional(t.Boolean()),
|
|
107
|
+
}, STRICT),
|
|
108
|
+
t.Object({
|
|
109
|
+
...fileOptions,
|
|
110
|
+
outputFormat: t.Optional(formatSchema("music")),
|
|
111
|
+
model: t.Union([t.Literal("music_v2"), t.Literal("music_v2_5")]),
|
|
112
|
+
compositionPlan: v2Plan,
|
|
113
|
+
seed: t.Optional(t.Integer({ minimum: 0, maximum: 2147483647 })),
|
|
114
|
+
}, STRICT),
|
|
115
|
+
]);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import * as t from "typebox";
|
|
5
|
+
import { Check } from "typebox/value";
|
|
6
|
+
import { authedFetch } from "../../_shared/authedFetch.js";
|
|
7
|
+
import { SEND_FILE_NOTE, writeMediaFile } from "../../_shared/mediaFile.js";
|
|
8
|
+
import { obj, readBounded, readBoundedBytes, seg, } from "../../_shared/provider.js";
|
|
9
|
+
export const voicePath = (id) => seg(id, "voice id", "elevenlabs");
|
|
10
|
+
const JSON_LIMIT = 8 * 1024 * 1024;
|
|
11
|
+
const AUDIO_LIMIT = 100 * 1024 * 1024;
|
|
12
|
+
/** A fixed origin, no redirects or retries, and one deadline through body consumption. */
|
|
13
|
+
async function request(ctx, path, init, timeoutMs, consume) {
|
|
14
|
+
const key = ctx.connection.config.apiKey;
|
|
15
|
+
if (typeof key !== "string" || !key.trim())
|
|
16
|
+
throw new Error("Missing ELEVENLABS_API_KEY");
|
|
17
|
+
let requestId = null;
|
|
18
|
+
try {
|
|
19
|
+
const response = await authedFetch(`https://api.elevenlabs.io${path}`, {
|
|
20
|
+
...init,
|
|
21
|
+
headers: {
|
|
22
|
+
"xi-api-key": key.trim(),
|
|
23
|
+
...(typeof init.body === "string"
|
|
24
|
+
? { "content-type": "application/json" }
|
|
25
|
+
: {}),
|
|
26
|
+
},
|
|
27
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
28
|
+
});
|
|
29
|
+
requestId = response.headers.get("request-id");
|
|
30
|
+
// Both JSON and audio errors use the provider's JSON error envelope.
|
|
31
|
+
if (!response.ok)
|
|
32
|
+
await json(response);
|
|
33
|
+
return await consume(response);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const mutation = init.method && init.method !== "GET";
|
|
37
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}${requestId ? ` (requestId ${requestId})` : ""}${mutation ? ". Request may have been applied or billed; do not retry automatically." : ""}`, { cause: error });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function json(response, limit = JSON_LIMIT) {
|
|
41
|
+
const text = await readBounded(response, limit, `elevenlabs: JSON exceeds ${limit / (1024 * 1024)} MiB`);
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = text ? JSON.parse(text) : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error(`elevenlabs HTTP ${response.status}: non-JSON response`);
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const detail = obj(body).detail;
|
|
51
|
+
const error = obj(detail);
|
|
52
|
+
// Include provider error messages and locations, not validation input payloads.
|
|
53
|
+
const message = typeof detail === "string"
|
|
54
|
+
? detail
|
|
55
|
+
: Array.isArray(detail)
|
|
56
|
+
? detail
|
|
57
|
+
.map((v) => {
|
|
58
|
+
const e = obj(v);
|
|
59
|
+
return `${Array.isArray(e.loc) ? e.loc.join(".") : "body"}: ${e.msg ?? "invalid"}`;
|
|
60
|
+
})
|
|
61
|
+
.join("; ")
|
|
62
|
+
: `${error.status ?? "request failed"}: ${error.message ?? ""}`;
|
|
63
|
+
throw new Error(`elevenlabs HTTP ${response.status}: ${message.slice(0, 1000)}`);
|
|
64
|
+
}
|
|
65
|
+
if (body === null || typeof body !== "object")
|
|
66
|
+
throw new Error("elevenlabs: expected a JSON object or array");
|
|
67
|
+
return body;
|
|
68
|
+
}
|
|
69
|
+
const id = t.String({ minLength: 1 });
|
|
70
|
+
// Validate the fields callers depend on; preserve additional provider metadata.
|
|
71
|
+
const voiceResponse = t.Object({ voice_id: id, name: t.String() });
|
|
72
|
+
const responses = {
|
|
73
|
+
models: t.Array(t.Object({ model_id: id, name: t.String() })),
|
|
74
|
+
voices: t.Object({
|
|
75
|
+
voices: t.Array(voiceResponse),
|
|
76
|
+
has_more: t.Boolean(),
|
|
77
|
+
next_page_token: t.Optional(t.Union([id, t.Null()])),
|
|
78
|
+
}),
|
|
79
|
+
voice: voiceResponse,
|
|
80
|
+
clone: t.Object({ voice_id: id, requires_verification: t.Boolean() }),
|
|
81
|
+
acknowledgement: t.Object({ status: t.Literal("ok") }),
|
|
82
|
+
transcription: t.Union([
|
|
83
|
+
t.Object({ text: t.String(), words: t.Array(t.Unknown()) }),
|
|
84
|
+
t.Object({
|
|
85
|
+
transcripts: t.Array(t.Object({ text: t.String(), words: t.Array(t.Unknown()) }), { minItems: 1 }),
|
|
86
|
+
}),
|
|
87
|
+
]),
|
|
88
|
+
history: t.Object({
|
|
89
|
+
history: t.Array(t.Object({ history_item_id: id })),
|
|
90
|
+
has_more: t.Boolean(),
|
|
91
|
+
last_history_item_id: t.Optional(t.Union([id, t.Null()])),
|
|
92
|
+
}),
|
|
93
|
+
historyItem: t.Object({ history_item_id: id, content_type: t.String() }),
|
|
94
|
+
settings: t.Object({
|
|
95
|
+
stability: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
96
|
+
similarity_boost: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
97
|
+
style: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
98
|
+
speed: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
99
|
+
use_speaker_boost: t.Optional(t.Union([t.Boolean(), t.Null()])),
|
|
100
|
+
}),
|
|
101
|
+
dictionaries: t.Object({
|
|
102
|
+
pronunciation_dictionaries: t.Array(t.Object({ id, name: t.String(), latest_version_id: id })),
|
|
103
|
+
has_more: t.Boolean(),
|
|
104
|
+
next_cursor: t.Optional(t.Union([id, t.Null()])),
|
|
105
|
+
}),
|
|
106
|
+
dictionary: t.Object({ id, version_id: id }),
|
|
107
|
+
};
|
|
108
|
+
export async function jsonRequest(ctx, path, expected, init = {}, timeoutMs = 60_000) {
|
|
109
|
+
return request(ctx, path, init, timeoutMs, async (response) => {
|
|
110
|
+
const body = await json(response);
|
|
111
|
+
if (!Check(responses[expected], body))
|
|
112
|
+
throw new Error(`elevenlabs: invalid ${expected} response`);
|
|
113
|
+
const cursor = {
|
|
114
|
+
voices: "next_page_token",
|
|
115
|
+
history: "last_history_item_id",
|
|
116
|
+
dictionaries: "next_cursor",
|
|
117
|
+
};
|
|
118
|
+
if (expected in cursor &&
|
|
119
|
+
obj(body).has_more &&
|
|
120
|
+
!obj(body)[cursor[expected]])
|
|
121
|
+
throw new Error("elevenlabs: page is missing its continuation token");
|
|
122
|
+
return body;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const alignment = t.Object({
|
|
126
|
+
characters: t.Array(t.String()),
|
|
127
|
+
character_start_times_seconds: t.Array(t.Number({ minimum: 0 })),
|
|
128
|
+
character_end_times_seconds: t.Array(t.Number({ minimum: 0 })),
|
|
129
|
+
});
|
|
130
|
+
const alignedAudio = t.Object({
|
|
131
|
+
audio_base64: t.String({
|
|
132
|
+
minLength: 1,
|
|
133
|
+
pattern: "^[A-Za-z0-9+/]*={0,2}$",
|
|
134
|
+
}),
|
|
135
|
+
alignment: t.Optional(t.Union([alignment, t.Null()])),
|
|
136
|
+
normalized_alignment: t.Optional(t.Union([alignment, t.Null()])),
|
|
137
|
+
});
|
|
138
|
+
/** Requested formats resolve generic headers; explicit audio types must agree. */
|
|
139
|
+
function audioMime(format, contentType) {
|
|
140
|
+
const mime = contentType?.split(";")[0].trim().toLowerCase();
|
|
141
|
+
const supported = [
|
|
142
|
+
"audio/mpeg",
|
|
143
|
+
"audio/mp3",
|
|
144
|
+
"audio/wav",
|
|
145
|
+
"audio/x-wav",
|
|
146
|
+
"audio/ogg",
|
|
147
|
+
"audio/opus",
|
|
148
|
+
"audio/x-pcm",
|
|
149
|
+
"audio/pcm",
|
|
150
|
+
];
|
|
151
|
+
if (mime && mime !== "application/octet-stream" && !supported.includes(mime))
|
|
152
|
+
throw new Error(`elevenlabs: expected audio, received ${mime}`);
|
|
153
|
+
const expected = format?.startsWith("pcm_")
|
|
154
|
+
? "audio/x-pcm"
|
|
155
|
+
: format?.startsWith("wav_")
|
|
156
|
+
? "audio/wav"
|
|
157
|
+
: format?.startsWith("opus_")
|
|
158
|
+
? "audio/ogg"
|
|
159
|
+
: format === "auto" || format?.startsWith("mp3_")
|
|
160
|
+
? "audio/mpeg"
|
|
161
|
+
: undefined;
|
|
162
|
+
const aliases = {
|
|
163
|
+
"audio/mp3": "audio/mpeg",
|
|
164
|
+
"audio/x-wav": "audio/wav",
|
|
165
|
+
"audio/pcm": "audio/x-pcm",
|
|
166
|
+
"audio/opus": "audio/ogg",
|
|
167
|
+
};
|
|
168
|
+
const actual = mime && supported.includes(mime) ? (aliases[mime] ?? mime) : undefined;
|
|
169
|
+
if (expected && actual && expected !== actual)
|
|
170
|
+
throw new Error(`elevenlabs: audio format mismatch: expected ${expected}, received ${actual}`);
|
|
171
|
+
const resolved = actual ?? expected;
|
|
172
|
+
if (!resolved)
|
|
173
|
+
throw new Error("elevenlabs: response has no identifiable audio format");
|
|
174
|
+
return resolved;
|
|
175
|
+
}
|
|
176
|
+
export async function audioRequest(ctx, path, body, options) {
|
|
177
|
+
const format = options.outputFormat ?? "mp3_44100_128";
|
|
178
|
+
return binaryRequest(ctx, `${path}?output_format=${encodeURIComponent(format)}`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
body: body instanceof FormData ? body : JSON.stringify(body),
|
|
181
|
+
}, { ...options, outputFormat: format });
|
|
182
|
+
}
|
|
183
|
+
/** All generated, aligned, isolated, and recovered audio uses the same file writer. */
|
|
184
|
+
export async function binaryRequest(ctx, path, init, options) {
|
|
185
|
+
return request(ctx, path, init, options.timeoutMs ?? 300_000, async (response) => {
|
|
186
|
+
let bytes;
|
|
187
|
+
let timestamps = {};
|
|
188
|
+
let mime;
|
|
189
|
+
if (options.timestamps) {
|
|
190
|
+
const body = await json(response, 32 * 1024 * 1024);
|
|
191
|
+
if (!Check(alignedAudio, body) || body.audio_base64.length % 4 !== 0)
|
|
192
|
+
throw new Error("elevenlabs: invalid aligned audio response");
|
|
193
|
+
for (const a of [body.alignment, body.normalized_alignment]) {
|
|
194
|
+
if (a &&
|
|
195
|
+
(a.characters.length !== a.character_start_times_seconds.length ||
|
|
196
|
+
a.characters.length !== a.character_end_times_seconds.length ||
|
|
197
|
+
a.character_end_times_seconds.some((end, i) => end < a.character_start_times_seconds[i])))
|
|
198
|
+
throw new Error("elevenlabs: invalid alignment lengths or times");
|
|
199
|
+
}
|
|
200
|
+
bytes = Buffer.from(body.audio_base64, "base64");
|
|
201
|
+
timestamps = {
|
|
202
|
+
alignment: body.alignment,
|
|
203
|
+
normalizedAlignment: body.normalized_alignment,
|
|
204
|
+
};
|
|
205
|
+
mime = audioMime(options.outputFormat, null);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
try {
|
|
209
|
+
mime = audioMime(options.outputFormat, response.headers.get("content-type"));
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
await response.body?.cancel();
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
bytes = await readBoundedBytes(response, AUDIO_LIMIT, "elevenlabs: audio exceeds 100 MiB");
|
|
216
|
+
}
|
|
217
|
+
if (!bytes.length)
|
|
218
|
+
throw new Error("elevenlabs: empty audio response");
|
|
219
|
+
const audio = writeMediaFile({
|
|
220
|
+
bytes,
|
|
221
|
+
mimeType: mime,
|
|
222
|
+
provider: "elevenlabs",
|
|
223
|
+
index: 0,
|
|
224
|
+
saveDir: options.saveDir,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
provider: "elevenlabs",
|
|
228
|
+
audio,
|
|
229
|
+
outputFormat: options.outputFormat ?? null,
|
|
230
|
+
requestId: response.headers.get("request-id"),
|
|
231
|
+
songId: response.headers.get("song-id"),
|
|
232
|
+
characterCost: response.headers.get("character-cost"),
|
|
233
|
+
...timestamps,
|
|
234
|
+
note: SEND_FILE_NOTE,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
export async function voiceForm(input) {
|
|
239
|
+
const form = new FormData();
|
|
240
|
+
form.append("name", input.name);
|
|
241
|
+
for (const path of input.audioPaths ?? [])
|
|
242
|
+
await appendFile(form, "files", path);
|
|
243
|
+
if (input.description !== undefined)
|
|
244
|
+
form.append("description", input.description);
|
|
245
|
+
if (input.labels !== undefined)
|
|
246
|
+
form.append("labels", JSON.stringify(input.labels));
|
|
247
|
+
if (input.removeBackgroundNoise !== undefined)
|
|
248
|
+
form.append("remove_background_noise", String(input.removeBackgroundNoise));
|
|
249
|
+
return form;
|
|
250
|
+
}
|
|
251
|
+
/** Bound uploads before allocating; read through the same handle to avoid stat/read races. */
|
|
252
|
+
export async function appendFile(form, field, path) {
|
|
253
|
+
// Nonblocking open lets fstat reject FIFOs without waiting for a writer.
|
|
254
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
255
|
+
try {
|
|
256
|
+
const stat = await file.stat();
|
|
257
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > 25 * 1024 * 1024)
|
|
258
|
+
throw new Error("elevenlabs: each input must be a nonempty regular file of at most 25 MiB");
|
|
259
|
+
const bytes = Buffer.alloc(stat.size);
|
|
260
|
+
let offset = 0;
|
|
261
|
+
while (offset < bytes.length) {
|
|
262
|
+
const { bytesRead } = await file.read(bytes, offset, bytes.length - offset, offset);
|
|
263
|
+
if (!bytesRead)
|
|
264
|
+
throw new Error("elevenlabs: input file changed while reading");
|
|
265
|
+
offset += bytesRead;
|
|
266
|
+
}
|
|
267
|
+
form.append(field, new Blob([bytes]), basename(path));
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
await file.close();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { seg } from "../../_shared/provider.js";
|
|
3
|
+
import { audioOptions, dictionaries, fileOptions, normalization, STRICT, seed, text, voiceSettings, } from "./schemas.js";
|
|
4
|
+
import { appendFile, audioRequest, binaryRequest, jsonRequest, voiceForm, voicePath, } from "./shared.js";
|
|
5
|
+
const dialogue = t.Object({
|
|
6
|
+
...audioOptions,
|
|
7
|
+
inputs: t.Array(t.Object({ text, voiceId: text }, STRICT), {
|
|
8
|
+
minItems: 1,
|
|
9
|
+
maxItems: 100,
|
|
10
|
+
description: "At most 10 unique voices and 2000 total characters per request.",
|
|
11
|
+
}),
|
|
12
|
+
model: t.Optional(text),
|
|
13
|
+
languageCode: t.Optional(t.String({ pattern: "^[a-z]{2}$" })),
|
|
14
|
+
stability: t.Optional(t.Number({ minimum: 0, maximum: 1 })),
|
|
15
|
+
seed: t.Optional(seed),
|
|
16
|
+
pronunciationDictionaries: t.Optional(dictionaries),
|
|
17
|
+
normalization: t.Optional(normalization),
|
|
18
|
+
}, STRICT);
|
|
19
|
+
const isolate = t.Object({ ...fileOptions, audioPath: text }, STRICT);
|
|
20
|
+
const historyList = t.Object({
|
|
21
|
+
limit: t.Optional(t.Integer({ minimum: 1, maximum: 1000 })),
|
|
22
|
+
cursor: t.Optional(text),
|
|
23
|
+
voiceId: t.Optional(text),
|
|
24
|
+
model: t.Optional(text),
|
|
25
|
+
search: t.Optional(text),
|
|
26
|
+
source: t.Optional(t.Union([t.Literal("TTS"), t.Literal("STS"), t.Literal("Flows")])),
|
|
27
|
+
}, STRICT);
|
|
28
|
+
const historyItem = t.Object({ historyItemId: text }, STRICT);
|
|
29
|
+
const historyDownload = t.Object({ historyItemId: text, ...fileOptions }, STRICT);
|
|
30
|
+
const voice = t.Object({ voiceId: text }, STRICT);
|
|
31
|
+
const settings = t.Object({
|
|
32
|
+
voiceId: text,
|
|
33
|
+
settings: t.Object(voiceSettings.properties, {
|
|
34
|
+
...STRICT,
|
|
35
|
+
minProperties: 1,
|
|
36
|
+
}),
|
|
37
|
+
}, STRICT);
|
|
38
|
+
const editVoice = t.Object({
|
|
39
|
+
voiceId: text,
|
|
40
|
+
name: text,
|
|
41
|
+
description: t.Optional(t.String()),
|
|
42
|
+
labels: t.Optional(t.Record(t.String(), t.String())),
|
|
43
|
+
audioPaths: t.Optional(t.Array(text, { maxItems: 10 })),
|
|
44
|
+
removeBackgroundNoise: t.Optional(t.Boolean()),
|
|
45
|
+
}, STRICT);
|
|
46
|
+
const dictionaryList = t.Object({
|
|
47
|
+
cursor: t.Optional(text),
|
|
48
|
+
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
49
|
+
}, STRICT);
|
|
50
|
+
const ruleOptions = {
|
|
51
|
+
string_to_replace: text,
|
|
52
|
+
case_sensitive: t.Optional(t.Boolean()),
|
|
53
|
+
word_boundaries: t.Optional(t.Boolean()),
|
|
54
|
+
};
|
|
55
|
+
const dictionaryCreate = t.Object({
|
|
56
|
+
name: text,
|
|
57
|
+
description: t.Optional(t.String()),
|
|
58
|
+
rules: t.Array(t.Union([
|
|
59
|
+
t.Object({ ...ruleOptions, type: t.Literal("alias"), alias: text }, STRICT),
|
|
60
|
+
t.Object({
|
|
61
|
+
...ruleOptions,
|
|
62
|
+
type: t.Literal("phoneme"),
|
|
63
|
+
phoneme: text,
|
|
64
|
+
alphabet: text,
|
|
65
|
+
}, STRICT),
|
|
66
|
+
]), { minItems: 1 }),
|
|
67
|
+
}, STRICT);
|
|
68
|
+
const historyPath = (id) => `/v1/history/${seg(id, "history item id", "elevenlabs")}`;
|
|
69
|
+
export function registerWorkflows(rl) {
|
|
70
|
+
rl.registerAction("dialogue.create", {
|
|
71
|
+
access: "write",
|
|
72
|
+
description: "Generate multi-speaker dialogue with Eleven v3 (billed). Up to 10 voices and 2000 total characters. Returns audio.path.",
|
|
73
|
+
inputSchema: dialogue,
|
|
74
|
+
async execute(input, ctx) {
|
|
75
|
+
const p = input;
|
|
76
|
+
if (new Set(p.inputs.map((i) => i.voiceId)).size > 10 ||
|
|
77
|
+
p.inputs.reduce((sum, i) => sum + i.text.length, 0) > 2000)
|
|
78
|
+
throw new Error("elevenlabs: dialogue allows at most 10 voices and 2000 total characters");
|
|
79
|
+
return audioRequest(ctx, "/v1/text-to-dialogue", {
|
|
80
|
+
inputs: p.inputs.map((i) => ({ text: i.text, voice_id: i.voiceId })),
|
|
81
|
+
model_id: p.model ?? "eleven_v3",
|
|
82
|
+
language_code: p.languageCode,
|
|
83
|
+
settings: p.stability === undefined ? undefined : { stability: p.stability },
|
|
84
|
+
seed: p.seed,
|
|
85
|
+
pronunciation_dictionary_locators: p.pronunciationDictionaries,
|
|
86
|
+
apply_text_normalization: p.normalization,
|
|
87
|
+
}, p);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
rl.registerAction("audio.isolate", {
|
|
91
|
+
access: "write",
|
|
92
|
+
description: "Remove background noise from a local recording (billed). Input up to 25 MiB. Saves the provider's output without transcoding.",
|
|
93
|
+
inputSchema: isolate,
|
|
94
|
+
async execute(input, ctx) {
|
|
95
|
+
const p = input;
|
|
96
|
+
const form = new FormData();
|
|
97
|
+
await appendFile(form, "audio", p.audioPath);
|
|
98
|
+
return binaryRequest(ctx, "/v1/audio-isolation", { method: "POST", body: form }, p);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
rl.registerAction("history.list", {
|
|
102
|
+
access: "read",
|
|
103
|
+
description: "List speech history (TTS, STS, Flows), not a universal music/sound archive. Pass last_history_item_id as cursor while has_more is true. Zero-retention requests are unavailable.",
|
|
104
|
+
inputSchema: historyList,
|
|
105
|
+
async execute(input, ctx) {
|
|
106
|
+
const p = input;
|
|
107
|
+
const query = new URLSearchParams({ page_size: String(p.limit ?? 100) });
|
|
108
|
+
for (const [key, value] of Object.entries({
|
|
109
|
+
start_after_history_item_id: p.cursor,
|
|
110
|
+
voice_id: p.voiceId,
|
|
111
|
+
model_id: p.model,
|
|
112
|
+
search: p.search,
|
|
113
|
+
source: p.source,
|
|
114
|
+
}))
|
|
115
|
+
if (value !== undefined)
|
|
116
|
+
query.set(key, value);
|
|
117
|
+
return jsonRequest(ctx, `/v1/history?${query}`, "history");
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
rl.registerAction("history.get", {
|
|
121
|
+
access: "read",
|
|
122
|
+
description: "Get one speech history item, including generation settings, text, and output format.",
|
|
123
|
+
inputSchema: historyItem,
|
|
124
|
+
async execute(input, ctx) {
|
|
125
|
+
return jsonRequest(ctx, historyPath(input.historyItemId), "historyItem");
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
rl.registerAction("history.download", {
|
|
129
|
+
access: "write",
|
|
130
|
+
description: "Recover existing speech audio without regenerating. Saves the downloaded response format to audio.path; the original generation format may differ.",
|
|
131
|
+
inputSchema: historyDownload,
|
|
132
|
+
async execute(input, ctx) {
|
|
133
|
+
const p = input;
|
|
134
|
+
return binaryRequest(ctx, `${historyPath(p.historyItemId)}/audio`, {}, p);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
rl.registerAction("voices.settings.get", {
|
|
138
|
+
access: "read",
|
|
139
|
+
description: "Read a voice's saved synthesis settings.",
|
|
140
|
+
inputSchema: voice,
|
|
141
|
+
async execute(input, ctx) {
|
|
142
|
+
return jsonRequest(ctx, `/v1/voices/${voicePath(input.voiceId)}/settings`, "settings");
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
rl.registerAction("voices.settings.update", {
|
|
146
|
+
access: "write",
|
|
147
|
+
description: "Update a voice's saved synthesis settings. Omitted fields follow provider defaults; read voices.settings.get to construct complete settings when preserving other values. Affects future synthesis using that voice.",
|
|
148
|
+
inputSchema: settings,
|
|
149
|
+
async execute(input, ctx) {
|
|
150
|
+
const p = input;
|
|
151
|
+
return jsonRequest(ctx, `/v1/voices/${voicePath(p.voiceId)}/settings/edit`, "acknowledgement", { method: "POST", body: JSON.stringify(p.settings) });
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
rl.registerAction("voices.update", {
|
|
155
|
+
access: "write",
|
|
156
|
+
description: "Edit voice name, description, labels, and optionally add owned/authorized audio samples (25 MiB each). Name is required by the API.",
|
|
157
|
+
inputSchema: editVoice,
|
|
158
|
+
async execute(input, ctx) {
|
|
159
|
+
const p = input;
|
|
160
|
+
const path = `/v1/voices/${voicePath(p.voiceId)}/edit`;
|
|
161
|
+
return jsonRequest(ctx, path, "acknowledgement", {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: await voiceForm(p),
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
rl.registerAction("pronunciationDictionaries.list", {
|
|
168
|
+
access: "read",
|
|
169
|
+
description: "List pronunciation dictionaries and their latest version IDs for speech/dialogue generation. Pass next_cursor as cursor.",
|
|
170
|
+
inputSchema: dictionaryList,
|
|
171
|
+
async execute(input, ctx) {
|
|
172
|
+
const p = input;
|
|
173
|
+
const query = new URLSearchParams({
|
|
174
|
+
page_size: String(p.limit ?? 30),
|
|
175
|
+
include_archived: "false",
|
|
176
|
+
});
|
|
177
|
+
if (p.cursor)
|
|
178
|
+
query.set("cursor", p.cursor);
|
|
179
|
+
return jsonRequest(ctx, `/v1/pronunciation-dictionaries?${query}`, "dictionaries");
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
rl.registerAction("pronunciationDictionaries.create", {
|
|
183
|
+
access: "write",
|
|
184
|
+
description: "Create a pronunciation dictionary from alias or phoneme rules. Returns id and version_id; phoneme support depends on the synthesis model.",
|
|
185
|
+
inputSchema: dictionaryCreate,
|
|
186
|
+
async execute(input, ctx) {
|
|
187
|
+
const p = input;
|
|
188
|
+
return jsonRequest(ctx, "/v1/pronunciation-dictionaries/add-from-rules", "dictionary", { method: "POST", body: JSON.stringify(p) });
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|