@transcribe-api/sdk 0.1.3 → 0.1.5

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.
Files changed (4) hide show
  1. package/README.md +389 -54
  2. package/index.js +1143 -1354
  3. package/package.json +1 -1
  4. 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 = 100 * 1024 * 1024;
7
- const DEFAULT_MULTIPART_CONCURRENCY = 8;
8
- const MAX_MULTIPART_CONCURRENCY = 32;
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
- if (!isRetryableError(error) || index === attempts - 1) {
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 normalizeMultipartConcurrency(value) {
96
+ function normalizeUploadConcurrency(value) {
95
97
  if (value === undefined || value === null || value === "") {
96
- return DEFAULT_MULTIPART_CONCURRENCY;
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("`multipartConcurrency` must be an integer >= 1.", {
101
- code: "invalid_multipart_concurrency",
102
+ throw new TranscribeAPIError("`uploadConcurrency` must be an integer >= 1.", {
103
+ code: "invalid_upload_concurrency",
102
104
  });
103
105
  }
104
- return Math.min(parsed, MAX_MULTIPART_CONCURRENCY);
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 isRemoteFileInput(input) {
157
+ function isRemoteBatchItem(input) {
156
158
  return Boolean(input && typeof input === "object" && typeof input.url === "string");
157
159
  }
158
160
 
159
- function isRemoteBatchItem(input) {
160
- return Boolean(input && typeof input === "object" && typeof input.url === "string");
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 `file_${String(index + 1).padStart(6, "0")}`;
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 `reference_id` and either `file` or `url`.", {
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 = { reference_id: referenceId };
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 === "function") {
234
- handler(event);
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" || typeof logger?.log !== "function") {
299
+ if (!event || typeof event !== "object") {
258
300
  return;
259
301
  }
260
302
  if (event.event === "upload_started") {
261
- logger.log(`Uploading ${event.uploadFiles}/${event.totalFiles} local file(s) for ${event.jobId}, ${formatBytes(event.totalBytes)} total`);
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
- logger.log(`Uploading ${renderProgressBar(loaded, total)} ${percent}% (${formatBytes(loaded)} / ${formatBytes(total)})`);
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" && !event.suppressLog) {
272
- logger.log(`Uploaded completed: ${JSON.stringify(event.response, null, 2)}`);
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 lastStatus = null;
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
- lastStatus = jobStatus || null;
282
- if (typeof logger?.log === "function") {
283
- logger.log(`Polling ${jobId} (${jobStatus || "waiting"})`);
284
- }
285
- },
286
- update({ jobId, jobStatus }) {
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
- lastStatus = jobStatus || null;
291
- if (typeof logger?.log === "function") {
292
- logger.log(`Polling ${jobId} (${jobStatus || "unknown"})`);
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
- if (typeof logger?.log === "function") {
297
- logger.log(resultUrl
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
- if (typeof logger?.log === "function") {
304
- logger.log(`Polling stopped: timeout after ${timeoutSeconds}s (${jobStatus})`);
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
- const bytes = new Uint8Array(await file.slice(0, headerLength).arrayBuffer());
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
- throw new TranscribeAPIError("Worker SDK does not support local file paths. Use a Blob, File, bytes, or remote URL.", {
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 = { raw: text };
718
+ data = text;
585
719
  }
586
720
  if (!response.ok) {
587
- const message = data.message || data.error || text || `HTTP ${response.status}`;
588
- const extra = { ...data };
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: data.code || null,
729
+ code: body.code || null,
595
730
  extra,
596
731
  response: {
597
732
  message,
598
- ...(data.code ? { code: data.code } : {}),
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 resolvedBody = typeof body === "function" ? await body() : body;
624
- const putResponse = await fetch(upload.url || upload, {
625
- method: "PUT",
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
- body: resolvedBody,
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
- multipartConcurrency = DEFAULT_MULTIPART_CONCURRENCY,
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 concurrency = normalizeMultipartConcurrency(multipartConcurrency);
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 = [...upload.parts];
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
- if (isRetryableError(error) && attempts < MAX_MULTIPART_ADAPTIVE_ATTEMPTS) {
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(`Multipart upload failed for part ${part.part_number}/${totalParts}.`, {
798
- status: error?.status || null,
799
- code: error?.code || "multipart_part_upload_failed",
800
- extra: {
801
- cause_error: error?.error || error?.message || String(error),
802
- cause_code: error?.code || error?.cause?.code || null,
803
- cause_status: error?.status || null,
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
- return putObjectWithRetry(upload, file, {
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
- for (const item of this.files) {
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
- multipartConcurrency = DEFAULT_MULTIPART_CONCURRENCY,
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.multipartConcurrency = normalizeMultipartConcurrency(multipartConcurrency);
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 pollingLogger = showLogs ? createSdkPollingLogger(logger) : null;
1280
+ const spinner = showLogs ? createSdkPollingLogger(logger) : null;
1067
1281
  let lastJob = initialJob || null;
1068
- pollingLogger?.start({
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
- pollingLogger?.timeout({
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
- pollingLogger?.update({
1093
- jobId,
1306
+ spinner?.update({
1094
1307
  jobStatus: lastJob?.job_status || "unknown",
1095
1308
  });
1096
1309
  }
1097
1310
 
1098
- pollingLogger?.finish({
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
- file,
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
- multipartConcurrency,
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 (isRemoteFileInput(file)) {
1125
- const job = await this.createBigFileJob({
1126
- file,
1127
- webhookUrl,
1128
- onProgress: progress,
1129
- showLogs: false,
1130
- multipartConcurrency,
1131
- });
1132
- const result = await job.upload({ onProgress: progress });
1133
- if (this.polling && !TERMINAL_JOB_STATUSES.has(String(result?.job_status || ""))) {
1134
- return this.waitForJobCompletion(result.job_id, {
1135
- polling: this.polling,
1136
- showLogs: showLogs ?? this.showLogs,
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
- initialJob: result,
1354
+ uploadConcurrency,
1355
+ language,
1356
+ exclude,
1139
1357
  });
1140
1358
  }
1141
- return result;
1142
- }
1143
1359
 
1144
- const normalizedFile = await normalizeFile(file);
1145
- const estimatedDurationSec = await estimateDurationFromFile(normalizedFile);
1146
- if (normalizedFile.size > MAX_SYNC_AUDIO_BYTES || estimatedDurationSec > MAX_SYNC_AUDIO_SECONDS) {
1147
- const job = await this.createBigFileJob({
1148
- file: normalizedFile,
1149
- webhookUrl,
1150
- durationEstimateSec: estimatedDurationSec,
1151
- onProgress: progress,
1152
- showLogs: false,
1153
- multipartConcurrency,
1154
- });
1155
- const result = await job.upload({ onProgress: progress });
1156
- if (this.polling && !TERMINAL_JOB_STATUSES.has(String(result?.job_status || ""))) {
1157
- return this.waitForJobCompletion(result.job_id, {
1158
- polling: this.polling,
1159
- showLogs: showLogs ?? this.showLogs,
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
- initialJob: result,
1384
+ uploadConcurrency,
1385
+ language,
1386
+ exclude,
1162
1387
  });
1163
1388
  }
1164
- return result;
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
- return this.transcribeDirect({
1168
- file: normalizedFile,
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("file", normalizedFile, normalizedFile.name);
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
- return parseApiResponse(response);
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
- multipartConcurrency,
1495
+ uploadConcurrency,
1496
+ language,
1497
+ exclude,
1215
1498
  } = {}) {
1216
- const progress = composeProgressHandler({
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: progress,
1226
- showLogs: false,
1227
- multipartConcurrency,
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
- multipartConcurrency,
1519
+ uploadConcurrency,
1520
+ language,
1521
+ exclude,
1249
1522
  } = {}) {
1250
- const progress = composeProgressHandler({
1523
+ return this.createBatchJob({
1524
+ files: [file],
1525
+ webhookUrl,
1526
+ durationEstimateSec,
1251
1527
  onProgress,
1252
- showLogs: showLogs ?? this.showLogs,
1528
+ showLogs,
1253
1529
  logger: logger ?? this.logger,
1254
- });
1255
- const normalizedConcurrency = normalizeMultipartConcurrency(multipartConcurrency ?? this.multipartConcurrency);
1256
- if (isRemoteFileInput(file)) {
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
- multipartConcurrency,
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 fileValue = await normalizeFile(item.file, `file_${String(index + 1).padStart(6, "0")}.mp3`);
1336
- assertSupportedBatchFormat(fileValue);
1574
+ const file = await normalizeFile(item.file, `file_${String(index + 1).padStart(6, "0")}.mp3`);
1575
+ assertSupportedBatchFormat(file);
1337
1576
  normalizedBatchItems.push({
1338
- file: fileValue,
1339
- referenceId: item.referenceId,
1577
+ file,
1578
+ referenceId: item.referenceId || defaultReferenceId(index),
1340
1579
  url: null,
1341
- durationEstimateSec: item.durationEstimateSec || await estimateDurationFromFile(fileValue),
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
- multipartConcurrency: normalizeMultipartConcurrency(multipartConcurrency ?? this.multipartConcurrency),
1619
+ uploadConcurrency: normalizeUploadConcurrency(uploadConcurrency ?? this.uploadConcurrency),
1373
1620
  });
1374
1621
  }
1375
1622
  }