whisperai-sdk 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,19 @@
1
+ // src/constant.ts
2
+ var WhisperStatus;
3
+ ((WhisperStatus2) => {
4
+ WhisperStatus2["PENDING"] = "pending";
5
+ WhisperStatus2["VALIDATING"] = "validating";
6
+ WhisperStatus2["UPLOADING"] = "uploading";
7
+ WhisperStatus2["PROCESSING"] = "processing";
8
+ WhisperStatus2["COMPLETED"] = "completed";
9
+ WhisperStatus2["FAILED"] = "failed";
10
+ WhisperStatus2["CANCELLED"] = "cancelled";
11
+ })(WhisperStatus ||= {});
12
+ var DEFAULT_UPLOAD_CHUNK_SIZE = 16 * 1024 * 1024;
13
+ var DEFAULT_MAX_UPLOAD_ATTEMPTS = 5;
14
+ var DEFAULT_POLL_INTERVAL_MS = 2000;
15
+ var DEFAULT_TRANSCRIPTION_TIMEOUT_MS = 30 * 60 * 1000;
16
+
1
17
  // src/errors.ts
2
18
  class WhisperError extends Error {
3
19
  constructor(message) {
@@ -32,6 +48,38 @@ class WhisperNetworkError extends WhisperError {
32
48
  }
33
49
  }
34
50
 
51
+ class WhisperUploadError extends WhisperError {
52
+ diagnosticId;
53
+ code = "UPLOAD_ERROR";
54
+ constructor(message, diagnosticId, cause) {
55
+ super(message);
56
+ this.diagnosticId = diagnosticId;
57
+ this.cause = cause;
58
+ }
59
+ }
60
+
61
+ class WhisperTranscriptionError extends WhisperError {
62
+ recordingId;
63
+ status;
64
+ code = "TRANSCRIPTION_ERROR";
65
+ constructor(recordingId, status) {
66
+ super(`Transcription ${recordingId} finished with status ${status}`);
67
+ this.recordingId = recordingId;
68
+ this.status = status;
69
+ }
70
+ }
71
+
72
+ class WhisperTimeoutError extends WhisperError {
73
+ recordingId;
74
+ timeoutMs;
75
+ code = "TIMEOUT_ERROR";
76
+ constructor(recordingId, timeoutMs) {
77
+ super(`Transcription ${recordingId} did not complete within ${timeoutMs}ms`);
78
+ this.recordingId = recordingId;
79
+ this.timeoutMs = timeoutMs;
80
+ }
81
+ }
82
+
35
83
  // src/client.ts
36
84
  class WhisperClient {
37
85
  cookies;
@@ -39,7 +87,13 @@ class WhisperClient {
39
87
  constructor(clientOptions) {
40
88
  this.clientOptions = {
41
89
  whisperUrl: "https://whisperai.com",
42
- chunkSize: 8 * 1024 * 1024,
90
+ chunkSize: DEFAULT_UPLOAD_CHUNK_SIZE,
91
+ maxUploadAttempts: DEFAULT_MAX_UPLOAD_ATTEMPTS,
92
+ initialRetryDelayMs: 1000,
93
+ maxRetryDelayMs: 30000,
94
+ diagnostics: true,
95
+ pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
96
+ transcriptionTimeoutMs: DEFAULT_TRANSCRIPTION_TIMEOUT_MS,
43
97
  ...clientOptions
44
98
  };
45
99
  }
@@ -55,105 +109,319 @@ class WhisperClient {
55
109
  subscriptionDetails() {
56
110
  return this.recall(() => this.get(this.subscriptionDetailsLink));
57
111
  }
58
- async upload(file, meta) {
59
- if (file instanceof Uint8Array) {
60
- const totalChunks2 = this.getCountChunks(file.length);
61
- const { recordingId: recordingId2 } = await this.recall(() => this.initChunkRequest(file.length, totalChunks2, meta));
62
- await Promise.all(Array.from({ length: totalChunks2 }, (_, i) => this.uploadChunkSlice(file, recordingId2, i)));
63
- return this.finalizeChunk(recordingId2);
64
- }
65
- if (meta.totalSize === undefined) {
112
+ async startTranscription(file, meta, options = {}) {
113
+ if (!(file instanceof Uint8Array) && meta.totalSize === undefined) {
66
114
  const buffer = new Uint8Array(await new Response(file).arrayBuffer());
67
- return this.upload(buffer, { ...meta, totalSize: buffer.length });
115
+ return this.startTranscription(buffer, { ...meta, totalSize: buffer.length }, options);
116
+ }
117
+ const totalSize = file instanceof Uint8Array ? file.byteLength : meta.totalSize;
118
+ if (totalSize <= 0)
119
+ throw new WhisperUploadError("Cannot upload an empty audio file");
120
+ const context = {
121
+ diagnosticId: options.diagnosticId ?? this.createDiagnosticId(),
122
+ diagnostics: options.diagnostics ?? this.clientOptions.diagnostics,
123
+ signal: options.signal,
124
+ onProgress: options.onProgress
125
+ };
126
+ const mimeType = meta.mimeType ?? "application/octet-stream";
127
+ this.throwIfAborted(context.signal);
128
+ this.sendDiagnostic(context, {
129
+ phase: "file-selected",
130
+ fileName: meta.filename,
131
+ fileSize: totalSize,
132
+ fileType: mimeType
133
+ });
134
+ this.sendDiagnostic(context, { phase: "init-requested" });
135
+ let signed;
136
+ try {
137
+ signed = await this.recall(() => this.post(this.signUploadLink, this.buildUploadMetadata(meta, totalSize, mimeType), { "X-Diagnostic-Id": context.diagnosticId }, context.signal));
138
+ } catch (error) {
139
+ this.sendDiagnostic(context, { phase: "init-failed", errorMessage: this.errorMessage(error) });
140
+ throw this.withDiagnosticId(error, context.diagnosticId);
141
+ }
142
+ let sessionUrl;
143
+ try {
144
+ sessionUrl = await this.startResumableSession(signed, context.signal);
145
+ this.sendDiagnostic(context, { phase: "init-succeeded", recordingId: signed.recordingId });
146
+ } catch (error) {
147
+ this.sendDiagnostic(context, {
148
+ phase: "init-failed",
149
+ recordingId: signed.recordingId,
150
+ errorMessage: this.errorMessage(error)
151
+ });
152
+ throw new WhisperUploadError("Failed to start resumable upload session", context.diagnosticId, error);
153
+ }
154
+ await this.uploadToGcs(file, totalSize, sessionUrl, signed.recordingId, context);
155
+ this.sendDiagnostic(context, { phase: "chunk-finalize-attempted", recordingId: signed.recordingId });
156
+ try {
157
+ const result = await this.recall(() => this.post(this.completeUploadLink(signed.recordingId), {}, { "X-Diagnostic-Id": context.diagnosticId }, context.signal));
158
+ this.sendDiagnostic(context, { phase: "chunk-finalize-succeeded", recordingId: signed.recordingId });
159
+ this.sendDiagnostic(context, { phase: "upload-complete", recordingId: signed.recordingId });
160
+ return result;
161
+ } catch (error) {
162
+ this.sendDiagnostic(context, {
163
+ phase: "chunk-finalize-failed",
164
+ recordingId: signed.recordingId,
165
+ errorMessage: this.errorMessage(error)
166
+ });
167
+ throw this.withDiagnosticId(error, context.diagnosticId);
68
168
  }
69
- const totalChunks = this.getCountChunks(meta.totalSize);
70
- const { recordingId } = await this.recall(() => this.initChunkRequest(meta.totalSize, totalChunks, meta));
71
- await this.uploadFromStream(file, recordingId);
72
- return this.finalizeChunk(recordingId);
73
- }
74
- initChunk(fileBuffer, meta) {
75
- const totalChunks = this.getCountChunks(fileBuffer.length);
76
- return this.recall(() => this.initChunkRequest(fileBuffer.length, totalChunks, meta));
77
- }
78
- uploadChunk(chunk, recordingId, chunkIndex) {
79
- return this.recall(() => this.uploadChunkRequest(chunk, recordingId, chunkIndex));
80
- }
81
- finalizeChunk(recordingId) {
82
- return this.recall(() => this.post(this.finalizeChunkLink, { recordingId }));
83
169
  }
84
- transcription(recordingId) {
85
- return this.recall(() => this.post(this.transcriptionLink, { recordingId }));
170
+ async transcribe(file, meta, options = {}) {
171
+ const started = await this.startTranscription(file, meta, options);
172
+ return this.waitForTranscription(started.id, options);
173
+ }
174
+ requestTranscription(recordingId, signal) {
175
+ return this.recall(() => this.post(this.transcriptionLink, { recordingId }, {}, signal));
176
+ }
177
+ recordingStatus(recordingIds, signal) {
178
+ if (recordingIds.length === 0)
179
+ return Promise.resolve([]);
180
+ const params = new URLSearchParams({ ids: recordingIds.join(",") });
181
+ return this.recall(() => this.get(`${this.recordingStatusLink}?${params}`, signal));
182
+ }
183
+ async waitForTranscription(recordingId, options = {}) {
184
+ const pollIntervalMs = options.pollIntervalMs ?? this.clientOptions.pollIntervalMs;
185
+ const timeoutMs = options.timeoutMs ?? this.clientOptions.transcriptionTimeoutMs;
186
+ const startedAt = Date.now();
187
+ while (true) {
188
+ this.throwIfAborted(options.signal);
189
+ const statuses = await this.recordingStatus([recordingId], options.signal);
190
+ const status = statuses.find((item) => item.recordingId === recordingId)?.recordingStatus;
191
+ if (status === "completed" /* COMPLETED */) {
192
+ const recording = await this.recording(recordingId, options.signal);
193
+ if (!recording.transcription)
194
+ throw new WhisperTranscriptionError(recordingId, "completed_without_transcription");
195
+ return recording;
196
+ }
197
+ if (status === "failed" /* FAILED */ || status === "cancelled" /* CANCELLED */ || status === "canceled") {
198
+ throw new WhisperTranscriptionError(recordingId, status);
199
+ }
200
+ if (Date.now() - startedAt >= timeoutMs)
201
+ throw new WhisperTimeoutError(recordingId, timeoutMs);
202
+ await this.sleep(Math.min(pollIntervalMs, Math.max(0, timeoutMs - (Date.now() - startedAt))), options.signal);
203
+ }
86
204
  }
87
205
  translate(recordingId, language) {
88
206
  return this.recall(() => this.post(this.translateLink(recordingId), { targetLanguage: language }));
89
207
  }
90
- recording(recordingId) {
91
- return this.recall(() => this.get(this.recordingLink(recordingId)));
92
- }
93
- recordings(query) {
94
- const params = new URLSearchParams({
95
- page: String(query?.page ?? 1),
96
- limit: String(query?.limit ?? 5)
97
- });
208
+ recording(recordingId, signal) {
209
+ return this.recall(() => this.get(this.recordingLink(recordingId), signal));
210
+ }
211
+ recordings(query = {}) {
212
+ const params = new URLSearchParams;
213
+ params.set("limit", String(query.limit ?? 20));
214
+ if (query.page !== undefined)
215
+ params.set("page", String(query.page));
216
+ if (query.cursor)
217
+ params.set("cursor", query.cursor);
218
+ if (query.direction)
219
+ params.set("direction", query.direction);
220
+ if (query.search)
221
+ params.set("q", query.search);
222
+ if (query.status)
223
+ params.set("status", query.status);
224
+ if (query.sort)
225
+ params.set("sort", query.sort);
98
226
  return this.recall(() => this.get(`${this.recordingsLink}?${params}`));
99
227
  }
100
228
  summary() {
101
229
  return this.recall(() => this.get(this.summaryLink));
102
230
  }
103
- async uploadFromStream(stream, recordingId) {
104
- const reader = stream.getReader();
105
- let buffer = new Uint8Array(0);
231
+ buildUploadMetadata(meta, totalSize, mimeType) {
232
+ const title = meta.title ?? meta.filename.replace(/\.[^.]+$/, "");
233
+ return {
234
+ filename: meta.filename,
235
+ mimeType,
236
+ totalSize,
237
+ title,
238
+ language: meta.language ?? "multi-auto",
239
+ enableSpeakerDetection: meta.enableSpeakerDetection ?? false,
240
+ speakerCount: meta.speakerCount ?? "auto",
241
+ durationSeconds: meta.durationSeconds,
242
+ transcriptionStyle: meta.transcriptionStyle ?? "clean_readable",
243
+ importantTerms: meta.importantTerms ?? "",
244
+ customPrompt: meta.customPrompt ?? "",
245
+ speakerIdentificationEnabled: meta.speakerIdentificationEnabled ?? false,
246
+ speakerIdentificationMode: meta.speakerIdentificationMode ?? "role",
247
+ speakerIdentificationValues: meta.speakerIdentificationValues ?? [],
248
+ ...meta.folderId === undefined ? {} : { folderId: meta.folderId }
249
+ };
250
+ }
251
+ async startResumableSession(signed, signal) {
252
+ let response;
253
+ try {
254
+ response = await fetch(signed.signedResumableInitUrl, {
255
+ method: "POST",
256
+ headers: signed.requiredHeaders,
257
+ body: "",
258
+ signal,
259
+ redirect: "manual"
260
+ });
261
+ } catch (cause) {
262
+ throw new WhisperNetworkError(cause);
263
+ }
264
+ if (response.status !== 201)
265
+ throw new WhisperUploadError(`GCS session initialization failed (${response.status})`);
266
+ const location = response.headers.get("location");
267
+ if (!location)
268
+ throw new WhisperUploadError("GCS session response did not include a Location header");
269
+ return location;
270
+ }
271
+ async uploadToGcs(input, totalSize, sessionUrl, recordingId, context) {
272
+ let offset = 0;
106
273
  let chunkIndex = 0;
274
+ context.onProgress?.(0);
275
+ for await (const chunk of this.readChunks(input, this.clientOptions.chunkSize)) {
276
+ const chunkStart = offset;
277
+ const expectedEnd = chunkStart + chunk.byteLength;
278
+ if (expectedEnd > totalSize)
279
+ throw new WhisperUploadError("Audio stream exceeded declared totalSize", context.diagnosticId);
280
+ while (offset < expectedEnd) {
281
+ if (offset < chunkStart) {
282
+ throw new WhisperUploadError("GCS requested data that is no longer available in the stream", context.diagnosticId);
283
+ }
284
+ const slice = chunk.subarray(offset - chunkStart);
285
+ const result = await this.uploadRangeWithRetry(sessionUrl, slice, offset, totalSize, recordingId, chunkIndex, context);
286
+ offset = result.nextOffset;
287
+ context.onProgress?.(Math.min(100, Math.round(offset / totalSize * 100)));
288
+ if (result.complete)
289
+ break;
290
+ }
291
+ chunkIndex++;
292
+ }
293
+ if (offset !== totalSize) {
294
+ throw new WhisperUploadError(`Audio stream size mismatch: expected ${totalSize}, uploaded ${offset}`, context.diagnosticId);
295
+ }
296
+ }
297
+ async uploadRangeWithRetry(sessionUrl, chunk, offset, totalSize, recordingId, chunkIndex, context) {
298
+ let currentOffset = offset;
299
+ let currentChunk = chunk;
300
+ let lastError;
301
+ for (let attempt = 1;attempt <= this.clientOptions.maxUploadAttempts; attempt++) {
302
+ this.throwIfAborted(context.signal);
303
+ this.sendDiagnostic(context, { phase: "chunk-fetch-sent", recordingId, chunkIndex, attempt });
304
+ try {
305
+ const result = await this.putRange(sessionUrl, currentChunk, currentOffset, totalSize, context.signal);
306
+ this.sendDiagnostic(context, {
307
+ phase: "chunk-fetch-status",
308
+ recordingId,
309
+ chunkIndex,
310
+ attempt,
311
+ httpStatus: result.status
312
+ });
313
+ return result;
314
+ } catch (error) {
315
+ lastError = error;
316
+ this.sendDiagnostic(context, {
317
+ phase: "chunk-fetch-error",
318
+ recordingId,
319
+ chunkIndex,
320
+ attempt,
321
+ errorMessage: this.errorMessage(error)
322
+ });
323
+ if (attempt >= this.clientOptions.maxUploadAttempts || !this.isRetryable(error))
324
+ break;
325
+ await this.sleep(this.retryDelay(attempt), context.signal);
326
+ try {
327
+ const probe = await this.probeUpload(sessionUrl, totalSize, context.signal);
328
+ if (probe.complete)
329
+ return probe;
330
+ if (probe.nextOffset < offset || probe.nextOffset > offset + chunk.byteLength) {
331
+ throw new WhisperUploadError(`Unexpected GCS resume offset ${probe.nextOffset}`);
332
+ }
333
+ if (probe.nextOffset === offset + chunk.byteLength)
334
+ return probe;
335
+ currentOffset = probe.nextOffset;
336
+ currentChunk = chunk.subarray(currentOffset - offset);
337
+ } catch (probeError) {
338
+ if (!this.isRetryable(probeError))
339
+ throw probeError;
340
+ }
341
+ }
342
+ }
343
+ this.sendDiagnostic(context, {
344
+ phase: "upload-given-up",
345
+ recordingId,
346
+ chunkIndex,
347
+ errorMessage: this.errorMessage(lastError)
348
+ });
349
+ throw new WhisperUploadError("GCS resumable upload failed after all retry attempts", context.diagnosticId, lastError);
350
+ }
351
+ async putRange(sessionUrl, chunk, offset, totalSize, signal) {
352
+ const end = offset + chunk.byteLength;
353
+ let response;
354
+ try {
355
+ response = await fetch(sessionUrl, {
356
+ method: "PUT",
357
+ headers: { "Content-Range": `bytes ${offset}-${end - 1}/${totalSize}` },
358
+ body: this.toArrayBuffer(chunk),
359
+ signal,
360
+ redirect: "manual"
361
+ });
362
+ } catch (cause) {
363
+ throw new WhisperNetworkError(cause);
364
+ }
365
+ if (response.status === 200 || response.status === 201) {
366
+ return { complete: true, nextOffset: totalSize, status: response.status };
367
+ }
368
+ if (response.status === 308) {
369
+ return { complete: false, nextOffset: this.nextOffset(response.headers.get("range"), end), status: 308 };
370
+ }
371
+ throw new WhisperApiError(response.status, await this.responseData(response));
372
+ }
373
+ async probeUpload(sessionUrl, totalSize, signal) {
374
+ let response;
375
+ try {
376
+ response = await fetch(sessionUrl, {
377
+ method: "PUT",
378
+ headers: { "Content-Range": `bytes */${totalSize}` },
379
+ body: "",
380
+ signal,
381
+ redirect: "manual"
382
+ });
383
+ } catch (cause) {
384
+ throw new WhisperNetworkError(cause);
385
+ }
386
+ if (response.status === 200 || response.status === 201) {
387
+ return { complete: true, nextOffset: totalSize, status: response.status };
388
+ }
389
+ if (response.status === 308) {
390
+ return { complete: false, nextOffset: this.nextOffset(response.headers.get("range"), 0), status: 308 };
391
+ }
392
+ throw new WhisperApiError(response.status, await this.responseData(response));
393
+ }
394
+ async* readChunks(input, chunkSize) {
395
+ if (input instanceof Uint8Array) {
396
+ for (let offset = 0;offset < input.byteLength; offset += chunkSize) {
397
+ yield input.subarray(offset, Math.min(offset + chunkSize, input.byteLength));
398
+ }
399
+ return;
400
+ }
401
+ const reader = input.getReader();
402
+ let buffer = new Uint8Array(0);
107
403
  try {
108
404
  while (true) {
109
405
  const { done, value } = await reader.read();
110
- if (done) {
111
- if (buffer.length > 0) {
112
- await this.uploadChunk(buffer, recordingId, chunkIndex);
113
- }
406
+ if (done)
114
407
  break;
115
- }
116
- const merged = new Uint8Array(buffer.length + value.length);
408
+ if (!(value instanceof Uint8Array))
409
+ throw new TypeError("Audio stream chunks must be Uint8Array values");
410
+ const merged = new Uint8Array(buffer.byteLength + value.byteLength);
117
411
  merged.set(buffer);
118
- merged.set(value, buffer.length);
412
+ merged.set(value, buffer.byteLength);
119
413
  buffer = merged;
120
- while (buffer.length >= this.clientOptions.chunkSize) {
121
- await this.uploadChunk(buffer.subarray(0, this.clientOptions.chunkSize), recordingId, chunkIndex);
122
- buffer = buffer.slice(this.clientOptions.chunkSize);
123
- chunkIndex++;
414
+ while (buffer.byteLength >= chunkSize) {
415
+ yield buffer.slice(0, chunkSize);
416
+ buffer = buffer.slice(chunkSize);
124
417
  }
125
418
  }
419
+ if (buffer.byteLength > 0)
420
+ yield buffer;
126
421
  } finally {
127
422
  reader.releaseLock();
128
423
  }
129
424
  }
130
- uploadChunkSlice(fileBuffer, recordingId, chunkIndex) {
131
- const start = chunkIndex * this.clientOptions.chunkSize;
132
- const end = Math.min(start + this.clientOptions.chunkSize, fileBuffer.length);
133
- return this.uploadChunk(fileBuffer.subarray(start, end), recordingId, chunkIndex);
134
- }
135
- initChunkRequest(totalSize, totalChunks, meta) {
136
- return this.post(this.initChunkLink, {
137
- filename: meta.filename,
138
- durationSeconds: meta.durationSeconds,
139
- totalSize,
140
- mimeType: meta.mimeType ?? "",
141
- title: meta.title ?? meta.filename,
142
- enableSpeakerDetection: meta.enableSpeakerDetection ?? false,
143
- speakerCount: meta.speakerCount ?? "auto",
144
- language: meta.language ?? "",
145
- totalChunks
146
- });
147
- }
148
- uploadChunkRequest(chunk, recordingId, chunkIndex) {
149
- const buffer = new ArrayBuffer(chunk.byteLength);
150
- new Uint8Array(buffer).set(chunk);
151
- const formData = new FormData;
152
- formData.append("chunk", new Blob([buffer], { type: "application/octet-stream" }), `chunk_${chunkIndex}`);
153
- formData.append("recordingId", recordingId.toString());
154
- formData.append("chunkIndex", chunkIndex.toString());
155
- return this.postForm(this.uploadChunkLink, formData);
156
- }
157
425
  async recall(call) {
158
426
  try {
159
427
  if (!this.cookies)
@@ -166,19 +434,17 @@ class WhisperClient {
166
434
  return call();
167
435
  }
168
436
  }
169
- get(url) {
170
- return this.request(url, { method: "GET" });
437
+ get(url, signal) {
438
+ return this.request(url, { method: "GET", signal });
171
439
  }
172
- post(url, body) {
440
+ post(url, body, headers = {}, signal) {
173
441
  return this.request(url, {
174
442
  method: "POST",
175
443
  body: JSON.stringify(body),
176
- extraHeaders: { "Content-Type": "application/json" }
444
+ signal,
445
+ extraHeaders: { "Content-Type": "application/json", ...headers }
177
446
  });
178
447
  }
179
- postForm(url, body) {
180
- return this.request(url, { method: "POST", body });
181
- }
182
448
  async request(url, init) {
183
449
  const { extraHeaders, ...fetchInit } = init;
184
450
  const cookieHeader = this.cookies?.join("; ");
@@ -201,46 +467,131 @@ class WhisperClient {
201
467
  if (!response.ok) {
202
468
  if (response.status === 401 || response.status === 403)
203
469
  throw new WhisperAuthError;
204
- const data = await response.json().catch(() => null);
205
- throw new WhisperApiError(response.status, data);
470
+ throw new WhisperApiError(response.status, await this.responseData(response));
471
+ }
472
+ if (response.status === 204)
473
+ return;
474
+ return await this.responseData(response);
475
+ }
476
+ async responseData(response) {
477
+ const text = await response.text();
478
+ if (!text)
479
+ return;
480
+ try {
481
+ return JSON.parse(text);
482
+ } catch {
483
+ return text;
484
+ }
485
+ }
486
+ sendDiagnostic(context, event) {
487
+ if (!context.diagnostics)
488
+ return;
489
+ const payload = {
490
+ diagId: context.diagnosticId,
491
+ ...event,
492
+ timestamp: new Date().toISOString()
493
+ };
494
+ this.request(this.diagnosticsLink, {
495
+ method: "POST",
496
+ body: JSON.stringify({ events: [payload] }),
497
+ extraHeaders: { "Content-Type": "application/json" },
498
+ signal: context.signal
499
+ }).catch(() => {
500
+ return;
501
+ });
502
+ }
503
+ isRetryable(error) {
504
+ if (error instanceof WhisperNetworkError)
505
+ return true;
506
+ return error instanceof WhisperApiError && (error.status === 408 || error.status === 429 || error.status >= 500);
507
+ }
508
+ retryDelay(attempt) {
509
+ const base = Math.min(this.clientOptions.maxRetryDelayMs, this.clientOptions.initialRetryDelayMs * 2 ** (attempt - 1));
510
+ return base + Math.random() * base * 0.25;
511
+ }
512
+ sleep(ms, signal) {
513
+ return new Promise((resolve, reject) => {
514
+ if (signal?.aborted)
515
+ return reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
516
+ const onAbort = () => {
517
+ clearTimeout(timeout);
518
+ reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
519
+ };
520
+ const timeout = setTimeout(() => {
521
+ signal?.removeEventListener("abort", onAbort);
522
+ resolve();
523
+ }, ms);
524
+ signal?.addEventListener("abort", onAbort, { once: true });
525
+ });
526
+ }
527
+ throwIfAborted(signal) {
528
+ if (signal?.aborted)
529
+ throw signal.reason ?? new DOMException("Aborted", "AbortError");
530
+ }
531
+ nextOffset(range, fallback) {
532
+ const match = range?.match(/bytes=0-(\d+)/i);
533
+ return match ? Number(match[1]) + 1 : fallback;
534
+ }
535
+ toArrayBuffer(bytes) {
536
+ const buffer = new ArrayBuffer(bytes.byteLength);
537
+ new Uint8Array(buffer).set(bytes);
538
+ return buffer;
539
+ }
540
+ errorMessage(error) {
541
+ return error instanceof Error ? error.message : String(error ?? "");
542
+ }
543
+ withDiagnosticId(error, diagnosticId) {
544
+ if (error && typeof error === "object") {
545
+ try {
546
+ Object.assign(error, { diagnosticId });
547
+ } catch {
548
+ return new WhisperUploadError(this.errorMessage(error), diagnosticId, error);
549
+ }
550
+ return error;
206
551
  }
207
- return response.json();
552
+ return new WhisperUploadError(this.errorMessage(error), diagnosticId, error);
553
+ }
554
+ createDiagnosticId() {
555
+ return (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`).replace(/-/g, "").slice(0, 8);
208
556
  }
209
557
  get loginLink() {
210
- return `${this.clientOptions.whisperUrl}/api/login`;
558
+ return `${this.clientOptions.whisperUrl}/api/v2/auth/login`;
211
559
  }
212
560
  get userLink() {
213
- return `${this.clientOptions.whisperUrl}/api/user`;
561
+ return `${this.clientOptions.whisperUrl}/api/v2/auth/user`;
214
562
  }
215
563
  get usageLink() {
216
- return `${this.clientOptions.whisperUrl}/api/usage`;
564
+ return `${this.clientOptions.whisperUrl}/api/v2/account/usage`;
217
565
  }
218
566
  get subscriptionDetailsLink() {
219
- return `${this.clientOptions.whisperUrl}/api/subscription-details`;
567
+ return `${this.clientOptions.whisperUrl}/api/v2/payment/subscription-details`;
220
568
  }
221
- get initChunkLink() {
222
- return `${this.clientOptions.whisperUrl}/api/v2/recordings/chunked/init`;
569
+ get signUploadLink() {
570
+ return `${this.clientOptions.whisperUrl}/api/v2/recordings/uploads/sign`;
223
571
  }
224
- get uploadChunkLink() {
225
- return `${this.clientOptions.whisperUrl}/api/v2/recordings/chunked/upload`;
572
+ completeUploadLink(recordingId) {
573
+ return `${this.clientOptions.whisperUrl}/api/v2/recordings/uploads/${recordingId}/complete`;
226
574
  }
227
- get finalizeChunkLink() {
228
- return `${this.clientOptions.whisperUrl}/api/v2/recordings/chunked/finalize`;
575
+ get diagnosticsLink() {
576
+ return `${this.clientOptions.whisperUrl}/api/v2/diagnostics/upload-breadcrumb`;
229
577
  }
230
578
  get transcriptionLink() {
231
579
  return `${this.clientOptions.whisperUrl}/api/v2/transcription`;
232
580
  }
581
+ get recordingStatusLink() {
582
+ return `${this.clientOptions.whisperUrl}/api/v2/recordings/status`;
583
+ }
233
584
  get summaryLink() {
234
- return `${this.clientOptions.whisperUrl}/api/analytics/summary`;
585
+ return `${this.clientOptions.whisperUrl}/api/v2/analytics/summary`;
235
586
  }
236
587
  get recordingsLink() {
237
- return `${this.clientOptions.whisperUrl}/api/recordings-paginated`;
588
+ return `${this.clientOptions.whisperUrl}/api/v2/recordings/paginated`;
238
589
  }
239
590
  translateLink(recordingId) {
240
591
  return `${this.clientOptions.whisperUrl}/api/v2/translation/${recordingId}/translate`;
241
592
  }
242
593
  recordingLink(recordingId) {
243
- return `${this.clientOptions.whisperUrl}/api/recordings/${recordingId}`;
594
+ return `${this.clientOptions.whisperUrl}/api/v2/recordings/${recordingId}`;
244
595
  }
245
596
  mergeCookies(incoming) {
246
597
  const map = new Map;
@@ -253,22 +604,19 @@ class WhisperClient {
253
604
  }
254
605
  this.cookies = [...map.values()];
255
606
  }
256
- getCountChunks(size) {
257
- return Math.ceil(size / this.clientOptions.chunkSize);
258
- }
259
607
  }
260
- // src/constant.ts
261
- var WhisperStatus;
262
- ((WhisperStatus2) => {
263
- WhisperStatus2["PENDING"] = "pending";
264
- WhisperStatus2["VALIDATING"] = "validating";
265
- WhisperStatus2["UPLOADING"] = "uploading";
266
- WhisperStatus2["PROCESSING"] = "processing";
267
- WhisperStatus2["COMPLETED"] = "completed";
268
- WhisperStatus2["FAILED"] = "failed";
269
- WhisperStatus2["CANCELLED"] = "cancelled";
270
- })(WhisperStatus ||= {});
271
608
  export {
609
+ WhisperUploadError,
610
+ WhisperTranscriptionError,
611
+ WhisperTimeoutError,
272
612
  WhisperStatus,
273
- WhisperClient
613
+ WhisperNetworkError,
614
+ WhisperError,
615
+ WhisperClient,
616
+ WhisperAuthError,
617
+ WhisperApiError,
618
+ DEFAULT_UPLOAD_CHUNK_SIZE,
619
+ DEFAULT_TRANSCRIPTION_TIMEOUT_MS,
620
+ DEFAULT_POLL_INTERVAL_MS,
621
+ DEFAULT_MAX_UPLOAD_ATTEMPTS
274
622
  };