@transcribe-api/sdk 0.1.3 → 0.1.6
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 +392 -54
- package/index.js +1143 -1354
- package/package.json +2 -2
- package/worker.js +543 -296
package/worker.js
CHANGED
|
@@ -3,12 +3,13 @@ const MAX_BATCH_FILES = 10000;
|
|
|
3
3
|
const MAX_BATCH_TOTAL_SIZE_BYTES = 10 * 1024 * 1024 * 1024;
|
|
4
4
|
const MAX_SYNC_AUDIO_BYTES = 30 * 1024 * 1024;
|
|
5
5
|
const MAX_SYNC_AUDIO_SECONDS = 10 * 60;
|
|
6
|
-
const MULTIPART_UPLOAD_THRESHOLD_BYTES =
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const MULTIPART_UPLOAD_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
7
|
+
const DEFAULT_UPLOAD_CONCURRENCY = 1;
|
|
8
|
+
const MAX_UPLOAD_CONCURRENCY = 32;
|
|
9
9
|
const MAX_MULTIPART_ADAPTIVE_ATTEMPTS = 12;
|
|
10
10
|
const MULTIPART_IDLE_WAIT_MS = 50;
|
|
11
11
|
const MIN_POLLING_INTERVAL_SECONDS = 10;
|
|
12
|
+
const DEFAULT_POLLING_SPINNER_INTERVAL_MS = 150;
|
|
12
13
|
const TERMINAL_JOB_STATUSES = new Set(["completed", "failed", "insufficient_funds"]);
|
|
13
14
|
const BATCH_MP4_UNSUPPORTED_MESSAGE = "Batch uploads do not support .mp4 for MVP. Supported batch audio formats: mp3, mpeg, mpga, m4a, wav, webm.";
|
|
14
15
|
const BATCH_UNSUPPORTED_MESSAGE = "Unsupported batch audio format. Supported batch audio formats: mp3, mpeg, mpga, m4a, wav, webm.";
|
|
@@ -82,7 +83,8 @@ async function retry(operation, { attempts = 3, baseDelayMs = 250 } = {}) {
|
|
|
82
83
|
return await operation(index);
|
|
83
84
|
} catch (error) {
|
|
84
85
|
lastError = error;
|
|
85
|
-
|
|
86
|
+
const retryable = isRetryableError(error);
|
|
87
|
+
if (!retryable || index === attempts - 1) {
|
|
86
88
|
throw error;
|
|
87
89
|
}
|
|
88
90
|
await sleep(baseDelayMs * (2 ** index));
|
|
@@ -91,17 +93,17 @@ async function retry(operation, { attempts = 3, baseDelayMs = 250 } = {}) {
|
|
|
91
93
|
throw lastError;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
function
|
|
96
|
+
function normalizeUploadConcurrency(value) {
|
|
95
97
|
if (value === undefined || value === null || value === "") {
|
|
96
|
-
return
|
|
98
|
+
return DEFAULT_UPLOAD_CONCURRENCY;
|
|
97
99
|
}
|
|
98
100
|
const parsed = Number.parseInt(String(value), 10);
|
|
99
101
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
100
|
-
throw new TranscribeAPIError("`
|
|
101
|
-
code: "
|
|
102
|
+
throw new TranscribeAPIError("`uploadConcurrency` must be an integer >= 1.", {
|
|
103
|
+
code: "invalid_upload_concurrency",
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
|
-
return Math.min(parsed,
|
|
106
|
+
return Math.min(parsed, MAX_UPLOAD_CONCURRENCY);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
function normalizePollingConfig(polling) {
|
|
@@ -152,31 +154,39 @@ function contentTypeFromName(name = "") {
|
|
|
152
154
|
return "application/octet-stream";
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
function
|
|
157
|
+
function isRemoteBatchItem(input) {
|
|
156
158
|
return Boolean(input && typeof input === "object" && typeof input.url === "string");
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
function
|
|
160
|
-
return
|
|
161
|
+
function isFilesInput(input) {
|
|
162
|
+
return Array.isArray(input);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeLanguageCode(language, fieldName = "`language`") {
|
|
166
|
+
const value = String(language || "").trim().toLowerCase();
|
|
167
|
+
if (!value || value === "auto") {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (!/^[a-z]{2}$/i.test(value)) {
|
|
171
|
+
throw new TranscribeAPIError(`${fieldName} must be a two-letter language code such as \`en\` or \`fr\`.`, {
|
|
172
|
+
code: "invalid_language",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
function defaultReferenceId(index) {
|
|
164
|
-
return
|
|
179
|
+
return String(index + 1).padStart(5, "0");
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
function normalizeBatchInputItem(item, index) {
|
|
168
183
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
169
|
-
throw new TranscribeAPIError("Each batch item must be an object with
|
|
184
|
+
throw new TranscribeAPIError("Each batch item must be an object with either `file` or `url`.", {
|
|
170
185
|
code: "invalid_batch_item",
|
|
171
186
|
});
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
const referenceId = String(item.reference_id || "").trim();
|
|
175
|
-
if (!referenceId) {
|
|
176
|
-
throw new TranscribeAPIError(`files[${index}].reference_id is required.`, {
|
|
177
|
-
code: "missing_reference_id",
|
|
178
|
-
});
|
|
179
|
-
}
|
|
189
|
+
const referenceId = String(item.reference_id || "").trim() || null;
|
|
180
190
|
|
|
181
191
|
const hasFile = Object.prototype.hasOwnProperty.call(item, "file");
|
|
182
192
|
const hasUrl = typeof item.url === "string" && item.url.trim();
|
|
@@ -196,11 +206,14 @@ function normalizeBatchInputItem(item, index) {
|
|
|
196
206
|
file: hasFile ? item.file : null,
|
|
197
207
|
url: hasUrl ? item.url.trim() : null,
|
|
198
208
|
durationEstimateSec: item.durationEstimateSec || item.duration_estimate_sec || null,
|
|
209
|
+
hasLanguage: Object.prototype.hasOwnProperty.call(item, "language"),
|
|
210
|
+
language: normalizeLanguageCode(item.language, `files[${index}].language`),
|
|
199
211
|
};
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
function uploadDescriptorForFile(referenceId, file) {
|
|
203
|
-
const descriptor = {
|
|
215
|
+
const descriptor = {};
|
|
216
|
+
if (referenceId) descriptor.reference_id = referenceId;
|
|
204
217
|
if (Number(file?.size || 0) >= MULTIPART_UPLOAD_THRESHOLD_BYTES) {
|
|
205
218
|
descriptor.size_bytes = Number(file.size || 0);
|
|
206
219
|
}
|
|
@@ -230,9 +243,10 @@ function uploadFromResponse(response, referenceId) {
|
|
|
230
243
|
}
|
|
231
244
|
|
|
232
245
|
function emitProgress(handler, event) {
|
|
233
|
-
if (typeof handler
|
|
234
|
-
|
|
246
|
+
if (typeof handler !== "function") {
|
|
247
|
+
return;
|
|
235
248
|
}
|
|
249
|
+
handler(event);
|
|
236
250
|
}
|
|
237
251
|
|
|
238
252
|
function renderProgressBar(loaded, total, width = 30) {
|
|
@@ -253,60 +267,151 @@ function formatBytes(bytes) {
|
|
|
253
267
|
}
|
|
254
268
|
|
|
255
269
|
function createSdkLoggerProgressHandler(logger = console) {
|
|
270
|
+
let activeProgressLine = false;
|
|
271
|
+
let lastRenderedLength = 0;
|
|
272
|
+
|
|
273
|
+
const writeLine = (line) => {
|
|
274
|
+
if (activeProgressLine && typeof process !== "undefined" && process?.stdout?.write) {
|
|
275
|
+
process.stdout.write("\r".padEnd(lastRenderedLength + 1, " "));
|
|
276
|
+
process.stdout.write("\n");
|
|
277
|
+
activeProgressLine = false;
|
|
278
|
+
lastRenderedLength = 0;
|
|
279
|
+
}
|
|
280
|
+
if (typeof logger?.log === "function") {
|
|
281
|
+
logger.log(line);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const writeProgress = (line) => {
|
|
286
|
+
if (typeof process !== "undefined" && process?.stdout?.write) {
|
|
287
|
+
const paddedLine = line.padEnd(lastRenderedLength, " ");
|
|
288
|
+
lastRenderedLength = paddedLine.length;
|
|
289
|
+
process.stdout.write(`\r${paddedLine}`);
|
|
290
|
+
activeProgressLine = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (typeof logger?.log === "function") {
|
|
294
|
+
logger.log(line);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
256
298
|
return (event) => {
|
|
257
|
-
if (!event || typeof event !== "object"
|
|
299
|
+
if (!event || typeof event !== "object") {
|
|
258
300
|
return;
|
|
259
301
|
}
|
|
260
302
|
if (event.event === "upload_started") {
|
|
261
|
-
|
|
303
|
+
writeLine(`Uploading ${event.uploadFiles} file(s) for ${event.jobId}`);
|
|
262
304
|
return;
|
|
263
305
|
}
|
|
264
306
|
if (event.event === "upload_progress") {
|
|
265
307
|
const loaded = Number(event.batchLoaded ?? event.loaded ?? 0);
|
|
266
308
|
const total = Number(event.batchTotal ?? event.total ?? 0);
|
|
267
309
|
const percent = total ? ((loaded / total) * 100).toFixed(1) : "0.0";
|
|
268
|
-
|
|
310
|
+
writeProgress(`Uploading ${renderProgressBar(loaded, total)} ${percent}% (${formatBytes(loaded)} / ${formatBytes(total)})`);
|
|
311
|
+
if (total && loaded >= total && activeProgressLine && typeof process !== "undefined" && process?.stdout?.write) {
|
|
312
|
+
process.stdout.write("\n");
|
|
313
|
+
activeProgressLine = false;
|
|
314
|
+
lastRenderedLength = 0;
|
|
315
|
+
}
|
|
269
316
|
return;
|
|
270
317
|
}
|
|
271
|
-
if (event.event === "upload_completed"
|
|
272
|
-
|
|
318
|
+
if (event.event === "upload_completed") {
|
|
319
|
+
if (event.suppressLog) {
|
|
320
|
+
if (activeProgressLine && typeof process !== "undefined" && process?.stdout?.write) {
|
|
321
|
+
process.stdout.write("\n");
|
|
322
|
+
activeProgressLine = false;
|
|
323
|
+
lastRenderedLength = 0;
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
writeLine(`Uploaded completed: ${JSON.stringify(event.response, null, 2)}`);
|
|
273
328
|
}
|
|
274
329
|
};
|
|
275
330
|
}
|
|
276
331
|
|
|
277
332
|
function createSdkPollingLogger(logger = console) {
|
|
278
|
-
let
|
|
333
|
+
let spinnerTimer = null;
|
|
334
|
+
let activeLine = false;
|
|
335
|
+
let currentFrameIndex = 0;
|
|
336
|
+
let currentStatus = "waiting";
|
|
337
|
+
let currentJobId = "unknown_job";
|
|
338
|
+
let lastRenderedLength = 0;
|
|
339
|
+
const frames = ["|", "/", "-", "\\"];
|
|
340
|
+
|
|
341
|
+
const stopSpinnerLine = () => {
|
|
342
|
+
if (spinnerTimer) {
|
|
343
|
+
clearInterval(spinnerTimer);
|
|
344
|
+
spinnerTimer = null;
|
|
345
|
+
}
|
|
346
|
+
if (activeLine && typeof process !== "undefined" && process?.stdout?.write) {
|
|
347
|
+
process.stdout.write("\n");
|
|
348
|
+
activeLine = false;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const renderLine = () => {
|
|
353
|
+
const frame = frames[currentFrameIndex % frames.length];
|
|
354
|
+
currentFrameIndex += 1;
|
|
355
|
+
const rawLine = `${frame} Polling ${currentJobId} (${currentStatus})`;
|
|
356
|
+
const paddedLine = rawLine.padEnd(lastRenderedLength, " ");
|
|
357
|
+
lastRenderedLength = paddedLine.length;
|
|
358
|
+
if (typeof process !== "undefined" && process?.stdout?.write) {
|
|
359
|
+
process.stdout.write(`\r${paddedLine}`);
|
|
360
|
+
activeLine = true;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (typeof logger?.log === "function") {
|
|
364
|
+
logger.log(rawLine);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const writeLine = (line) => {
|
|
369
|
+
stopSpinnerLine();
|
|
370
|
+
if (typeof logger?.log === "function") {
|
|
371
|
+
logger.log(line);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
279
375
|
return {
|
|
280
376
|
start({ jobId, jobStatus }) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (jobStatus === lastStatus) {
|
|
377
|
+
currentJobId = jobId || currentJobId;
|
|
378
|
+
currentStatus = jobStatus || currentStatus;
|
|
379
|
+
currentFrameIndex = 0;
|
|
380
|
+
if (typeof process !== "undefined" && process?.stdout?.write) {
|
|
381
|
+
renderLine();
|
|
382
|
+
spinnerTimer = setInterval(renderLine, DEFAULT_POLLING_SPINNER_INTERVAL_MS);
|
|
288
383
|
return;
|
|
289
384
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
385
|
+
writeLine(`Polling ${currentJobId} (${currentStatus})`);
|
|
386
|
+
},
|
|
387
|
+
update({ jobStatus }) {
|
|
388
|
+
currentStatus = jobStatus || currentStatus;
|
|
389
|
+
if (!spinnerTimer && !(typeof process !== "undefined" && process?.stdout?.write)) {
|
|
390
|
+
writeLine(`Polling ${currentJobId} (${currentStatus})`);
|
|
293
391
|
}
|
|
294
392
|
},
|
|
295
393
|
finish({ jobStatus, resultUrl }) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
? `Polling complete: ${jobStatus} - ${resultUrl}`
|
|
299
|
-
: `Polling complete: ${jobStatus}`);
|
|
300
|
-
}
|
|
394
|
+
currentStatus = jobStatus || currentStatus;
|
|
395
|
+
writeLine(resultUrl || `Polling complete: ${currentStatus}`);
|
|
301
396
|
},
|
|
302
397
|
timeout({ timeoutSeconds, jobStatus }) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
398
|
+
currentStatus = jobStatus || currentStatus;
|
|
399
|
+
writeLine(`Polling stopped: timeout after ${timeoutSeconds}s (${currentStatus})`);
|
|
306
400
|
},
|
|
307
401
|
};
|
|
308
402
|
}
|
|
309
403
|
|
|
404
|
+
function logTerminalAsyncResult(result, logger = console) {
|
|
405
|
+
if (typeof logger?.log !== "function" || !result || typeof result !== "object") {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (result.result_url) {
|
|
409
|
+
logger.log(String(result.result_url));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
logger.log(JSON.stringify(result, null, 2));
|
|
413
|
+
}
|
|
414
|
+
|
|
310
415
|
function composeProgressHandler({ onProgress, showLogs = false, logger = console } = {}) {
|
|
311
416
|
const logHandler = showLogs ? createSdkLoggerProgressHandler(logger) : null;
|
|
312
417
|
if (!logHandler) {
|
|
@@ -521,7 +626,14 @@ async function estimateDurationFromFile(file) {
|
|
|
521
626
|
}
|
|
522
627
|
|
|
523
628
|
try {
|
|
524
|
-
|
|
629
|
+
let bytes;
|
|
630
|
+
if (typeof file?.readSlice === "function") {
|
|
631
|
+
bytes = new Uint8Array(await file.readSlice(0, headerLength));
|
|
632
|
+
} else if (typeof file?.slice === "function") {
|
|
633
|
+
bytes = new Uint8Array(await file.slice(0, headerLength).arrayBuffer());
|
|
634
|
+
} else {
|
|
635
|
+
return estimateDurationFromSize(file?.size);
|
|
636
|
+
}
|
|
525
637
|
const type = typeFromNameOrContentType(file?.name, file?.type || contentTypeFromName(file?.name));
|
|
526
638
|
const duration = type === "wav"
|
|
527
639
|
? parseWavDuration(bytes)
|
|
@@ -538,6 +650,30 @@ async function estimateDurationFromFile(file) {
|
|
|
538
650
|
return estimateDurationFromSize(file?.size);
|
|
539
651
|
}
|
|
540
652
|
|
|
653
|
+
async function fileFromPath(pathValue) {
|
|
654
|
+
const fs = await import("node:fs/promises");
|
|
655
|
+
const path = await import("node:path");
|
|
656
|
+
const stats = await fs.stat(pathValue);
|
|
657
|
+
const name = path.basename(pathValue);
|
|
658
|
+
return {
|
|
659
|
+
name,
|
|
660
|
+
size: stats.size,
|
|
661
|
+
type: contentTypeFromName(name),
|
|
662
|
+
path: pathValue,
|
|
663
|
+
async readSlice(start, end) {
|
|
664
|
+
const handle = await fs.open(pathValue, "r");
|
|
665
|
+
try {
|
|
666
|
+
const length = Math.max(0, end - start);
|
|
667
|
+
const buffer = Buffer.alloc(length);
|
|
668
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
669
|
+
return buffer.subarray(0, bytesRead);
|
|
670
|
+
} finally {
|
|
671
|
+
await handle.close();
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
541
677
|
function makeFile(parts, name, type) {
|
|
542
678
|
if (typeof File !== "undefined") {
|
|
543
679
|
return new File(parts, name, { type });
|
|
@@ -554,14 +690,12 @@ async function normalizeFile(input, fallbackName = "audio.mp3") {
|
|
|
554
690
|
&& typeof input.name === "string"
|
|
555
691
|
&& Number.isFinite(Number(input.size))
|
|
556
692
|
&& typeof input.type === "string"
|
|
557
|
-
&& typeof input.slice === "function"
|
|
693
|
+
&& (typeof input.readSlice === "function" || typeof input.slice === "function")
|
|
558
694
|
) {
|
|
559
695
|
return input;
|
|
560
696
|
}
|
|
561
697
|
if (typeof input === "string") {
|
|
562
|
-
|
|
563
|
-
code: "unsupported_file_path",
|
|
564
|
-
});
|
|
698
|
+
return fileFromPath(input);
|
|
565
699
|
}
|
|
566
700
|
if (typeof File !== "undefined" && input instanceof File) {
|
|
567
701
|
return input;
|
|
@@ -572,7 +706,7 @@ async function normalizeFile(input, fallbackName = "audio.mp3") {
|
|
|
572
706
|
if (input?.data instanceof Uint8Array || input?.data instanceof ArrayBuffer) {
|
|
573
707
|
return makeFile([input.data], input.name || fallbackName, input.type || contentTypeFromName(input.name || fallbackName));
|
|
574
708
|
}
|
|
575
|
-
throw new TranscribeAPIError("Invalid file. Provide a File, Blob, or { data, name, type }.");
|
|
709
|
+
throw new TranscribeAPIError("Invalid file. Provide a path, File, Blob, or { data, name, type }.");
|
|
576
710
|
}
|
|
577
711
|
|
|
578
712
|
async function parseApiResponse(response) {
|
|
@@ -581,21 +715,22 @@ async function parseApiResponse(response) {
|
|
|
581
715
|
try {
|
|
582
716
|
data = text ? JSON.parse(text) : {};
|
|
583
717
|
} catch {
|
|
584
|
-
data =
|
|
718
|
+
data = text;
|
|
585
719
|
}
|
|
586
720
|
if (!response.ok) {
|
|
587
|
-
const
|
|
588
|
-
const
|
|
721
|
+
const body = data && typeof data === "object" ? data : {};
|
|
722
|
+
const message = body.message || body.error || text || `HTTP ${response.status}`;
|
|
723
|
+
const extra = { ...body };
|
|
589
724
|
delete extra.message;
|
|
590
725
|
delete extra.error;
|
|
591
726
|
delete extra.code;
|
|
592
727
|
throw new TranscribeAPIError(message, {
|
|
593
728
|
status: response.status,
|
|
594
|
-
code:
|
|
729
|
+
code: body.code || null,
|
|
595
730
|
extra,
|
|
596
731
|
response: {
|
|
597
732
|
message,
|
|
598
|
-
...(
|
|
733
|
+
...(body.code ? { code: body.code } : {}),
|
|
599
734
|
...extra,
|
|
600
735
|
},
|
|
601
736
|
});
|
|
@@ -611,6 +746,68 @@ function normalizeHeaders(headers = {}) {
|
|
|
611
746
|
);
|
|
612
747
|
}
|
|
613
748
|
|
|
749
|
+
function isNodeStreamBody(body) {
|
|
750
|
+
return Boolean(body && typeof body === "object" && typeof body.pipe === "function");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function putNodeStream(upload, streamBody, headers) {
|
|
754
|
+
const http = await import("node:http");
|
|
755
|
+
const https = await import("node:https");
|
|
756
|
+
const { pipeline } = await import("node:stream/promises");
|
|
757
|
+
const target = new URL(upload.url || upload);
|
|
758
|
+
const transport = target.protocol === "https:" ? https : http;
|
|
759
|
+
|
|
760
|
+
return new Promise((resolve, reject) => {
|
|
761
|
+
let settled = false;
|
|
762
|
+
const finishResolve = (value) => {
|
|
763
|
+
if (settled) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
settled = true;
|
|
767
|
+
resolve(value);
|
|
768
|
+
};
|
|
769
|
+
const finishReject = (error) => {
|
|
770
|
+
if (settled) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
settled = true;
|
|
774
|
+
reject(error);
|
|
775
|
+
};
|
|
776
|
+
const request = transport.request(target, {
|
|
777
|
+
method: "PUT",
|
|
778
|
+
headers,
|
|
779
|
+
agent: false,
|
|
780
|
+
}, (response) => {
|
|
781
|
+
response.resume();
|
|
782
|
+
response.on("error", finishReject);
|
|
783
|
+
response.on("end", () => {
|
|
784
|
+
finishResolve({
|
|
785
|
+
ok: response.statusCode >= 200 && response.statusCode < 300,
|
|
786
|
+
status: response.statusCode || 0,
|
|
787
|
+
headers: {
|
|
788
|
+
get(name) {
|
|
789
|
+
const value = response.headers[String(name || "").toLowerCase()];
|
|
790
|
+
return Array.isArray(value) ? value[0] : value || null;
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
request.on("error", finishReject);
|
|
798
|
+
request.on("socket", (socket) => {
|
|
799
|
+
socket.on("error", finishReject);
|
|
800
|
+
});
|
|
801
|
+
request.setTimeout(120000, () => {
|
|
802
|
+
request.destroy(new Error("stream_upload_timeout"));
|
|
803
|
+
});
|
|
804
|
+
if (typeof streamBody.on === "function") {
|
|
805
|
+
streamBody.on("error", finishReject);
|
|
806
|
+
}
|
|
807
|
+
pipeline(streamBody, request).catch(finishReject);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
614
811
|
async function putObjectWithRetry(upload, body, {
|
|
615
812
|
onProgress,
|
|
616
813
|
loadedOffset = 0,
|
|
@@ -618,17 +815,39 @@ async function putObjectWithRetry(upload, body, {
|
|
|
618
815
|
contentLength = null,
|
|
619
816
|
progressMeta = null,
|
|
620
817
|
debugContext = null,
|
|
818
|
+
uploadLimiter = null,
|
|
621
819
|
} = {}) {
|
|
622
|
-
const response = await retry(async () => {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
headers: normalizeHeaders({
|
|
820
|
+
const response = await retry(async (attemptIndex) => {
|
|
821
|
+
const executePut = async () => {
|
|
822
|
+
const resolvedBody = typeof body === "function" ? await body() : body;
|
|
823
|
+
const headers = normalizeHeaders({
|
|
627
824
|
...(upload.headers || {}),
|
|
628
825
|
...(contentLength !== null ? { "Content-Length": contentLength } : {}),
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
826
|
+
...(typeof body === "function" ? { Connection: "close" } : {}),
|
|
827
|
+
});
|
|
828
|
+
const putResponse = isNodeStreamBody(resolvedBody)
|
|
829
|
+
? await putNodeStream(upload, resolvedBody, headers)
|
|
830
|
+
: await fetch(upload.url || upload, {
|
|
831
|
+
method: "PUT",
|
|
832
|
+
headers,
|
|
833
|
+
body: resolvedBody,
|
|
834
|
+
});
|
|
835
|
+
if (!putResponse.ok) {
|
|
836
|
+
throw new TranscribeAPIError(`R2 upload failed with HTTP ${putResponse.status}.`, {
|
|
837
|
+
status: putResponse.status,
|
|
838
|
+
code: "upload_failed",
|
|
839
|
+
extra: {
|
|
840
|
+
...(progressMeta || {}),
|
|
841
|
+
...(debugContext || {}),
|
|
842
|
+
content_length: contentLength,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
return putResponse;
|
|
847
|
+
};
|
|
848
|
+
const putResponse = uploadLimiter?.withToken
|
|
849
|
+
? await uploadLimiter.withToken(executePut)
|
|
850
|
+
: await executePut();
|
|
632
851
|
if (!putResponse.ok) {
|
|
633
852
|
throw new TranscribeAPIError(`R2 upload failed with HTTP ${putResponse.status}.`, {
|
|
634
853
|
status: putResponse.status,
|
|
@@ -645,7 +864,6 @@ async function putObjectWithRetry(upload, body, {
|
|
|
645
864
|
attempts: 5,
|
|
646
865
|
baseDelayMs: 1000,
|
|
647
866
|
});
|
|
648
|
-
|
|
649
867
|
const transferredBytes = Number(
|
|
650
868
|
contentLength
|
|
651
869
|
?? body?.size
|
|
@@ -663,6 +881,14 @@ async function putObjectWithRetry(upload, body, {
|
|
|
663
881
|
return response;
|
|
664
882
|
}
|
|
665
883
|
|
|
884
|
+
async function openFileBody(file, start = 0, end = null) {
|
|
885
|
+
if (!file?.path) {
|
|
886
|
+
return file;
|
|
887
|
+
}
|
|
888
|
+
const fs = await import("node:fs");
|
|
889
|
+
return fs.createReadStream(file.path, end === null ? { start } : { start, end: end - 1 });
|
|
890
|
+
}
|
|
891
|
+
|
|
666
892
|
async function runWithConcurrency(items, concurrency, worker) {
|
|
667
893
|
let index = 0;
|
|
668
894
|
const workerCount = Math.max(1, Math.min(items.length || 1, concurrency));
|
|
@@ -679,6 +905,41 @@ async function runWithConcurrency(items, concurrency, worker) {
|
|
|
679
905
|
await Promise.all(workers);
|
|
680
906
|
}
|
|
681
907
|
|
|
908
|
+
function createUploadConcurrencyLimiter(concurrency) {
|
|
909
|
+
const maxConcurrency = Math.max(1, normalizeUploadConcurrency(concurrency));
|
|
910
|
+
let active = 0;
|
|
911
|
+
const waiters = [];
|
|
912
|
+
|
|
913
|
+
const acquire = async () => {
|
|
914
|
+
if (active < maxConcurrency) {
|
|
915
|
+
active += 1;
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
919
|
+
active += 1;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const release = () => {
|
|
923
|
+
active = Math.max(0, active - 1);
|
|
924
|
+
const next = waiters.shift();
|
|
925
|
+
if (next) {
|
|
926
|
+
next();
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
return {
|
|
931
|
+
concurrency: maxConcurrency,
|
|
932
|
+
async withToken(operation) {
|
|
933
|
+
await acquire();
|
|
934
|
+
try {
|
|
935
|
+
return await operation();
|
|
936
|
+
} finally {
|
|
937
|
+
release();
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
682
943
|
function multipartCompleteXml(parts) {
|
|
683
944
|
const rows = parts
|
|
684
945
|
.sort((a, b) => a.partNumber - b.partNumber)
|
|
@@ -689,18 +950,19 @@ function multipartCompleteXml(parts) {
|
|
|
689
950
|
|
|
690
951
|
async function uploadMultipart(upload, file, {
|
|
691
952
|
onProgress,
|
|
692
|
-
|
|
953
|
+
uploadConcurrency = DEFAULT_UPLOAD_CONCURRENCY,
|
|
693
954
|
onConcurrencyChange = null,
|
|
955
|
+
uploadLimiter = null,
|
|
694
956
|
} = {}) {
|
|
695
957
|
const completed = [];
|
|
696
|
-
const completedPartNumbers = new Set();
|
|
697
|
-
let completedBytes = 0;
|
|
698
958
|
const totalParts = upload.parts.length;
|
|
699
|
-
const
|
|
959
|
+
const completedPartNumbers = new Set(completed.map((part) => part.partNumber));
|
|
960
|
+
let completedBytes = 0;
|
|
961
|
+
const concurrency = normalizeUploadConcurrency(uploadConcurrency);
|
|
700
962
|
let targetConcurrency = concurrency;
|
|
701
963
|
let activeWorkers = 0;
|
|
702
964
|
let fatalError = null;
|
|
703
|
-
const pendingParts =
|
|
965
|
+
const pendingParts = upload.parts.filter((part) => !completedPartNumbers.has(part.part_number));
|
|
704
966
|
const partAttempts = new Map();
|
|
705
967
|
|
|
706
968
|
const workers = Array.from({ length: concurrency }, async (_, workerIndex) => {
|
|
@@ -715,7 +977,6 @@ async function uploadMultipart(upload, file, {
|
|
|
715
977
|
await sleep(MULTIPART_IDLE_WAIT_MS);
|
|
716
978
|
continue;
|
|
717
979
|
}
|
|
718
|
-
|
|
719
980
|
const part = pendingParts.shift();
|
|
720
981
|
if (!part) {
|
|
721
982
|
if (!activeWorkers) {
|
|
@@ -732,7 +993,7 @@ async function uploadMultipart(upload, file, {
|
|
|
732
993
|
try {
|
|
733
994
|
const response = await putObjectWithRetry(
|
|
734
995
|
{ url: part.url },
|
|
735
|
-
file.slice(start, end),
|
|
996
|
+
file.path ? (() => openFileBody(file, start, end)) : file.slice(start, end),
|
|
736
997
|
{
|
|
737
998
|
onProgress: null,
|
|
738
999
|
loadedOffset: 0,
|
|
@@ -750,6 +1011,7 @@ async function uploadMultipart(upload, file, {
|
|
|
750
1011
|
range_start: start,
|
|
751
1012
|
range_end_exclusive: end,
|
|
752
1013
|
},
|
|
1014
|
+
uploadLimiter,
|
|
753
1015
|
},
|
|
754
1016
|
);
|
|
755
1017
|
const etag = response.headers.get("ETag") || response.headers.get("etag");
|
|
@@ -773,7 +1035,8 @@ async function uploadMultipart(upload, file, {
|
|
|
773
1035
|
} catch (error) {
|
|
774
1036
|
const attempts = (partAttempts.get(part.part_number) || 0) + 1;
|
|
775
1037
|
partAttempts.set(part.part_number, attempts);
|
|
776
|
-
|
|
1038
|
+
const retryable = isRetryableError(error);
|
|
1039
|
+
if (retryable && attempts < MAX_MULTIPART_ADAPTIVE_ATTEMPTS) {
|
|
777
1040
|
const previousConcurrency = targetConcurrency;
|
|
778
1041
|
targetConcurrency = Math.max(1, Math.floor(targetConcurrency / 2));
|
|
779
1042
|
pendingParts.push(part);
|
|
@@ -794,19 +1057,32 @@ async function uploadMultipart(upload, file, {
|
|
|
794
1057
|
await sleep(Math.min(10000, 1000 * (2 ** Math.min(attempts - 1, 3))));
|
|
795
1058
|
continue;
|
|
796
1059
|
}
|
|
797
|
-
fatalError = new TranscribeAPIError(
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1060
|
+
fatalError = new TranscribeAPIError(
|
|
1061
|
+
`Multipart upload failed for part ${part.part_number}/${totalParts}.`,
|
|
1062
|
+
{
|
|
1063
|
+
status: error?.status || null,
|
|
1064
|
+
code: error?.code || "multipart_part_upload_failed",
|
|
1065
|
+
extra: {
|
|
1066
|
+
cause_error: error?.error || error?.message || String(error),
|
|
1067
|
+
cause_code: error?.code || error?.cause?.code || null,
|
|
1068
|
+
cause_status: error?.status || null,
|
|
1069
|
+
upload_type: "multipart",
|
|
1070
|
+
part_number: part.part_number,
|
|
1071
|
+
total_parts: totalParts,
|
|
1072
|
+
chunk_bytes: chunkSize,
|
|
1073
|
+
completed_bytes_before_failure: completedBytes,
|
|
1074
|
+
multipart_concurrency: targetConcurrency,
|
|
1075
|
+
file_name: file?.name || null,
|
|
1076
|
+
file_size: file?.size || null,
|
|
1077
|
+
range_start: start,
|
|
1078
|
+
range_end_exclusive: end,
|
|
1079
|
+
attempts,
|
|
1080
|
+
},
|
|
804
1081
|
},
|
|
805
|
-
|
|
1082
|
+
);
|
|
806
1083
|
activeWorkers -= 1;
|
|
807
1084
|
return;
|
|
808
1085
|
}
|
|
809
|
-
|
|
810
1086
|
activeWorkers -= 1;
|
|
811
1087
|
}
|
|
812
1088
|
});
|
|
@@ -837,7 +1113,8 @@ async function uploadUsingInstructions(upload, file, options = {}) {
|
|
|
837
1113
|
if (upload.type === "multipart") {
|
|
838
1114
|
return uploadMultipart(upload, file, options);
|
|
839
1115
|
}
|
|
840
|
-
|
|
1116
|
+
const body = file.path ? (() => openFileBody(file)) : file;
|
|
1117
|
+
return putObjectWithRetry(upload, body, {
|
|
841
1118
|
onProgress: options.onProgress,
|
|
842
1119
|
loadedOffset: 0,
|
|
843
1120
|
totalBytes: file.size,
|
|
@@ -850,6 +1127,7 @@ async function uploadUsingInstructions(upload, file, options = {}) {
|
|
|
850
1127
|
file_name: file?.name || null,
|
|
851
1128
|
file_size: file?.size || null,
|
|
852
1129
|
},
|
|
1130
|
+
uploadLimiter: options.uploadLimiter || null,
|
|
853
1131
|
});
|
|
854
1132
|
}
|
|
855
1133
|
|
|
@@ -866,66 +1144,6 @@ function assertBatchLimits(totalFiles, totalSizeBytes) {
|
|
|
866
1144
|
}
|
|
867
1145
|
}
|
|
868
1146
|
|
|
869
|
-
class BigFileJob {
|
|
870
|
-
constructor(client, file, createResponse, options) {
|
|
871
|
-
this.client = client;
|
|
872
|
-
this.file = file;
|
|
873
|
-
this.createResponse = createResponse;
|
|
874
|
-
this.referenceId = options.referenceId || defaultReferenceId(0);
|
|
875
|
-
this.jobId = createResponse.job_id;
|
|
876
|
-
this.jobStatus = createResponse.job_status;
|
|
877
|
-
this.model = createResponse.model || options.model || null;
|
|
878
|
-
this.uploadInfo = uploadFromResponse(createResponse, this.referenceId);
|
|
879
|
-
this.options = options;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
async upload({ onProgress } = {}) {
|
|
883
|
-
const progress = onProgress || this.options.onProgress;
|
|
884
|
-
if (!this.uploadInfo) {
|
|
885
|
-
return this.createResponse;
|
|
886
|
-
}
|
|
887
|
-
await uploadUsingInstructions(this.uploadInfo, this.file, {
|
|
888
|
-
onProgress: progress,
|
|
889
|
-
multipartConcurrency: this.options.multipartConcurrency,
|
|
890
|
-
onConcurrencyChange: (event) => {
|
|
891
|
-
if (typeof onProgress === "function" || typeof this.options.onProgress === "function") {
|
|
892
|
-
(onProgress || this.options.onProgress)({
|
|
893
|
-
uploadType: "multipart",
|
|
894
|
-
event: "adaptive_concurrency",
|
|
895
|
-
previousConcurrency: event.previousConcurrency,
|
|
896
|
-
multipartConcurrency: event.nextConcurrency,
|
|
897
|
-
partNumber: event.partNumber,
|
|
898
|
-
attempts: event.attempts,
|
|
899
|
-
error: event.error,
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
},
|
|
903
|
-
});
|
|
904
|
-
const completion = await this.client.jobs.complete(this.jobId);
|
|
905
|
-
emitProgress(progress, {
|
|
906
|
-
event: "upload_completed",
|
|
907
|
-
jobId: this.jobId,
|
|
908
|
-
response: completion,
|
|
909
|
-
suppressLog: Boolean(this.client.polling),
|
|
910
|
-
});
|
|
911
|
-
return completion;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
class RemoteBigFileJob {
|
|
916
|
-
constructor(client, createResponse, options) {
|
|
917
|
-
this.client = client;
|
|
918
|
-
this.createResponse = createResponse;
|
|
919
|
-
this.jobId = createResponse.job_id;
|
|
920
|
-
this.jobStatus = createResponse.job_status;
|
|
921
|
-
this.model = createResponse.model || options.model || null;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
async upload() {
|
|
925
|
-
return this.createResponse;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
1147
|
class BatchJob {
|
|
930
1148
|
constructor(client, files, createResponse, options) {
|
|
931
1149
|
this.client = client;
|
|
@@ -949,6 +1167,7 @@ class BatchJob {
|
|
|
949
1167
|
const totalBytes = this.files.reduce((sum, item) => sum + Number(item?.file?.size || 0), 0);
|
|
950
1168
|
const loadedByReferenceId = new Map();
|
|
951
1169
|
const uploadableFiles = this.files.filter((item) => item.file);
|
|
1170
|
+
const uploadLimiter = this.options.uploadLimiter || createUploadConcurrencyLimiter(this.options.uploadConcurrency);
|
|
952
1171
|
|
|
953
1172
|
emitProgress(progress, {
|
|
954
1173
|
event: "upload_started",
|
|
@@ -958,10 +1177,7 @@ class BatchJob {
|
|
|
958
1177
|
totalBytes,
|
|
959
1178
|
});
|
|
960
1179
|
|
|
961
|
-
|
|
962
|
-
if (!item.file) {
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
1180
|
+
await runWithConcurrency(uploadableFiles, this.options.uploadConcurrency, async (item) => {
|
|
965
1181
|
const upload = this.uploadsByReferenceId.get(item.referenceId);
|
|
966
1182
|
if (!upload) {
|
|
967
1183
|
throw new TranscribeAPIError(`Missing upload instructions for \`${item.referenceId}\`.`, {
|
|
@@ -969,6 +1185,8 @@ class BatchJob {
|
|
|
969
1185
|
});
|
|
970
1186
|
}
|
|
971
1187
|
await uploadUsingInstructions(upload, item.file, {
|
|
1188
|
+
uploadConcurrency: this.options.uploadConcurrency,
|
|
1189
|
+
uploadLimiter,
|
|
972
1190
|
onProgress: (event) => {
|
|
973
1191
|
const fileLoaded = Math.max(0, Math.min(Number(event?.loaded || 0), Number(item.file.size || 0)));
|
|
974
1192
|
loadedByReferenceId.set(item.referenceId, fileLoaded);
|
|
@@ -987,8 +1205,7 @@ class BatchJob {
|
|
|
987
1205
|
});
|
|
988
1206
|
},
|
|
989
1207
|
});
|
|
990
|
-
}
|
|
991
|
-
|
|
1208
|
+
});
|
|
992
1209
|
const completion = await this.client.jobs.complete(this.jobId);
|
|
993
1210
|
emitProgress(progress, {
|
|
994
1211
|
event: "upload_completed",
|
|
@@ -998,7 +1215,7 @@ class BatchJob {
|
|
|
998
1215
|
batchTotal: totalBytes,
|
|
999
1216
|
totalFiles: this.files.length,
|
|
1000
1217
|
uploadFiles: uploadableFiles.length,
|
|
1001
|
-
suppressLog: Boolean(this.client.polling),
|
|
1218
|
+
suppressLog: Boolean(this.client.polling) && !TERMINAL_JOB_STATUSES.has(String(completion?.job_status || "")),
|
|
1002
1219
|
});
|
|
1003
1220
|
return completion;
|
|
1004
1221
|
}
|
|
@@ -1008,7 +1225,7 @@ export class TranscribeAPI {
|
|
|
1008
1225
|
constructor({
|
|
1009
1226
|
apiKey,
|
|
1010
1227
|
baseUrl = DEFAULT_BASE_URL,
|
|
1011
|
-
|
|
1228
|
+
uploadConcurrency = DEFAULT_UPLOAD_CONCURRENCY,
|
|
1012
1229
|
showLogs = false,
|
|
1013
1230
|
logger = console,
|
|
1014
1231
|
polling = null,
|
|
@@ -1019,13 +1236,10 @@ export class TranscribeAPI {
|
|
|
1019
1236
|
|
|
1020
1237
|
this.apiKey = apiKey;
|
|
1021
1238
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1022
|
-
this.
|
|
1239
|
+
this.uploadConcurrency = normalizeUploadConcurrency(uploadConcurrency);
|
|
1023
1240
|
this.showLogs = Boolean(showLogs);
|
|
1024
1241
|
this.logger = logger || console;
|
|
1025
1242
|
this.polling = normalizePollingConfig(polling);
|
|
1026
|
-
this.batch = {
|
|
1027
|
-
transcribe: (options) => this.transcribeMany(options),
|
|
1028
|
-
};
|
|
1029
1243
|
this.jobs = {
|
|
1030
1244
|
createBigFile: (options) => this.createBigFileJob(options),
|
|
1031
1245
|
createBatch: (options) => this.createBatchJob(options),
|
|
@@ -1063,16 +1277,16 @@ export class TranscribeAPI {
|
|
|
1063
1277
|
}
|
|
1064
1278
|
|
|
1065
1279
|
const startedAt = Date.now();
|
|
1066
|
-
const
|
|
1280
|
+
const spinner = showLogs ? createSdkPollingLogger(logger) : null;
|
|
1067
1281
|
let lastJob = initialJob || null;
|
|
1068
|
-
|
|
1282
|
+
spinner?.start({
|
|
1069
1283
|
jobId,
|
|
1070
1284
|
jobStatus: lastJob?.job_status || "waiting",
|
|
1071
1285
|
});
|
|
1072
1286
|
|
|
1073
1287
|
while (!TERMINAL_JOB_STATUSES.has(String(lastJob?.job_status || ""))) {
|
|
1074
1288
|
if (effectivePolling.timeout !== null && (Date.now() - startedAt) >= effectivePolling.timeout * 1000) {
|
|
1075
|
-
|
|
1289
|
+
spinner?.timeout({
|
|
1076
1290
|
timeoutSeconds: effectivePolling.timeout,
|
|
1077
1291
|
jobStatus: lastJob?.job_status || "unknown",
|
|
1078
1292
|
});
|
|
@@ -1089,13 +1303,12 @@ export class TranscribeAPI {
|
|
|
1089
1303
|
|
|
1090
1304
|
await sleep(effectivePolling.interval * 1000);
|
|
1091
1305
|
lastJob = await this.jobs.get(jobId);
|
|
1092
|
-
|
|
1093
|
-
jobId,
|
|
1306
|
+
spinner?.update({
|
|
1094
1307
|
jobStatus: lastJob?.job_status || "unknown",
|
|
1095
1308
|
});
|
|
1096
1309
|
}
|
|
1097
1310
|
|
|
1098
|
-
|
|
1311
|
+
spinner?.finish({
|
|
1099
1312
|
jobStatus: lastJob?.job_status || "unknown",
|
|
1100
1313
|
resultUrl: lastJob?.result_url || null,
|
|
1101
1314
|
});
|
|
@@ -1103,96 +1316,152 @@ export class TranscribeAPI {
|
|
|
1103
1316
|
}
|
|
1104
1317
|
|
|
1105
1318
|
async transcribe({
|
|
1106
|
-
|
|
1319
|
+
files,
|
|
1107
1320
|
webhookUrl,
|
|
1108
1321
|
onProgress,
|
|
1109
1322
|
showLogs,
|
|
1110
1323
|
logger,
|
|
1111
1324
|
language,
|
|
1112
|
-
task,
|
|
1113
|
-
vadFilter,
|
|
1114
|
-
initialPrompt,
|
|
1115
|
-
vttGranularity,
|
|
1116
1325
|
exclude,
|
|
1117
|
-
|
|
1326
|
+
uploadConcurrency,
|
|
1118
1327
|
} = {}) {
|
|
1119
1328
|
const progress = composeProgressHandler({
|
|
1120
1329
|
onProgress,
|
|
1121
1330
|
showLogs: showLogs ?? this.showLogs,
|
|
1122
1331
|
logger: logger ?? this.logger,
|
|
1123
1332
|
});
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
file,
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1333
|
+
if (isFilesInput(files)) {
|
|
1334
|
+
if (files.length === 0) {
|
|
1335
|
+
throw new TranscribeAPIError("Transcribe requires at least one file.", {
|
|
1336
|
+
code: "invalid_files",
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
if (files.length > 1) {
|
|
1340
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
1341
|
+
if (!String(files[index]?.reference_id || "").trim()) {
|
|
1342
|
+
throw new TranscribeAPIError(`files[${index}].reference_id is required when sending multiple files.`, {
|
|
1343
|
+
code: "missing_reference_id",
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return this._transcribeAsync({
|
|
1348
|
+
files,
|
|
1349
|
+
webhookUrl,
|
|
1350
|
+
durationEstimateSec: undefined,
|
|
1351
|
+
onProgress,
|
|
1352
|
+
showLogs,
|
|
1137
1353
|
logger: logger ?? this.logger,
|
|
1138
|
-
|
|
1354
|
+
uploadConcurrency,
|
|
1355
|
+
language,
|
|
1356
|
+
exclude,
|
|
1139
1357
|
});
|
|
1140
1358
|
}
|
|
1141
|
-
return result;
|
|
1142
|
-
}
|
|
1143
1359
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1360
|
+
const normalizedItem = normalizeBatchInputItem(files[0], 0);
|
|
1361
|
+
const normalizedSingleFile = normalizedItem.url ? null : await normalizeFile(normalizedItem.file);
|
|
1362
|
+
const estimatedDurationSec = normalizedItem.url
|
|
1363
|
+
? normalizedItem.durationEstimateSec
|
|
1364
|
+
: (normalizedItem.durationEstimateSec || await estimateDurationFromFile(normalizedSingleFile));
|
|
1365
|
+
const effectiveLanguage = normalizedItem.hasLanguage ? normalizedItem.language : normalizeLanguageCode(language);
|
|
1366
|
+
const isAsync = files.length > 1
|
|
1367
|
+
|| Boolean(normalizedItem.url)
|
|
1368
|
+
|| Boolean(webhookUrl)
|
|
1369
|
+
|| (
|
|
1370
|
+
normalizedSingleFile
|
|
1371
|
+
&& (
|
|
1372
|
+
normalizedSingleFile.size > MAX_SYNC_AUDIO_BYTES
|
|
1373
|
+
|| estimatedDurationSec > MAX_SYNC_AUDIO_SECONDS
|
|
1374
|
+
)
|
|
1375
|
+
);
|
|
1376
|
+
if (isAsync) {
|
|
1377
|
+
return this._transcribeAsync({
|
|
1378
|
+
files,
|
|
1379
|
+
webhookUrl,
|
|
1380
|
+
durationEstimateSec: estimatedDurationSec,
|
|
1381
|
+
onProgress,
|
|
1382
|
+
showLogs,
|
|
1160
1383
|
logger: logger ?? this.logger,
|
|
1161
|
-
|
|
1384
|
+
uploadConcurrency,
|
|
1385
|
+
language,
|
|
1386
|
+
exclude,
|
|
1162
1387
|
});
|
|
1163
1388
|
}
|
|
1164
|
-
|
|
1389
|
+
|
|
1390
|
+
return this.transcribeDirect({
|
|
1391
|
+
file: normalizedSingleFile,
|
|
1392
|
+
referenceId: normalizedItem.referenceId,
|
|
1393
|
+
language: effectiveLanguage,
|
|
1394
|
+
exclude,
|
|
1395
|
+
webhookUrl,
|
|
1396
|
+
showLogs: showLogs ?? this.showLogs,
|
|
1397
|
+
logger: logger ?? this.logger,
|
|
1398
|
+
});
|
|
1165
1399
|
}
|
|
1400
|
+
throw new TranscribeAPIError("`transcribe` requires a `files` array.", {
|
|
1401
|
+
code: "invalid_files",
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1166
1404
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1405
|
+
async _transcribeAsync({
|
|
1406
|
+
files,
|
|
1407
|
+
webhookUrl,
|
|
1408
|
+
durationEstimateSec,
|
|
1409
|
+
onProgress,
|
|
1410
|
+
showLogs,
|
|
1411
|
+
logger,
|
|
1412
|
+
uploadConcurrency,
|
|
1413
|
+
language,
|
|
1414
|
+
exclude,
|
|
1415
|
+
} = {}) {
|
|
1416
|
+
const progress = composeProgressHandler({
|
|
1417
|
+
onProgress,
|
|
1418
|
+
showLogs: showLogs ?? this.showLogs,
|
|
1419
|
+
logger: logger ?? this.logger,
|
|
1420
|
+
});
|
|
1421
|
+
const job = await this.createBatchJob({
|
|
1422
|
+
files,
|
|
1423
|
+
webhookUrl,
|
|
1424
|
+
durationEstimateSec,
|
|
1425
|
+
onProgress: progress,
|
|
1426
|
+
showLogs: false,
|
|
1427
|
+
uploadConcurrency,
|
|
1169
1428
|
language,
|
|
1170
|
-
task,
|
|
1171
|
-
vadFilter,
|
|
1172
|
-
initialPrompt,
|
|
1173
|
-
vttGranularity,
|
|
1174
1429
|
exclude,
|
|
1175
1430
|
});
|
|
1431
|
+
const result = await job.upload({ onProgress: progress });
|
|
1432
|
+
if (this.polling && !TERMINAL_JOB_STATUSES.has(String(result?.job_status || ""))) {
|
|
1433
|
+
return this.waitForJobCompletion(result.job_id, {
|
|
1434
|
+
polling: this.polling,
|
|
1435
|
+
showLogs: showLogs ?? this.showLogs,
|
|
1436
|
+
logger: logger ?? this.logger,
|
|
1437
|
+
initialJob: result,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
if ((showLogs ?? this.showLogs) && TERMINAL_JOB_STATUSES.has(String(result?.job_status || ""))) {
|
|
1441
|
+
logTerminalAsyncResult(result, logger ?? this.logger);
|
|
1442
|
+
}
|
|
1443
|
+
return result;
|
|
1176
1444
|
}
|
|
1177
1445
|
|
|
1178
1446
|
async transcribeDirect({
|
|
1179
1447
|
file,
|
|
1448
|
+
referenceId,
|
|
1180
1449
|
language,
|
|
1181
|
-
task,
|
|
1182
|
-
vadFilter,
|
|
1183
|
-
initialPrompt,
|
|
1184
|
-
vttGranularity,
|
|
1185
1450
|
exclude,
|
|
1451
|
+
webhookUrl,
|
|
1452
|
+
showLogs = false,
|
|
1453
|
+
logger = console,
|
|
1186
1454
|
} = {}) {
|
|
1187
1455
|
const normalizedFile = await normalizeFile(file);
|
|
1456
|
+
const directFile = normalizedFile.path
|
|
1457
|
+
? makeFile([await normalizedFile.readSlice(0, normalizedFile.size)], normalizedFile.name, normalizedFile.type)
|
|
1458
|
+
: normalizedFile;
|
|
1188
1459
|
const form = new FormData();
|
|
1189
|
-
form.set("
|
|
1460
|
+
if (String(referenceId || "").trim()) form.set("reference_id", String(referenceId).trim());
|
|
1461
|
+
form.set("file", directFile, directFile.name);
|
|
1190
1462
|
if (language) form.set("language", language);
|
|
1191
|
-
if (task) form.set("task", task);
|
|
1192
|
-
if (vadFilter !== undefined) form.set("vad_filter", String(Boolean(vadFilter)));
|
|
1193
|
-
if (initialPrompt) form.set("initial_prompt", initialPrompt);
|
|
1194
|
-
if (vttGranularity) form.set("vtt_granularity", vttGranularity);
|
|
1195
1463
|
if (exclude) form.set("exclude", Array.isArray(exclude) ? exclude.join(",") : exclude);
|
|
1464
|
+
if (webhookUrl) form.set("webhook_url", webhookUrl);
|
|
1196
1465
|
|
|
1197
1466
|
const response = await fetch(`${this.baseUrl}/transcribe`, {
|
|
1198
1467
|
method: "POST",
|
|
@@ -1201,7 +1470,19 @@ export class TranscribeAPI {
|
|
|
1201
1470
|
},
|
|
1202
1471
|
body: form,
|
|
1203
1472
|
});
|
|
1204
|
-
|
|
1473
|
+
const result = await parseApiResponse(response);
|
|
1474
|
+
if (this.polling && result && typeof result === "object" && result.job_status && !TERMINAL_JOB_STATUSES.has(String(result.job_status))) {
|
|
1475
|
+
return this.waitForJobCompletion(result.job_id, {
|
|
1476
|
+
polling: this.polling,
|
|
1477
|
+
showLogs,
|
|
1478
|
+
logger,
|
|
1479
|
+
initialJob: result,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
if (showLogs && typeof logger?.log === "function") {
|
|
1483
|
+
logger.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
|
|
1484
|
+
}
|
|
1485
|
+
return result;
|
|
1205
1486
|
}
|
|
1206
1487
|
|
|
1207
1488
|
async transcribeMany({
|
|
@@ -1211,31 +1492,21 @@ export class TranscribeAPI {
|
|
|
1211
1492
|
onProgress,
|
|
1212
1493
|
showLogs,
|
|
1213
1494
|
logger,
|
|
1214
|
-
|
|
1495
|
+
uploadConcurrency,
|
|
1496
|
+
language,
|
|
1497
|
+
exclude,
|
|
1215
1498
|
} = {}) {
|
|
1216
|
-
|
|
1217
|
-
onProgress,
|
|
1218
|
-
showLogs: showLogs ?? this.showLogs,
|
|
1219
|
-
logger: logger ?? this.logger,
|
|
1220
|
-
});
|
|
1221
|
-
const job = await this.createBatchJob({
|
|
1499
|
+
return this._transcribeAsync({
|
|
1222
1500
|
files,
|
|
1223
1501
|
webhookUrl,
|
|
1224
1502
|
durationEstimateSec,
|
|
1225
|
-
onProgress
|
|
1226
|
-
showLogs
|
|
1227
|
-
|
|
1503
|
+
onProgress,
|
|
1504
|
+
showLogs,
|
|
1505
|
+
logger: logger ?? this.logger,
|
|
1506
|
+
uploadConcurrency,
|
|
1507
|
+
language,
|
|
1508
|
+
exclude,
|
|
1228
1509
|
});
|
|
1229
|
-
const result = await job.upload({ onProgress: progress });
|
|
1230
|
-
if (this.polling && !TERMINAL_JOB_STATUSES.has(String(result?.job_status || ""))) {
|
|
1231
|
-
return this.waitForJobCompletion(result.job_id, {
|
|
1232
|
-
polling: this.polling,
|
|
1233
|
-
showLogs: showLogs ?? this.showLogs,
|
|
1234
|
-
logger: logger ?? this.logger,
|
|
1235
|
-
initialJob: result,
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
return result;
|
|
1239
1510
|
}
|
|
1240
1511
|
|
|
1241
1512
|
async createBigFileJob({
|
|
@@ -1245,57 +1516,20 @@ export class TranscribeAPI {
|
|
|
1245
1516
|
onProgress,
|
|
1246
1517
|
showLogs,
|
|
1247
1518
|
logger,
|
|
1248
|
-
|
|
1519
|
+
uploadConcurrency,
|
|
1520
|
+
language,
|
|
1521
|
+
exclude,
|
|
1249
1522
|
} = {}) {
|
|
1250
|
-
|
|
1523
|
+
return this.createBatchJob({
|
|
1524
|
+
files: [file],
|
|
1525
|
+
webhookUrl,
|
|
1526
|
+
durationEstimateSec,
|
|
1251
1527
|
onProgress,
|
|
1252
|
-
showLogs
|
|
1528
|
+
showLogs,
|
|
1253
1529
|
logger: logger ?? this.logger,
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const response = await this.requestJson("/transcribe", {
|
|
1258
|
-
method: "POST",
|
|
1259
|
-
body: {
|
|
1260
|
-
files: [{
|
|
1261
|
-
reference_id: defaultReferenceId(0),
|
|
1262
|
-
url: file.url,
|
|
1263
|
-
}],
|
|
1264
|
-
...(webhookUrl ? { webhook_url: webhookUrl } : {}),
|
|
1265
|
-
},
|
|
1266
|
-
retryable: true,
|
|
1267
|
-
});
|
|
1268
|
-
return new RemoteBigFileJob(this, response, {
|
|
1269
|
-
onProgress: progress,
|
|
1270
|
-
multipartConcurrency: normalizedConcurrency,
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
const normalizedFile = await normalizeFile(file);
|
|
1275
|
-
const estimate = durationEstimateSec || await estimateDurationFromFile(normalizedFile);
|
|
1276
|
-
const response = await this.requestJson("/transcribe", {
|
|
1277
|
-
method: "POST",
|
|
1278
|
-
body: {
|
|
1279
|
-
files: [
|
|
1280
|
-
estimate > MAX_SYNC_AUDIO_SECONDS || normalizedFile.size > MAX_SYNC_AUDIO_BYTES
|
|
1281
|
-
? uploadDescriptorForFile(defaultReferenceId(0), normalizedFile)
|
|
1282
|
-
: { reference_id: defaultReferenceId(0) },
|
|
1283
|
-
],
|
|
1284
|
-
...(webhookUrl ? { webhook_url: webhookUrl } : {}),
|
|
1285
|
-
},
|
|
1286
|
-
retryable: true,
|
|
1287
|
-
});
|
|
1288
|
-
emitProgress(progress, {
|
|
1289
|
-
event: "upload_urls_received",
|
|
1290
|
-
jobId: response.job_id,
|
|
1291
|
-
jobStatus: response.job_status,
|
|
1292
|
-
uploadCount: normalizeResponseUploads(response).length,
|
|
1293
|
-
totalFiles: 1,
|
|
1294
|
-
});
|
|
1295
|
-
return new BigFileJob(this, normalizedFile, response, {
|
|
1296
|
-
onProgress: progress,
|
|
1297
|
-
multipartConcurrency: normalizedConcurrency,
|
|
1298
|
-
referenceId: defaultReferenceId(0),
|
|
1530
|
+
uploadConcurrency,
|
|
1531
|
+
language,
|
|
1532
|
+
exclude,
|
|
1299
1533
|
});
|
|
1300
1534
|
}
|
|
1301
1535
|
|
|
@@ -1306,13 +1540,16 @@ export class TranscribeAPI {
|
|
|
1306
1540
|
onProgress,
|
|
1307
1541
|
showLogs,
|
|
1308
1542
|
logger,
|
|
1309
|
-
|
|
1543
|
+
uploadConcurrency,
|
|
1544
|
+
language,
|
|
1545
|
+
exclude,
|
|
1310
1546
|
} = {}) {
|
|
1311
1547
|
const progress = composeProgressHandler({
|
|
1312
1548
|
onProgress,
|
|
1313
1549
|
showLogs: showLogs ?? this.showLogs,
|
|
1314
1550
|
logger: logger ?? this.logger,
|
|
1315
1551
|
});
|
|
1552
|
+
const normalizedLanguage = normalizeLanguageCode(language);
|
|
1316
1553
|
if (!Array.isArray(files) || files.length === 0) {
|
|
1317
1554
|
throw new TranscribeAPIError("Batch upload requires at least one file.", { code: "invalid_files" });
|
|
1318
1555
|
}
|
|
@@ -1328,17 +1565,21 @@ export class TranscribeAPI {
|
|
|
1328
1565
|
referenceId: item.referenceId,
|
|
1329
1566
|
url: item.url,
|
|
1330
1567
|
durationEstimateSec: item.durationEstimateSec || durationEstimateSec || null,
|
|
1568
|
+
hasLanguage: item.hasLanguage,
|
|
1569
|
+
language: item.language,
|
|
1331
1570
|
});
|
|
1332
1571
|
continue;
|
|
1333
1572
|
}
|
|
1334
1573
|
|
|
1335
|
-
const
|
|
1336
|
-
assertSupportedBatchFormat(
|
|
1574
|
+
const file = await normalizeFile(item.file, `file_${String(index + 1).padStart(6, "0")}.mp3`);
|
|
1575
|
+
assertSupportedBatchFormat(file);
|
|
1337
1576
|
normalizedBatchItems.push({
|
|
1338
|
-
file
|
|
1339
|
-
referenceId: item.referenceId,
|
|
1577
|
+
file,
|
|
1578
|
+
referenceId: item.referenceId || defaultReferenceId(index),
|
|
1340
1579
|
url: null,
|
|
1341
|
-
durationEstimateSec: item.durationEstimateSec || await estimateDurationFromFile(
|
|
1580
|
+
durationEstimateSec: item.durationEstimateSec || await estimateDurationFromFile(file),
|
|
1581
|
+
hasLanguage: item.hasLanguage,
|
|
1582
|
+
language: item.language,
|
|
1342
1583
|
});
|
|
1343
1584
|
}
|
|
1344
1585
|
|
|
@@ -1350,12 +1591,18 @@ export class TranscribeAPI {
|
|
|
1350
1591
|
files: normalizedBatchItems.map((item) => (
|
|
1351
1592
|
item.url
|
|
1352
1593
|
? {
|
|
1353
|
-
reference_id: item.referenceId,
|
|
1594
|
+
...(item.referenceId ? { reference_id: item.referenceId } : {}),
|
|
1354
1595
|
url: item.url,
|
|
1596
|
+
...(item.hasLanguage ? { language: item.language } : {}),
|
|
1597
|
+
}
|
|
1598
|
+
: {
|
|
1599
|
+
...uploadDescriptorForFile(item.referenceId, item.file),
|
|
1600
|
+
...(item.hasLanguage ? { language: item.language } : {}),
|
|
1355
1601
|
}
|
|
1356
|
-
: uploadDescriptorForFile(item.referenceId, item.file)
|
|
1357
1602
|
)),
|
|
1603
|
+
...(normalizedLanguage ? { language: normalizedLanguage } : {}),
|
|
1358
1604
|
...(webhookUrl ? { webhook_url: webhookUrl } : {}),
|
|
1605
|
+
...(exclude ? { exclude: Array.isArray(exclude) ? exclude.join(",") : exclude } : {}),
|
|
1359
1606
|
},
|
|
1360
1607
|
retryable: true,
|
|
1361
1608
|
});
|
|
@@ -1369,7 +1616,7 @@ export class TranscribeAPI {
|
|
|
1369
1616
|
return new BatchJob(this, normalizedBatchItems, response, {
|
|
1370
1617
|
onProgress: progress,
|
|
1371
1618
|
webhookUrl,
|
|
1372
|
-
|
|
1619
|
+
uploadConcurrency: normalizeUploadConcurrency(uploadConcurrency ?? this.uploadConcurrency),
|
|
1373
1620
|
});
|
|
1374
1621
|
}
|
|
1375
1622
|
}
|