@byte_fluffy/nexra-sdk 0.1.0-alpha.50 → 0.1.0-alpha.53
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 +15 -110
- package/dist/catalog.d.ts +3 -3
- package/dist/catalog.js +2 -2
- package/dist/{chunk-WXPCZYOK.js → chunk-LWJWWD7Z.js} +6871 -8177
- package/dist/chunk-RBKLCJ3H.js +745 -0
- package/dist/{chunk-BS3B6F7W.js → chunk-V3F6VPMP.js} +122 -13
- package/dist/{errors-DJZuAKoO.d.ts → errors-BXPUVOh7.d.ts} +2 -2
- package/dist/index.d.ts +12 -11
- package/dist/index.js +16 -466
- package/dist/{json-Cz0MkWQn.d.ts → json-dnsDzk-s.d.ts} +1 -1
- package/dist/language.d.ts +3 -3
- package/dist/language.js +28 -243
- package/dist/{media-DzOxLGs-.d.ts → media-IaOdOMQC.d.ts} +14 -2
- package/dist/{media-geometry-COln1N_2.d.ts → media-geometry-B92BvuQL.d.ts} +3 -2
- package/dist/media.d.ts +2 -2
- package/dist/media.js +1 -1
- package/dist/node.d.ts +17 -0
- package/dist/node.js +78 -0
- package/dist/types.d.ts +5 -5
- package/dist/{voices-xyPqhQqR.d.ts → voices-Dc94LfyS.d.ts} +42 -5
- package/package.json +7 -1
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CatalogClient,
|
|
3
|
+
HttpTransport,
|
|
4
|
+
NexraApiError,
|
|
5
|
+
NexraTaskError,
|
|
6
|
+
NexraWaitTimeoutError,
|
|
7
|
+
applyMediaGeometry,
|
|
8
|
+
applyNoWatermark,
|
|
9
|
+
formatModelDisplayName,
|
|
10
|
+
getModelSpec,
|
|
11
|
+
jsonBody,
|
|
12
|
+
parseServerSentEvents,
|
|
13
|
+
queryString,
|
|
14
|
+
sleep
|
|
15
|
+
} from "./chunk-LWJWWD7Z.js";
|
|
16
|
+
import {
|
|
17
|
+
ASR_MAX_FRAME_BYTES,
|
|
18
|
+
ORGANIZATION_HEADER,
|
|
19
|
+
asrInputSecondsLimit,
|
|
20
|
+
createTaskRequest,
|
|
21
|
+
decodeVolcengineAsrFrame,
|
|
22
|
+
encodeVolcengineAsrFrame,
|
|
23
|
+
isTerminalTaskStatus,
|
|
24
|
+
volcengineAsrFrameJson
|
|
25
|
+
} from "./chunk-V3F6VPMP.js";
|
|
26
|
+
|
|
27
|
+
// src/assets.ts
|
|
28
|
+
var AssetsClient = class {
|
|
29
|
+
constructor(transport) {
|
|
30
|
+
this.transport = transport;
|
|
31
|
+
}
|
|
32
|
+
transport;
|
|
33
|
+
async upload(file, options = {}) {
|
|
34
|
+
const name = options.name ?? (isFile(file) ? file.name : void 0);
|
|
35
|
+
const headers = new Headers({
|
|
36
|
+
"content-type": file.type || "application/octet-stream",
|
|
37
|
+
"x-nexra-asset-kind": options.kind ?? inferKind(file.type)
|
|
38
|
+
});
|
|
39
|
+
if (name) headers.set("x-nexra-file-name", encodeURIComponent(name));
|
|
40
|
+
return this.transport.json(
|
|
41
|
+
"/assets",
|
|
42
|
+
{
|
|
43
|
+
method: "POST",
|
|
44
|
+
body: file,
|
|
45
|
+
headers,
|
|
46
|
+
signal: options.signal
|
|
47
|
+
},
|
|
48
|
+
{ retryable: false }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
import(request, signal) {
|
|
52
|
+
return this.transport.json("/assets/import", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
...jsonBody(request),
|
|
55
|
+
signal
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
get(assetId, signal) {
|
|
59
|
+
return this.transport.json(`/assets/${encodeURIComponent(assetId)}`, {
|
|
60
|
+
signal
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
certifyForSeedance(asset, request = {}, signal) {
|
|
64
|
+
const assetId = typeof asset === "string" ? asset : asset.id;
|
|
65
|
+
return this.transport.json(
|
|
66
|
+
`/assets/${encodeURIComponent(assetId)}/certifications/seedance`,
|
|
67
|
+
{
|
|
68
|
+
method: "POST",
|
|
69
|
+
...jsonBody(request),
|
|
70
|
+
signal
|
|
71
|
+
},
|
|
72
|
+
{ retryable: false }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
getSeedanceCertification(asset, signal) {
|
|
76
|
+
const assetId = typeof asset === "string" ? asset : asset.id;
|
|
77
|
+
return this.transport.json(
|
|
78
|
+
`/assets/${encodeURIComponent(assetId)}/certifications/seedance`,
|
|
79
|
+
{ signal }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
async waitForSeedanceCertification(asset, options = {}) {
|
|
83
|
+
const pollIntervalMs = positiveInteger(options.pollIntervalMs ?? 2e3, "pollIntervalMs");
|
|
84
|
+
const timeoutMs = positiveInteger(options.timeoutMs ?? 3e5, "timeoutMs");
|
|
85
|
+
const deadline = Date.now() + timeoutMs;
|
|
86
|
+
while (true) {
|
|
87
|
+
const certification = await this.getSeedanceCertification(asset, options.signal);
|
|
88
|
+
if (certification.status !== "processing") return certification;
|
|
89
|
+
if (Date.now() >= deadline) {
|
|
90
|
+
throw new Error("Timed out waiting for Seedance Asset certification");
|
|
91
|
+
}
|
|
92
|
+
await wait(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())), options.signal);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
download(asset, signal) {
|
|
96
|
+
const assetId = typeof asset === "string" ? asset : asset.id;
|
|
97
|
+
return this.transport.response(`/assets/${encodeURIComponent(assetId)}/content`, {
|
|
98
|
+
signal
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async stream(asset, signal) {
|
|
102
|
+
const response = await this.download(asset, signal);
|
|
103
|
+
if (!response.body) throw new Error("Asset response did not contain a body stream");
|
|
104
|
+
return response.body;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function positiveInteger(value, name) {
|
|
108
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive integer`);
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
async function wait(milliseconds, signal) {
|
|
112
|
+
if (signal?.aborted) throw signal.reason;
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
const timer = setTimeout(resolve, milliseconds);
|
|
115
|
+
signal?.addEventListener("abort", () => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
reject(signal.reason);
|
|
118
|
+
}, { once: true });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function inferKind(mimeType) {
|
|
122
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
123
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
124
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
125
|
+
if (mimeType.startsWith("text/") || mimeType === "application/pdf") return "document";
|
|
126
|
+
if (mimeType.includes("zip") || mimeType.includes("tar")) return "archive";
|
|
127
|
+
return "other";
|
|
128
|
+
}
|
|
129
|
+
function isFile(value) {
|
|
130
|
+
return typeof File !== "undefined" && value instanceof File;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/models.ts
|
|
134
|
+
function withModelDisplayName(model) {
|
|
135
|
+
return { ...model, displayName: formatModelDisplayName(model.displayName, model.slug ? { modelId: model.slug } : void 0) };
|
|
136
|
+
}
|
|
137
|
+
var ModelsClient = class {
|
|
138
|
+
constructor(transport) {
|
|
139
|
+
this.transport = transport;
|
|
140
|
+
}
|
|
141
|
+
transport;
|
|
142
|
+
async search(params = {}, signal) {
|
|
143
|
+
const page = await this.transport.json(
|
|
144
|
+
`/models${queryString({
|
|
145
|
+
q: params.query,
|
|
146
|
+
modality: params.modality,
|
|
147
|
+
operation: params.operation,
|
|
148
|
+
category: params.category,
|
|
149
|
+
limit: params.limit,
|
|
150
|
+
cursor: params.cursor
|
|
151
|
+
})}`,
|
|
152
|
+
{ signal }
|
|
153
|
+
);
|
|
154
|
+
return { ...page, data: page.data.map(withModelDisplayName) };
|
|
155
|
+
}
|
|
156
|
+
async get(model, signal) {
|
|
157
|
+
const detail = await this.transport.json(`/models/${encodeURIComponent(model)}`, {
|
|
158
|
+
signal
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
...withModelDisplayName(detail),
|
|
162
|
+
...detail.familyVersions ? { familyVersions: detail.familyVersions.map(withModelDisplayName) } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
schema(model, signal) {
|
|
166
|
+
return this.transport.json(
|
|
167
|
+
`/models/${encodeURIComponent(model)}/schema`,
|
|
168
|
+
{ signal }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
resources(model, resource, signal) {
|
|
172
|
+
return this.transport.json(
|
|
173
|
+
`/models/${encodeURIComponent(model)}/resources/${encodeURIComponent(resource)}`,
|
|
174
|
+
{ signal }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/voices.ts
|
|
180
|
+
var VoicesClient = class {
|
|
181
|
+
constructor(transport) {
|
|
182
|
+
this.transport = transport;
|
|
183
|
+
}
|
|
184
|
+
transport;
|
|
185
|
+
async list(signal) {
|
|
186
|
+
const response = await this.transport.json("/voices", { signal });
|
|
187
|
+
return response.data;
|
|
188
|
+
}
|
|
189
|
+
get(voiceId, signal) {
|
|
190
|
+
return this.transport.json(`/voices/${encodeURIComponent(voiceId)}`, { signal });
|
|
191
|
+
}
|
|
192
|
+
create(request, options) {
|
|
193
|
+
if (!options.idempotencyKey.trim()) throw new TypeError("Idempotency-Key is required");
|
|
194
|
+
return this.transport.json("/voices", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
...jsonBody(request),
|
|
197
|
+
headers: { "content-type": "application/json", "idempotency-key": options.idempotencyKey },
|
|
198
|
+
signal: options.signal
|
|
199
|
+
}, { retryable: true });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/input.ts
|
|
204
|
+
async function normalizeTaskInput(input, assets, signal) {
|
|
205
|
+
const normalized = await normalizeValue(input, assets, signal, /* @__PURE__ */ new WeakSet());
|
|
206
|
+
if (!isPlainObject(normalized)) {
|
|
207
|
+
throw new TypeError("Task input must be a JSON object");
|
|
208
|
+
}
|
|
209
|
+
return normalized;
|
|
210
|
+
}
|
|
211
|
+
async function normalizeValue(value, assets, signal, ancestors) {
|
|
212
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
if (typeof value === "number") {
|
|
216
|
+
if (!Number.isFinite(value)) throw new TypeError("Task input numbers must be finite");
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (value instanceof URL) return value.toString();
|
|
220
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) {
|
|
221
|
+
const uploaded = await assets.upload(value, { signal });
|
|
222
|
+
return { assetId: uploaded.id };
|
|
223
|
+
}
|
|
224
|
+
if (isMediaAsset(value)) {
|
|
225
|
+
return { assetId: value.id };
|
|
226
|
+
}
|
|
227
|
+
if (isAssetReference(value)) return { assetId: value.assetId };
|
|
228
|
+
if (Array.isArray(value)) {
|
|
229
|
+
assertNotCircular(value, ancestors);
|
|
230
|
+
const normalized = await Promise.all(
|
|
231
|
+
value.map((item) => normalizeValue(item, assets, signal, ancestors))
|
|
232
|
+
);
|
|
233
|
+
ancestors.delete(value);
|
|
234
|
+
return normalized;
|
|
235
|
+
}
|
|
236
|
+
if (isPlainObject(value)) {
|
|
237
|
+
assertNotCircular(value, ancestors);
|
|
238
|
+
const normalized = {};
|
|
239
|
+
for (const [key, item] of Object.entries(value)) {
|
|
240
|
+
if (item === void 0) continue;
|
|
241
|
+
normalized[key] = await normalizeValue(item, assets, signal, ancestors);
|
|
242
|
+
}
|
|
243
|
+
ancestors.delete(value);
|
|
244
|
+
return normalized;
|
|
245
|
+
}
|
|
246
|
+
throw new TypeError(`Task input contains a non-serializable ${typeof value} value`);
|
|
247
|
+
}
|
|
248
|
+
function assertNotCircular(value, ancestors) {
|
|
249
|
+
if (ancestors.has(value)) throw new TypeError("Task input cannot contain circular references");
|
|
250
|
+
ancestors.add(value);
|
|
251
|
+
}
|
|
252
|
+
function isPlainObject(value) {
|
|
253
|
+
if (!value || typeof value !== "object") return false;
|
|
254
|
+
const prototype = Object.getPrototypeOf(value);
|
|
255
|
+
return prototype === Object.prototype || prototype === null;
|
|
256
|
+
}
|
|
257
|
+
function isAssetReference(value) {
|
|
258
|
+
return isPlainObject(value) && typeof value.assetId === "string";
|
|
259
|
+
}
|
|
260
|
+
function isMediaAsset(value) {
|
|
261
|
+
return isPlainObject(value) && typeof value.id === "string" && typeof value.kind === "string" && typeof value.status === "string" && typeof value.source === "string";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/tasks.ts
|
|
265
|
+
var TasksClient = class {
|
|
266
|
+
constructor(transport, assets) {
|
|
267
|
+
this.transport = transport;
|
|
268
|
+
this.assets = assets;
|
|
269
|
+
}
|
|
270
|
+
transport;
|
|
271
|
+
assets;
|
|
272
|
+
async create(model, input, options = {}) {
|
|
273
|
+
const schema = getModelSpec(model)?.operations[0]?.inputSchema;
|
|
274
|
+
const mapped = schema && input && typeof input === "object" && !Array.isArray(input) ? applyNoWatermark(schema, applyMediaGeometry(schema, input)) : input;
|
|
275
|
+
const normalizedInput = await normalizeTaskInput(mapped, this.assets, options.signal);
|
|
276
|
+
const body = createTaskRequest(model, normalizedInput, {
|
|
277
|
+
quoteToken: options.quoteToken,
|
|
278
|
+
submittedAt: options.submittedAt,
|
|
279
|
+
metadata: options.metadata,
|
|
280
|
+
callback_url: options.callback_url,
|
|
281
|
+
webhook: options.webhook,
|
|
282
|
+
schemaVersion: options.schemaVersion
|
|
283
|
+
});
|
|
284
|
+
const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
285
|
+
return this.transport.json(
|
|
286
|
+
"/tasks",
|
|
287
|
+
{
|
|
288
|
+
method: "POST",
|
|
289
|
+
...jsonBody(body),
|
|
290
|
+
headers: {
|
|
291
|
+
...jsonBody(body).headers,
|
|
292
|
+
"idempotency-key": idempotencyKey
|
|
293
|
+
},
|
|
294
|
+
signal: options.signal
|
|
295
|
+
},
|
|
296
|
+
{ retryable: true }
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
get(taskId, signal) {
|
|
300
|
+
return this.transport.json(`/tasks/${encodeURIComponent(taskId)}`, {
|
|
301
|
+
signal
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
list(params = {}, signal) {
|
|
305
|
+
return this.transport.json(
|
|
306
|
+
`/tasks${queryString({
|
|
307
|
+
status: params.status,
|
|
308
|
+
model: params.model,
|
|
309
|
+
limit: params.limit,
|
|
310
|
+
cursor: params.cursor
|
|
311
|
+
})}`,
|
|
312
|
+
{ signal }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
cancel(taskId, signal) {
|
|
316
|
+
return this.transport.json(
|
|
317
|
+
`/tasks/${encodeURIComponent(taskId)}/cancel`,
|
|
318
|
+
{ method: "POST", signal },
|
|
319
|
+
{ retryable: true }
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
async *watch(taskId, options = {}) {
|
|
323
|
+
let lastEventId = options.lastEventId;
|
|
324
|
+
let reconnects = 0;
|
|
325
|
+
let reconnectDelay = 500;
|
|
326
|
+
const maxReconnects = options.maxReconnects ?? 5;
|
|
327
|
+
while (true) {
|
|
328
|
+
const headers = new Headers({ accept: "text/event-stream" });
|
|
329
|
+
if (lastEventId) headers.set("last-event-id", lastEventId);
|
|
330
|
+
const response = await this.transport.response(
|
|
331
|
+
`/tasks/${encodeURIComponent(taskId)}/events`,
|
|
332
|
+
{ headers, signal: options.signal },
|
|
333
|
+
{ retryable: false }
|
|
334
|
+
);
|
|
335
|
+
let reachedTerminalState = false;
|
|
336
|
+
for await (const serverEvent of parseServerSentEvents(response)) {
|
|
337
|
+
if (serverEvent.id) lastEventId = serverEvent.id;
|
|
338
|
+
if (serverEvent.retry !== void 0) reconnectDelay = serverEvent.retry;
|
|
339
|
+
const event = JSON.parse(serverEvent.data);
|
|
340
|
+
yield event;
|
|
341
|
+
if (event.status && isTerminalTaskStatus(event.status)) {
|
|
342
|
+
const task = event.payload?.task;
|
|
343
|
+
const deliveryPreparing = task && typeof task === "object" && !Array.isArray(task) && task.deliveryStatus === "preparing";
|
|
344
|
+
if (!deliveryPreparing) reachedTerminalState = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (reachedTerminalState) return;
|
|
348
|
+
if (reconnects >= maxReconnects) return;
|
|
349
|
+
reconnects += 1;
|
|
350
|
+
await sleep(reconnectDelay, options.signal);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async wait(taskId, options = {}) {
|
|
354
|
+
const timeoutController = new AbortController();
|
|
355
|
+
const timeoutMs = options.timeoutMs;
|
|
356
|
+
const timeout = timeoutMs ? setTimeout(() => timeoutController.abort("nexra_wait_timeout"), timeoutMs) : void 0;
|
|
357
|
+
const signal = combineSignals(options.signal, timeoutMs ? timeoutController.signal : void 0);
|
|
358
|
+
try {
|
|
359
|
+
const current = await this.get(taskId, signal);
|
|
360
|
+
if (isTerminalTaskStatus(current.status) && current.deliveryStatus !== "preparing") return current;
|
|
361
|
+
try {
|
|
362
|
+
for await (const event of this.watch(taskId, {
|
|
363
|
+
signal,
|
|
364
|
+
lastEventId: options.lastEventId,
|
|
365
|
+
maxReconnects: options.maxReconnects
|
|
366
|
+
})) {
|
|
367
|
+
await options.onEvent?.(event);
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (signal?.aborted) throw error;
|
|
371
|
+
if (error instanceof NexraApiError && ![405, 406, 501].includes(error.status)) {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
while (true) {
|
|
376
|
+
const task = await this.get(taskId, signal);
|
|
377
|
+
if (isTerminalTaskStatus(task.status) && task.deliveryStatus !== "preparing") return task;
|
|
378
|
+
await sleep(options.pollIntervalMs ?? 1e3, signal);
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (timeoutController.signal.aborted && timeoutMs) {
|
|
382
|
+
throw new NexraWaitTimeoutError(taskId, timeoutMs);
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
} finally {
|
|
386
|
+
if (timeout) clearTimeout(timeout);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async run(model, input, options = {}) {
|
|
390
|
+
const task = await this.create(model, input, options);
|
|
391
|
+
let completed;
|
|
392
|
+
try {
|
|
393
|
+
completed = await this.wait(task.id, options);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (options.cancelOnAbort && options.signal?.aborted) {
|
|
396
|
+
await this.cancel(task.id).catch(() => void 0);
|
|
397
|
+
}
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
if (completed.status !== "succeeded") {
|
|
401
|
+
throw new NexraTaskError(completed);
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
taskId: completed.id,
|
|
405
|
+
output: completed.output,
|
|
406
|
+
assets: completed.assets ?? [],
|
|
407
|
+
task: completed
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
function combineSignals(first, second) {
|
|
412
|
+
const signals = [first, second].filter((signal) => Boolean(signal));
|
|
413
|
+
if (signals.length === 0) return void 0;
|
|
414
|
+
if (signals.length === 1) return signals[0];
|
|
415
|
+
return AbortSignal.any(signals);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/transcription.ts
|
|
419
|
+
var NexraTranscriptionError = class extends Error {
|
|
420
|
+
constructor(code, message, nexraRequestId, closeCode, upstreamCode) {
|
|
421
|
+
super(message);
|
|
422
|
+
this.code = code;
|
|
423
|
+
this.nexraRequestId = nexraRequestId;
|
|
424
|
+
this.closeCode = closeCode;
|
|
425
|
+
this.upstreamCode = upstreamCode;
|
|
426
|
+
this.name = "NexraTranscriptionError";
|
|
427
|
+
}
|
|
428
|
+
code;
|
|
429
|
+
nexraRequestId;
|
|
430
|
+
closeCode;
|
|
431
|
+
upstreamCode;
|
|
432
|
+
};
|
|
433
|
+
function browserSocket(url, headers) {
|
|
434
|
+
if (["authorization", "x-api-key", "cookie"].some((key) => headers.has(key))) {
|
|
435
|
+
throw new NexraTranscriptionError("browser_credentials_unsupported", "Browser transcription uses a Nexra session cookie; keep API keys on your server.");
|
|
436
|
+
}
|
|
437
|
+
return new WebSocket(url);
|
|
438
|
+
}
|
|
439
|
+
var TranscriptionClient = class {
|
|
440
|
+
constructor(transport, connect = browserSocket) {
|
|
441
|
+
this.transport = transport;
|
|
442
|
+
this.connect = connect;
|
|
443
|
+
}
|
|
444
|
+
transport;
|
|
445
|
+
connect;
|
|
446
|
+
async *stream(options) {
|
|
447
|
+
const endpoint = getModelSpec(options.model)?.endpoints.find((endpoint2) => endpoint2.protocol === "volcengine-asr-ws");
|
|
448
|
+
const operation = endpoint?.httpOperations?.find((operation2) => operation2.key === (options.mode ?? "realtime"));
|
|
449
|
+
if (!operation) throw new NexraTranscriptionError("unsupported_model", "This model/mode has no supported ASR WebSocket endpoint.");
|
|
450
|
+
const configuration = structuredClone(options.configuration);
|
|
451
|
+
const audio = object(configuration.audio);
|
|
452
|
+
const request = object(configuration.request);
|
|
453
|
+
const schema = operation.messageParameters;
|
|
454
|
+
const properties = object(schema.properties);
|
|
455
|
+
const audioSchema = object(object(properties.audio).properties);
|
|
456
|
+
const requestSchema = object(object(properties.request).properties);
|
|
457
|
+
if (!Array.isArray(object(audioSchema.format).enum) || !object(audioSchema.format).enum.includes(audio.format) || request.model_name !== object(requestSchema.model_name).const) {
|
|
458
|
+
throw new NexraTranscriptionError("invalid_configuration", "Provide the model's native audio format and request.model_name.");
|
|
459
|
+
}
|
|
460
|
+
for (const key of ["rate", "bits", "channel"]) {
|
|
461
|
+
const value = audio[key];
|
|
462
|
+
const field = object(audioSchema[key]);
|
|
463
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isSafeInteger(value) || value < Number(field.minimum ?? 1) || Array.isArray(field.enum) && !field.enum.includes(value))) {
|
|
464
|
+
throw new NexraTranscriptionError("invalid_configuration", `Invalid audio.${key}.`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const all = endpoint.inputSchema;
|
|
468
|
+
for (const [value, selected, full] of [
|
|
469
|
+
[audio, audioSchema, object(object(object(all.properties).audio).properties)],
|
|
470
|
+
[request, requestSchema, object(object(object(all.properties).request).properties)]
|
|
471
|
+
]) {
|
|
472
|
+
for (const key of Object.keys(value)) if (key in full && !(key in selected)) {
|
|
473
|
+
throw new NexraTranscriptionError("invalid_configuration", `${key} is unavailable in this ASR mode.`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const controller = new AbortController();
|
|
477
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
478
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
479
|
+
if (options.signal?.aborted) abort();
|
|
480
|
+
const iterator = options.audio[Symbol.asyncIterator]();
|
|
481
|
+
let socket;
|
|
482
|
+
let timer;
|
|
483
|
+
let responseChain = Promise.resolve();
|
|
484
|
+
let notify;
|
|
485
|
+
const queue = [];
|
|
486
|
+
let queuedBytes = 0, pendingBytes = 0;
|
|
487
|
+
let failure;
|
|
488
|
+
let done = false, finalSent = false, finalReceived = false;
|
|
489
|
+
const headers = this.transport.headers();
|
|
490
|
+
const requestId = headers.get("x-nexra-client-request-id");
|
|
491
|
+
const error = (code, message, closeCode, upstreamCode) => new NexraTranscriptionError(code, message, this.connect === browserSocket ? void 0 : requestId, closeCode, upstreamCode);
|
|
492
|
+
const wake = () => {
|
|
493
|
+
notify?.();
|
|
494
|
+
notify = void 0;
|
|
495
|
+
};
|
|
496
|
+
const fail = (cause) => {
|
|
497
|
+
if (done) return;
|
|
498
|
+
failure = { cause };
|
|
499
|
+
done = true;
|
|
500
|
+
clearTimeout(timer);
|
|
501
|
+
controller.abort(cause);
|
|
502
|
+
socket?.close();
|
|
503
|
+
wake();
|
|
504
|
+
};
|
|
505
|
+
const cancelled = () => fail(controller.signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
506
|
+
controller.signal.addEventListener("abort", cancelled, { once: true });
|
|
507
|
+
const arm = (milliseconds, code) => {
|
|
508
|
+
clearTimeout(timer);
|
|
509
|
+
timer = setTimeout(() => fail(error(code, "ASR session timed out.")), milliseconds);
|
|
510
|
+
};
|
|
511
|
+
try {
|
|
512
|
+
let first;
|
|
513
|
+
do {
|
|
514
|
+
first = await abortable(iterator.next(), controller.signal);
|
|
515
|
+
} while (!first.done && first.value.byteLength === 0);
|
|
516
|
+
if (first.done) throw error("invalid_audio", "Provide at least one nonempty audio chunk.");
|
|
517
|
+
const url = new URL(this.transport.baseUrl);
|
|
518
|
+
url.pathname = url.pathname.replace(/\/v1\/?$/, "").replace(/\/$/, "") + operation.pathTemplate;
|
|
519
|
+
url.protocol = url.protocol === "https:" ? "wss:" : url.protocol === "http:" ? "ws:" : url.protocol;
|
|
520
|
+
if (!["ws:", "wss:"].includes(url.protocol) || url.username || url.password) throw error("invalid_url", "ASR requires an HTTP(S) API base URL.");
|
|
521
|
+
const organization = options.organizationId ?? headers.get(ORGANIZATION_HEADER);
|
|
522
|
+
if (organization) {
|
|
523
|
+
url.searchParams.set("organization", organization);
|
|
524
|
+
headers.set(ORGANIZATION_HEADER, organization);
|
|
525
|
+
}
|
|
526
|
+
const headerSchema = object(operation.headerParameters?.properties);
|
|
527
|
+
const resource = object(headerSchema["X-Api-Resource-Id"]).const;
|
|
528
|
+
if (typeof resource !== "string") throw error("invalid_configuration", "ASR resource metadata is missing.");
|
|
529
|
+
headers.set("x-api-resource-id", resource);
|
|
530
|
+
headers.set("x-api-request-id", requestId);
|
|
531
|
+
socket = this.connect(url, headers);
|
|
532
|
+
socket.binaryType = "arraybuffer";
|
|
533
|
+
socket.onmessage = (message) => {
|
|
534
|
+
if (done) return;
|
|
535
|
+
if (!(message.data instanceof ArrayBuffer) || (pendingBytes += message.data.byteLength) > ASR_MAX_FRAME_BYTES) {
|
|
536
|
+
fail(error("invalid_frame", "ASR response buffer exceeded its limit."));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
responseChain = responseChain.then(async () => {
|
|
540
|
+
if (done) return;
|
|
541
|
+
const frame = await decodeVolcengineAsrFrame(new Uint8Array(message.data));
|
|
542
|
+
pendingBytes -= message.data.byteLength;
|
|
543
|
+
if (done) return;
|
|
544
|
+
if (frame.type === 15) throw error("provider_rejected", "ASR provider rejected the session.", void 0, frame.errorCode);
|
|
545
|
+
if (frame.type !== 9 || finalReceived) throw error("invalid_frame", "Unexpected ASR response.");
|
|
546
|
+
const response = volcengineAsrFrameJson(frame);
|
|
547
|
+
const final = Boolean(frame.flags & 2);
|
|
548
|
+
if (final && (!finalSent || typeof response.result?.text !== "string" || typeof response.audio_info?.duration !== "number")) {
|
|
549
|
+
throw error("incomplete_result", "ASR final response lacks verified text/duration or arrived before final input.");
|
|
550
|
+
}
|
|
551
|
+
const size = new TextEncoder().encode(JSON.stringify(response)).byteLength;
|
|
552
|
+
if ((queuedBytes += size) > ASR_MAX_FRAME_BYTES) throw error("result_backpressure", "ASR results are not being consumed fast enough.");
|
|
553
|
+
finalReceived = final;
|
|
554
|
+
queue.push({ event: { final, response }, bytes: size });
|
|
555
|
+
wake();
|
|
556
|
+
}).catch(fail);
|
|
557
|
+
};
|
|
558
|
+
socket.onerror = () => {
|
|
559
|
+
if (!done) arm(1e3, "connection_failed");
|
|
560
|
+
};
|
|
561
|
+
socket.onclose = (event) => {
|
|
562
|
+
void responseChain.then(() => {
|
|
563
|
+
if (done) return;
|
|
564
|
+
if (event.code !== 1e3) fail(error("connection_closed", event.reason || "ASR connection closed unexpectedly.", event.code));
|
|
565
|
+
else if (!finalReceived) fail(error("incomplete_result", "ASR closed without a final result.", event.code));
|
|
566
|
+
else {
|
|
567
|
+
done = true;
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
controller.abort();
|
|
570
|
+
wake();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
arm(1e4, "connection_timeout");
|
|
575
|
+
await abortable(new Promise((resolve, reject) => {
|
|
576
|
+
const open = () => {
|
|
577
|
+
cleanup();
|
|
578
|
+
resolve();
|
|
579
|
+
};
|
|
580
|
+
const close = () => {
|
|
581
|
+
cleanup();
|
|
582
|
+
reject(error("connection_closed", "ASR connection closed before opening."));
|
|
583
|
+
};
|
|
584
|
+
const cleanup = () => {
|
|
585
|
+
socket.removeEventListener("open", open);
|
|
586
|
+
socket.removeEventListener("close", close);
|
|
587
|
+
controller.signal.removeEventListener("abort", cleanup);
|
|
588
|
+
};
|
|
589
|
+
socket.addEventListener("open", open, { once: true });
|
|
590
|
+
socket.addEventListener("close", close, { once: true });
|
|
591
|
+
controller.signal.addEventListener("abort", cleanup, { once: true });
|
|
592
|
+
}), controller.signal);
|
|
593
|
+
socket.send(encodeVolcengineAsrFrame(1, new TextEncoder().encode(JSON.stringify(configuration)), { json: true }));
|
|
594
|
+
options.onConnected?.();
|
|
595
|
+
controller.signal.throwIfAborted();
|
|
596
|
+
const limit = asrInputSecondsLimit(operation.pathTemplate);
|
|
597
|
+
const bytesPerSecond = Number(audio.rate ?? 16e3) * Number(audio.bits ?? 16) / 8 * Number(audio.channel ?? 1);
|
|
598
|
+
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) throw error("invalid_configuration", "Invalid audio sample configuration.");
|
|
599
|
+
let sentBytes = 0;
|
|
600
|
+
const pump = async () => {
|
|
601
|
+
let chunk = first;
|
|
602
|
+
while (!chunk.done) {
|
|
603
|
+
controller.signal.throwIfAborted();
|
|
604
|
+
const bytes = chunk.value;
|
|
605
|
+
if (!(bytes instanceof Uint8Array) || bytes.length + 8 > ASR_MAX_FRAME_BYTES) throw error("invalid_audio", "Invalid ASR audio chunk.");
|
|
606
|
+
if (bytes.length) {
|
|
607
|
+
if ((audio.format === "pcm" || audio.format === "wav") && limit !== void 0 && (sentBytes + bytes.length) / bytesPerSecond > limit + (audio.format === "wav" ? 0.01 : 0)) {
|
|
608
|
+
throw error("audio_duration_exceeded", "Sentence recognition accepts at most 60 seconds.");
|
|
609
|
+
}
|
|
610
|
+
if (socket.bufferedAmount > 1024 * 1024) throw error("audio_backpressure", "ASR audio send buffer is full.");
|
|
611
|
+
socket.send(encodeVolcengineAsrFrame(2, bytes));
|
|
612
|
+
sentBytes += bytes.length;
|
|
613
|
+
}
|
|
614
|
+
arm(1e4, "audio_timeout");
|
|
615
|
+
chunk = await abortable(iterator.next(), controller.signal);
|
|
616
|
+
}
|
|
617
|
+
finalSent = true;
|
|
618
|
+
socket.send(encodeVolcengineAsrFrame(2, new Uint8Array(), { final: true }));
|
|
619
|
+
arm(1e4, "final_timeout");
|
|
620
|
+
};
|
|
621
|
+
void pump().catch((cause) => {
|
|
622
|
+
if (!done) fail(cause);
|
|
623
|
+
});
|
|
624
|
+
for (; ; ) {
|
|
625
|
+
if (failure) throw failure.cause;
|
|
626
|
+
const next = queue.shift();
|
|
627
|
+
if (next) {
|
|
628
|
+
queuedBytes -= next.bytes;
|
|
629
|
+
yield next.event;
|
|
630
|
+
} else if (done) return;
|
|
631
|
+
else await new Promise((resolve) => {
|
|
632
|
+
notify = resolve;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
} finally {
|
|
636
|
+
done = true;
|
|
637
|
+
clearTimeout(timer);
|
|
638
|
+
controller.abort();
|
|
639
|
+
controller.signal.removeEventListener("abort", cancelled);
|
|
640
|
+
options.signal?.removeEventListener("abort", abort);
|
|
641
|
+
if (socket) {
|
|
642
|
+
socket.onmessage = null;
|
|
643
|
+
socket.onerror = null;
|
|
644
|
+
socket.onclose = null;
|
|
645
|
+
socket.close();
|
|
646
|
+
}
|
|
647
|
+
if (iterator.return) void Promise.resolve(iterator.return()).catch(() => {
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
function object(value) {
|
|
653
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
654
|
+
}
|
|
655
|
+
async function abortable(promise, signal) {
|
|
656
|
+
let abort;
|
|
657
|
+
try {
|
|
658
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
659
|
+
abort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
660
|
+
if (signal.aborted) abort();
|
|
661
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
662
|
+
})]);
|
|
663
|
+
} finally {
|
|
664
|
+
signal.removeEventListener("abort", abort);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/client.ts
|
|
669
|
+
var NexraClient = class {
|
|
670
|
+
assets;
|
|
671
|
+
catalog;
|
|
672
|
+
models;
|
|
673
|
+
tasks;
|
|
674
|
+
voices;
|
|
675
|
+
transcription;
|
|
676
|
+
constructor(options = {}, connect) {
|
|
677
|
+
const transport = new HttpTransport(options);
|
|
678
|
+
this.assets = new AssetsClient(transport);
|
|
679
|
+
this.catalog = new CatalogClient(transport);
|
|
680
|
+
this.models = new ModelsClient(transport);
|
|
681
|
+
this.tasks = new TasksClient(transport, this.assets);
|
|
682
|
+
this.voices = new VoicesClient(transport);
|
|
683
|
+
this.transcription = new TranscriptionClient(transport, connect);
|
|
684
|
+
}
|
|
685
|
+
run(model, input, options = {}) {
|
|
686
|
+
return this.tasks.run(model, input, options);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
function createNexra(options = {}) {
|
|
690
|
+
return new NexraClient(options);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/webhooks.ts
|
|
694
|
+
async function verifyWebhook(request, secret, options = {}) {
|
|
695
|
+
const webhookId = request.headers.get("nexra-webhook-id");
|
|
696
|
+
const timestamp = request.headers.get("nexra-webhook-timestamp");
|
|
697
|
+
const signatureHeader = request.headers.get("nexra-webhook-signature");
|
|
698
|
+
if (!webhookId || !timestamp || !signatureHeader) return false;
|
|
699
|
+
const timestampSeconds = Number(timestamp);
|
|
700
|
+
if (!Number.isFinite(timestampSeconds)) return false;
|
|
701
|
+
const nowSeconds = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
702
|
+
if (Math.abs(nowSeconds - timestampSeconds) > (options.toleranceSeconds ?? 300)) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
const body = await request.clone().text();
|
|
706
|
+
const signedPayload = `${webhookId}.${timestamp}.${body}`;
|
|
707
|
+
const key = await crypto.subtle.importKey(
|
|
708
|
+
"raw",
|
|
709
|
+
new TextEncoder().encode(secret),
|
|
710
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
711
|
+
false,
|
|
712
|
+
["sign"]
|
|
713
|
+
);
|
|
714
|
+
const digest = new Uint8Array(
|
|
715
|
+
await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signedPayload))
|
|
716
|
+
);
|
|
717
|
+
const expected = toBase64Url(digest);
|
|
718
|
+
const signatures = signatureHeader.split(" ").flatMap((part) => part.split(",")).map((part) => part.trim()).filter((part) => part.startsWith("v1=")).map((part) => part.slice(3));
|
|
719
|
+
return signatures.some((signature) => timingSafeEqual(signature, expected));
|
|
720
|
+
}
|
|
721
|
+
function toBase64Url(bytes) {
|
|
722
|
+
let binary = "";
|
|
723
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
724
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
725
|
+
}
|
|
726
|
+
function timingSafeEqual(left, right) {
|
|
727
|
+
if (left.length !== right.length) return false;
|
|
728
|
+
let difference = 0;
|
|
729
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
730
|
+
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
731
|
+
}
|
|
732
|
+
return difference === 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export {
|
|
736
|
+
AssetsClient,
|
|
737
|
+
ModelsClient,
|
|
738
|
+
VoicesClient,
|
|
739
|
+
TasksClient,
|
|
740
|
+
NexraTranscriptionError,
|
|
741
|
+
TranscriptionClient,
|
|
742
|
+
NexraClient,
|
|
743
|
+
createNexra,
|
|
744
|
+
verifyWebhook
|
|
745
|
+
};
|