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/README.md +84 -110
- package/dist/client.d.ts +31 -16
- package/dist/constant.d.ts +4 -0
- package/dist/errors.d.ts +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +461 -113
- package/dist/types.d.ts +138 -94
- package/package.json +2 -1
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:
|
|
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
|
|
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.
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
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
|
-
|
|
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.
|
|
412
|
+
merged.set(value, buffer.byteLength);
|
|
119
413
|
buffer = merged;
|
|
120
|
-
while (buffer.
|
|
121
|
-
|
|
122
|
-
buffer = buffer.slice(
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
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
|
|
222
|
-
return `${this.clientOptions.whisperUrl}/api/v2/recordings/
|
|
569
|
+
get signUploadLink() {
|
|
570
|
+
return `${this.clientOptions.whisperUrl}/api/v2/recordings/uploads/sign`;
|
|
223
571
|
}
|
|
224
|
-
|
|
225
|
-
return `${this.clientOptions.whisperUrl}/api/v2/recordings/
|
|
572
|
+
completeUploadLink(recordingId) {
|
|
573
|
+
return `${this.clientOptions.whisperUrl}/api/v2/recordings/uploads/${recordingId}/complete`;
|
|
226
574
|
}
|
|
227
|
-
get
|
|
228
|
-
return `${this.clientOptions.whisperUrl}/api/v2/
|
|
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
|
|
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
|
-
|
|
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
|
};
|