@soniox/node 1.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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/index.cjs +3638 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3043 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +3043 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3597 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3597 @@
|
|
|
1
|
+
//#region src/constants.ts
|
|
2
|
+
const SONIOX_API_BASE_URL = "https://api.soniox.com";
|
|
3
|
+
const SONIOX_API_WS_URL = "wss://stt-rt.soniox.com/transcribe-websocket";
|
|
4
|
+
const SONIOX_TMP_API_KEY_USAGE_TYPE = "transcribe_websocket";
|
|
5
|
+
const SONIOX_TMP_API_KEY_DURATION_MIN = 1;
|
|
6
|
+
const SONIOX_TMP_API_KEY_DURATION_MAX = 3600;
|
|
7
|
+
const SONIOX_API_WEBHOOK_HEADER_ENV = "SONIOX_API_WEBHOOK_HEADER";
|
|
8
|
+
const SONIOX_API_WEBHOOK_SECRET_ENV = "SONIOX_API_WEBHOOK_SECRET";
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/async/auth.ts
|
|
12
|
+
var SonioxAuthAPI = class {
|
|
13
|
+
constructor(http) {
|
|
14
|
+
this.http = http;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a temporary API key for client-side use.
|
|
18
|
+
*
|
|
19
|
+
* @param request - Request parameters for the temporary key
|
|
20
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
21
|
+
* @returns The temporary API key response
|
|
22
|
+
*/
|
|
23
|
+
async createTemporaryKey(request, signal) {
|
|
24
|
+
if (request.expires_in_seconds < 1 || request.expires_in_seconds > 3600) throw new Error("expires_in_seconds must be between 1 and 3600");
|
|
25
|
+
return (await this.http.request({
|
|
26
|
+
method: "POST",
|
|
27
|
+
path: "/v1/auth/temporary-api-key",
|
|
28
|
+
body: request,
|
|
29
|
+
...signal && { signal }
|
|
30
|
+
})).data;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/http/errors.ts
|
|
36
|
+
/** Maximum body text length to include in error details (4KB) */
|
|
37
|
+
const MAX_BODY_TEXT_LENGTH = 4096;
|
|
38
|
+
/**
|
|
39
|
+
* Base error class for all Soniox SDK errors.
|
|
40
|
+
*
|
|
41
|
+
* All SDK errors extend this class for error handling across both REST (HTTP) and WebSocket (Real-time) APIs.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* try {
|
|
46
|
+
* await client.transcribe(file);
|
|
47
|
+
* await session.connect();
|
|
48
|
+
* } catch (error) {
|
|
49
|
+
* if (error instanceof SonioxError) {
|
|
50
|
+
* console.log(error.code); // 'auth_error', 'network_error', etc.
|
|
51
|
+
* console.log(error.statusCode); // 401, 500, etc. (when applicable)
|
|
52
|
+
* console.log(error.toJSON()); // Consistent serialization
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
var SonioxError = class extends Error {
|
|
58
|
+
/**
|
|
59
|
+
* Error code describing the type of error
|
|
60
|
+
*/
|
|
61
|
+
code;
|
|
62
|
+
/**
|
|
63
|
+
* HTTP status code when applicable (e.g., 401 for auth errors, 500 for server errors).
|
|
64
|
+
*/
|
|
65
|
+
statusCode;
|
|
66
|
+
/**
|
|
67
|
+
* The underlying error that caused this error, if any.
|
|
68
|
+
*/
|
|
69
|
+
cause;
|
|
70
|
+
constructor(message, code = "soniox_error", statusCode, cause) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "SonioxError";
|
|
73
|
+
this.code = code;
|
|
74
|
+
this.statusCode = statusCode;
|
|
75
|
+
this.cause = cause;
|
|
76
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
|
|
77
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Creates a human-readable string representation
|
|
81
|
+
*/
|
|
82
|
+
toString() {
|
|
83
|
+
const parts = [`${this.name} [${this.code}]: ${this.message}`];
|
|
84
|
+
if (this.statusCode !== void 0) parts.push(` Status: ${this.statusCode}`);
|
|
85
|
+
return parts.join("\n");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Converts to a plain object for logging/serialization
|
|
89
|
+
*/
|
|
90
|
+
toJSON() {
|
|
91
|
+
return {
|
|
92
|
+
name: this.name,
|
|
93
|
+
code: this.code,
|
|
94
|
+
message: this.message,
|
|
95
|
+
...this.statusCode !== void 0 && { statusCode: this.statusCode }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* HTTP error class for all HTTP-related failures (REST API).
|
|
101
|
+
*
|
|
102
|
+
* Thrown when HTTP requests fail due to network issues, timeouts,
|
|
103
|
+
* server errors, or response parsing failures.
|
|
104
|
+
*/
|
|
105
|
+
var SonioxHttpError = class extends SonioxError {
|
|
106
|
+
/** Request URL */
|
|
107
|
+
url;
|
|
108
|
+
/** HTTP method */
|
|
109
|
+
method;
|
|
110
|
+
/** Response headers (only for http_error) */
|
|
111
|
+
headers;
|
|
112
|
+
/** Response body text, capped at 4KB (only for http_error/parse_error) */
|
|
113
|
+
bodyText;
|
|
114
|
+
constructor(details) {
|
|
115
|
+
super(details.message, details.code, details.statusCode, details.cause);
|
|
116
|
+
this.name = "SonioxHttpError";
|
|
117
|
+
this.url = details.url;
|
|
118
|
+
this.method = details.method;
|
|
119
|
+
this.headers = details.headers;
|
|
120
|
+
this.bodyText = details.bodyText;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Creates a human-readable string representation
|
|
124
|
+
*/
|
|
125
|
+
toString() {
|
|
126
|
+
const parts = [`SonioxHttpError [${this.code}]: ${this.message}`];
|
|
127
|
+
parts.push(` Method: ${this.method}`);
|
|
128
|
+
parts.push(` URL: ${this.url}`);
|
|
129
|
+
if (this.statusCode !== void 0) parts.push(` Status: ${this.statusCode}`);
|
|
130
|
+
return parts.join("\n");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Converts to a plain object for logging/serialization
|
|
134
|
+
*/
|
|
135
|
+
toJSON() {
|
|
136
|
+
return {
|
|
137
|
+
name: this.name,
|
|
138
|
+
code: this.code,
|
|
139
|
+
message: this.message,
|
|
140
|
+
url: this.url,
|
|
141
|
+
method: this.method,
|
|
142
|
+
...this.statusCode !== void 0 && { statusCode: this.statusCode },
|
|
143
|
+
...this.headers !== void 0 && { headers: this.headers },
|
|
144
|
+
...this.bodyText !== void 0 && { bodyText: this.bodyText }
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Creates a network error
|
|
150
|
+
*/
|
|
151
|
+
function createNetworkError(url, method, cause) {
|
|
152
|
+
return new SonioxHttpError({
|
|
153
|
+
code: "network_error",
|
|
154
|
+
message: `Network error: ${cause instanceof Error ? cause.message : "Network request failed"}`,
|
|
155
|
+
url,
|
|
156
|
+
method,
|
|
157
|
+
cause
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Creates a timeout error
|
|
162
|
+
*/
|
|
163
|
+
function createTimeoutError(url, method, timeoutMs) {
|
|
164
|
+
return new SonioxHttpError({
|
|
165
|
+
code: "timeout",
|
|
166
|
+
message: `Request timed out after ${timeoutMs}ms`,
|
|
167
|
+
url,
|
|
168
|
+
method
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Creates an abort error
|
|
173
|
+
*/
|
|
174
|
+
function createAbortError(url, method, cause) {
|
|
175
|
+
return new SonioxHttpError({
|
|
176
|
+
code: "aborted",
|
|
177
|
+
message: "Request was aborted",
|
|
178
|
+
url,
|
|
179
|
+
method,
|
|
180
|
+
cause
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Creates an HTTP error (non-2xx status)
|
|
185
|
+
*/
|
|
186
|
+
function createHttpError(url, method, statusCode, headers, bodyText) {
|
|
187
|
+
const cappedBody = truncateBodyText(bodyText);
|
|
188
|
+
return new SonioxHttpError({
|
|
189
|
+
code: "http_error",
|
|
190
|
+
message: `HTTP ${statusCode}`,
|
|
191
|
+
url,
|
|
192
|
+
method,
|
|
193
|
+
statusCode,
|
|
194
|
+
headers,
|
|
195
|
+
bodyText: cappedBody
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Creates a parse error (invalid JSON, etc.)
|
|
200
|
+
*/
|
|
201
|
+
function createParseError(url, method, bodyText, cause) {
|
|
202
|
+
const message = cause instanceof Error ? cause.message : "Failed to parse response";
|
|
203
|
+
const cappedBody = truncateBodyText(bodyText);
|
|
204
|
+
return new SonioxHttpError({
|
|
205
|
+
code: "parse_error",
|
|
206
|
+
message: `Parse error: ${message}`,
|
|
207
|
+
url,
|
|
208
|
+
method,
|
|
209
|
+
bodyText: cappedBody,
|
|
210
|
+
cause
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Truncates body text to the maximum allowed length
|
|
215
|
+
*/
|
|
216
|
+
function truncateBodyText(text) {
|
|
217
|
+
if (text.length <= MAX_BODY_TEXT_LENGTH) return text;
|
|
218
|
+
return text.slice(0, MAX_BODY_TEXT_LENGTH) + "... [truncated]";
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Type guard to check if an error is an AbortError
|
|
222
|
+
*/
|
|
223
|
+
function isAbortError(error) {
|
|
224
|
+
if (error instanceof Error) return error.name === "AbortError" || error.name === "TimeoutError";
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Type guard to check if an error is any SonioxError (base class).
|
|
229
|
+
* This catches all SDK errors including HTTP and real-time errors.
|
|
230
|
+
*/
|
|
231
|
+
function isSonioxError(error) {
|
|
232
|
+
return error instanceof SonioxError;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Type guard to check if an error is a SonioxHttpError
|
|
236
|
+
*/
|
|
237
|
+
function isSonioxHttpError(error) {
|
|
238
|
+
return error instanceof SonioxHttpError;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Checks if an error is a 404 Not Found error
|
|
242
|
+
*/
|
|
243
|
+
function isNotFoundError(error) {
|
|
244
|
+
return isSonioxHttpError(error) && error.statusCode === 404;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/async/files.ts
|
|
249
|
+
/**
|
|
250
|
+
* Uploaded file
|
|
251
|
+
*/
|
|
252
|
+
var SonioxFile = class {
|
|
253
|
+
id;
|
|
254
|
+
filename;
|
|
255
|
+
size;
|
|
256
|
+
created_at;
|
|
257
|
+
client_reference_id;
|
|
258
|
+
constructor(data, _http) {
|
|
259
|
+
this._http = _http;
|
|
260
|
+
this.id = data.id;
|
|
261
|
+
this.filename = data.filename;
|
|
262
|
+
this.size = data.size;
|
|
263
|
+
this.created_at = data.created_at;
|
|
264
|
+
if (data.client_reference_id) {
|
|
265
|
+
if (data.client_reference_id.length > 256) throw new Error("client_reference_id exceeds maximum length of 256 characters");
|
|
266
|
+
this.client_reference_id = data.client_reference_id;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Returns the raw data for this file.
|
|
271
|
+
*/
|
|
272
|
+
toJSON() {
|
|
273
|
+
return {
|
|
274
|
+
id: this.id,
|
|
275
|
+
filename: this.filename,
|
|
276
|
+
size: this.size,
|
|
277
|
+
created_at: this.created_at,
|
|
278
|
+
client_reference_id: this.client_reference_id
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Permanently deletes this file.
|
|
283
|
+
* This operation is idempotent - succeeds even if the file doesn't exist.
|
|
284
|
+
*
|
|
285
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
286
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```typescript
|
|
290
|
+
* const file = await client.files.get('550e8400-e29b-41d4-a716-446655440000');
|
|
291
|
+
* if (file) {
|
|
292
|
+
* await file.delete();
|
|
293
|
+
* }
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
async delete(signal) {
|
|
297
|
+
try {
|
|
298
|
+
await this._http.request({
|
|
299
|
+
method: "DELETE",
|
|
300
|
+
path: `/v1/files/${this.id}`,
|
|
301
|
+
...signal && { signal }
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (!isNotFoundError(error)) throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Result set for file listing
|
|
310
|
+
*/
|
|
311
|
+
var FileListResult = class {
|
|
312
|
+
/**
|
|
313
|
+
* Files from the first page of results
|
|
314
|
+
*/
|
|
315
|
+
files;
|
|
316
|
+
/**
|
|
317
|
+
* Pagination cursor for the next page. Null if no more pages
|
|
318
|
+
*/
|
|
319
|
+
next_page_cursor;
|
|
320
|
+
constructor(initialResponse, _http, _limit, _signal = void 0) {
|
|
321
|
+
this._http = _http;
|
|
322
|
+
this._limit = _limit;
|
|
323
|
+
this._signal = _signal;
|
|
324
|
+
this.files = initialResponse.files.map((data) => new SonioxFile(data, _http));
|
|
325
|
+
this.next_page_cursor = initialResponse.next_page_cursor;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Returns the raw data for this list result.
|
|
329
|
+
* Also used by JSON.stringify() to prevent serialization of internal HTTP client.
|
|
330
|
+
*/
|
|
331
|
+
toJSON() {
|
|
332
|
+
return {
|
|
333
|
+
files: this.files.map((f) => f.toJSON()),
|
|
334
|
+
next_page_cursor: this.next_page_cursor
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Returns true if there are more pages of results beyond the first page
|
|
339
|
+
*/
|
|
340
|
+
isPaged() {
|
|
341
|
+
return this.next_page_cursor !== null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Async iterator that automatically fetches all pages
|
|
345
|
+
* Use with `for await...of` to iterate through all files
|
|
346
|
+
*/
|
|
347
|
+
async *[Symbol.asyncIterator]() {
|
|
348
|
+
for (const file of this.files) yield file;
|
|
349
|
+
let cursor = this.next_page_cursor;
|
|
350
|
+
while (cursor !== null) {
|
|
351
|
+
const response = await this._http.request({
|
|
352
|
+
method: "GET",
|
|
353
|
+
path: "/v1/files",
|
|
354
|
+
query: {
|
|
355
|
+
limit: this._limit,
|
|
356
|
+
cursor
|
|
357
|
+
},
|
|
358
|
+
...this._signal && { signal: this._signal }
|
|
359
|
+
});
|
|
360
|
+
for (const data of response.data.files) yield new SonioxFile(data, this._http);
|
|
361
|
+
cursor = response.data.next_page_cursor;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
366
|
+
* Helper to extract file ID from a FileIdentifier
|
|
367
|
+
*/
|
|
368
|
+
function getFileId(file) {
|
|
369
|
+
return typeof file === "string" ? file : file.id;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Maximum file size allowed by the API (1 GB)
|
|
373
|
+
*/
|
|
374
|
+
const MAX_FILE_SIZE = 1073741824;
|
|
375
|
+
/**
|
|
376
|
+
* Default filename when none can be inferred
|
|
377
|
+
*/
|
|
378
|
+
const DEFAULT_FILENAME = "file";
|
|
379
|
+
/**
|
|
380
|
+
* Checks if the input is an async-iterable Node.js readable stream
|
|
381
|
+
*/
|
|
382
|
+
function isNodeReadableStream(input) {
|
|
383
|
+
return typeof input === "object" && input !== null && "pipe" in input && typeof input.pipe === "function" && Symbol.asyncIterator in input;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Checks if the input is a Web ReadableStream
|
|
387
|
+
*/
|
|
388
|
+
function isWebReadableStream(input) {
|
|
389
|
+
return typeof input === "object" && input !== null && "getReader" in input && typeof input.getReader === "function";
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Collects chunks from a Node.js readable stream into a Buffer
|
|
393
|
+
* Aborts early if size exceeds MAX_FILE_SIZE to prevent OOM
|
|
394
|
+
*/
|
|
395
|
+
async function collectNodeStream(stream) {
|
|
396
|
+
const chunks = [];
|
|
397
|
+
let totalLength = 0;
|
|
398
|
+
for await (const chunk of stream) {
|
|
399
|
+
if (typeof chunk === "string") throw new Error("Stream returned string chunks. Use a binary stream (e.g., fs.createReadStream without encoding option).");
|
|
400
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
401
|
+
totalLength += buf.length;
|
|
402
|
+
if (totalLength > MAX_FILE_SIZE) throw new Error(`File size exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
|
|
403
|
+
chunks.push(buf);
|
|
404
|
+
}
|
|
405
|
+
return Buffer.concat(chunks);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Collects chunks from a Web ReadableStream into a Uint8Array
|
|
409
|
+
* Aborts early if size exceeds MAX_FILE_SIZE to prevent OOM
|
|
410
|
+
*/
|
|
411
|
+
async function collectWebStream(stream) {
|
|
412
|
+
const reader = stream.getReader();
|
|
413
|
+
const chunks = [];
|
|
414
|
+
let totalLength = 0;
|
|
415
|
+
try {
|
|
416
|
+
while (true) {
|
|
417
|
+
const { done, value } = await reader.read();
|
|
418
|
+
if (done) break;
|
|
419
|
+
totalLength += value.length;
|
|
420
|
+
if (totalLength > MAX_FILE_SIZE) throw new Error(`File size exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
|
|
421
|
+
chunks.push(value);
|
|
422
|
+
}
|
|
423
|
+
} finally {
|
|
424
|
+
reader.releaseLock();
|
|
425
|
+
}
|
|
426
|
+
const result = new Uint8Array(totalLength);
|
|
427
|
+
let offset = 0;
|
|
428
|
+
for (const chunk of chunks) {
|
|
429
|
+
result.set(chunk, offset);
|
|
430
|
+
offset += chunk.length;
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Resolves the file input to a Blob and filename
|
|
436
|
+
*/
|
|
437
|
+
async function resolveFileInput(input, filenameOverride) {
|
|
438
|
+
if (input instanceof Blob) {
|
|
439
|
+
if (input.size > MAX_FILE_SIZE) throw new Error(`File size (${input.size} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
|
|
440
|
+
return {
|
|
441
|
+
blob: input,
|
|
442
|
+
filename: filenameOverride ?? ("name" in input && typeof input.name === "string" ? input.name : DEFAULT_FILENAME)
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (Buffer.isBuffer(input)) {
|
|
446
|
+
if (input.length > MAX_FILE_SIZE) throw new Error(`File size (${input.length} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
|
|
447
|
+
return {
|
|
448
|
+
blob: new Blob([new Uint8Array(input)]),
|
|
449
|
+
filename: filenameOverride ?? DEFAULT_FILENAME
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (input instanceof Uint8Array) {
|
|
453
|
+
if (input.length > MAX_FILE_SIZE) throw new Error(`File size (${input.length} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
|
|
454
|
+
return {
|
|
455
|
+
blob: new Blob([new Uint8Array(input)]),
|
|
456
|
+
filename: filenameOverride ?? DEFAULT_FILENAME
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (isWebReadableStream(input)) {
|
|
460
|
+
const data = await collectWebStream(input);
|
|
461
|
+
return {
|
|
462
|
+
blob: new Blob([new Uint8Array(data)]),
|
|
463
|
+
filename: filenameOverride ?? DEFAULT_FILENAME
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (isNodeReadableStream(input)) {
|
|
467
|
+
const buffer = await collectNodeStream(input);
|
|
468
|
+
return {
|
|
469
|
+
blob: new Blob([new Uint8Array(buffer)]),
|
|
470
|
+
filename: filenameOverride ?? DEFAULT_FILENAME
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
throw new Error("Invalid file input. Expected Buffer, Uint8Array, Blob, or ReadableStream.");
|
|
474
|
+
}
|
|
475
|
+
var SonioxFilesAPI = class {
|
|
476
|
+
constructor(http) {
|
|
477
|
+
this.http = http;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Uploads a file to Soniox for transcription
|
|
481
|
+
*
|
|
482
|
+
* @param file - Buffer, Uint8Array, Blob, or ReadableStream
|
|
483
|
+
* @param options - Upload options
|
|
484
|
+
* @returns The uploaded file metadata
|
|
485
|
+
* @throws {SonioxHttpError} On API errors
|
|
486
|
+
* @throws {Error} On validation errors (file too large, invalid input)
|
|
487
|
+
*
|
|
488
|
+
* @example Upload from file path (Node.js)
|
|
489
|
+
* ```typescript
|
|
490
|
+
* import * as fs from 'node:fs';
|
|
491
|
+
*
|
|
492
|
+
* const buffer = await fs.promises.readFile('/path/to/audio.mp3');
|
|
493
|
+
* const file = await client.files.upload(buffer, { filename: 'audio.mp3' });
|
|
494
|
+
* ```
|
|
495
|
+
*
|
|
496
|
+
* @example Upload from file path (Bun)
|
|
497
|
+
* ```typescript
|
|
498
|
+
* const file = await client.files.upload(Bun.file('/path/to/audio.mp3'));
|
|
499
|
+
* ```
|
|
500
|
+
*
|
|
501
|
+
* @example Upload with tracking ID
|
|
502
|
+
* ```typescript
|
|
503
|
+
* const file = await client.files.upload(buffer, {
|
|
504
|
+
* filename: 'audio.mp3',
|
|
505
|
+
* client_reference_id: 'order-12345',
|
|
506
|
+
* });
|
|
507
|
+
* ```
|
|
508
|
+
*
|
|
509
|
+
* @example Upload with cancellation
|
|
510
|
+
* ```typescript
|
|
511
|
+
* const controller = new AbortController();
|
|
512
|
+
* setTimeout(() => controller.abort(), 30000);
|
|
513
|
+
*
|
|
514
|
+
* const file = await client.files.upload(buffer, {
|
|
515
|
+
* filename: 'audio.mp3',
|
|
516
|
+
* signal: controller.signal,
|
|
517
|
+
* });
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
520
|
+
async upload(file, options = {}) {
|
|
521
|
+
const { filename, client_reference_id, signal, timeout_ms } = options;
|
|
522
|
+
if (client_reference_id !== void 0 && client_reference_id.length > 256) throw new Error(`client_reference_id exceeds maximum length of 256 characters (got ${client_reference_id.length})`);
|
|
523
|
+
const { blob, filename: resolvedFilename } = await resolveFileInput(file, filename);
|
|
524
|
+
const formData = new FormData();
|
|
525
|
+
formData.append("file", blob, resolvedFilename);
|
|
526
|
+
if (client_reference_id !== void 0) formData.append("client_reference_id", client_reference_id);
|
|
527
|
+
const requestOptions = {
|
|
528
|
+
method: "POST",
|
|
529
|
+
path: "/v1/files",
|
|
530
|
+
body: formData
|
|
531
|
+
};
|
|
532
|
+
if (signal !== void 0) requestOptions.signal = signal;
|
|
533
|
+
if (timeout_ms !== void 0) requestOptions.timeoutMs = timeout_ms;
|
|
534
|
+
return new SonioxFile((await this.http.request(requestOptions)).data, this.http);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Retrieves list of uploaded files
|
|
538
|
+
*
|
|
539
|
+
* The returned result is async iterable - use `for await...of`
|
|
540
|
+
*
|
|
541
|
+
* @param options - Optional pagination and cancellation parameters
|
|
542
|
+
* @returns FileListResult
|
|
543
|
+
* @throws {SonioxHttpError}
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
* ```typescript
|
|
547
|
+
* const result = await client.files.list();
|
|
548
|
+
*
|
|
549
|
+
* // Automatic paging - iterates through ALL files across all pages
|
|
550
|
+
* for await (const file of result) {
|
|
551
|
+
* console.log(file.filename, file.size);
|
|
552
|
+
* }
|
|
553
|
+
*
|
|
554
|
+
* // Or access just the first page
|
|
555
|
+
* for (const file of result.files) {
|
|
556
|
+
* console.log(file.filename);
|
|
557
|
+
* }
|
|
558
|
+
*
|
|
559
|
+
* // Check if there are more pages
|
|
560
|
+
* if (result.isPaged()) {
|
|
561
|
+
* console.log('More pages available');
|
|
562
|
+
* }
|
|
563
|
+
*
|
|
564
|
+
* // Manual paging using cursor
|
|
565
|
+
* const page1 = await client.files.list({ limit: 10 });
|
|
566
|
+
* if (page1.next_page_cursor) {
|
|
567
|
+
* const page2 = await client.files.list({ cursor: page1.next_page_cursor });
|
|
568
|
+
* }
|
|
569
|
+
*
|
|
570
|
+
* // With cancellation
|
|
571
|
+
* const controller = new AbortController();
|
|
572
|
+
* const result = await client.files.list({ signal: controller.signal });
|
|
573
|
+
* ```
|
|
574
|
+
*/
|
|
575
|
+
async list(options = {}) {
|
|
576
|
+
const { limit, cursor, signal } = options;
|
|
577
|
+
return new FileListResult((await this.http.request({
|
|
578
|
+
method: "GET",
|
|
579
|
+
path: "/v1/files",
|
|
580
|
+
query: {
|
|
581
|
+
limit,
|
|
582
|
+
cursor
|
|
583
|
+
},
|
|
584
|
+
...signal && { signal }
|
|
585
|
+
})).data, this.http, limit, signal);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Retrieve metadata for an uploaded file.
|
|
589
|
+
*
|
|
590
|
+
* @param file - The UUID of the file or a SonioxFile instance
|
|
591
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
592
|
+
* @returns The file instance, or null if not found
|
|
593
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```typescript
|
|
597
|
+
* const file = await client.files.get('550e8400-e29b-41d4-a716-446655440000');
|
|
598
|
+
* if (file) {
|
|
599
|
+
* console.log(file.filename, file.size);
|
|
600
|
+
* }
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
async get(file, signal) {
|
|
604
|
+
const file_id = getFileId(file);
|
|
605
|
+
try {
|
|
606
|
+
return new SonioxFile((await this.http.request({
|
|
607
|
+
method: "GET",
|
|
608
|
+
path: `/v1/files/${file_id}`,
|
|
609
|
+
...signal && { signal }
|
|
610
|
+
})).data, this.http);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (isNotFoundError(error)) return null;
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Permanently deletes a file.
|
|
618
|
+
* This operation is idempotent - succeeds even if the file doesn't exist.
|
|
619
|
+
*
|
|
620
|
+
* @param file - The UUID of the file or a SonioxFile instance
|
|
621
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
622
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* ```typescript
|
|
626
|
+
* // Delete by ID
|
|
627
|
+
* await client.files.delete('550e8400-e29b-41d4-a716-446655440000');
|
|
628
|
+
*
|
|
629
|
+
* // Or delete a file instance
|
|
630
|
+
* const file = await client.files.get('550e8400-e29b-41d4-a716-446655440000');
|
|
631
|
+
* if (file) {
|
|
632
|
+
* await client.files.delete(file);
|
|
633
|
+
* }
|
|
634
|
+
*
|
|
635
|
+
* // Or just use the instance method
|
|
636
|
+
* await file.delete();
|
|
637
|
+
* ```
|
|
638
|
+
*/
|
|
639
|
+
async delete(file, signal) {
|
|
640
|
+
const file_id = getFileId(file);
|
|
641
|
+
try {
|
|
642
|
+
await this.http.request({
|
|
643
|
+
method: "DELETE",
|
|
644
|
+
path: `/v1/files/${file_id}`,
|
|
645
|
+
...signal && { signal }
|
|
646
|
+
});
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (!isNotFoundError(error)) throw error;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Permanently deletes all uploaded files.
|
|
653
|
+
* Iterates through all pages of files and deletes each one.
|
|
654
|
+
*
|
|
655
|
+
* @param options - Optional signal and progress callback.
|
|
656
|
+
* @returns The number of files deleted.
|
|
657
|
+
* @throws {SonioxHttpError} On API errors.
|
|
658
|
+
* @throws {Error} If the operation is aborted via signal.
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* // Delete all files
|
|
663
|
+
* const { deleted } = await client.files.purge();
|
|
664
|
+
* console.log(`Deleted ${deleted} files.`);
|
|
665
|
+
*
|
|
666
|
+
* // With progress logging
|
|
667
|
+
* const { deleted } = await client.files.purge({
|
|
668
|
+
* on_progress: (file, index) => {
|
|
669
|
+
* console.log(`Deleting file: ${file.id} (${index + 1})`);
|
|
670
|
+
* },
|
|
671
|
+
* });
|
|
672
|
+
*
|
|
673
|
+
* // With cancellation
|
|
674
|
+
* const controller = new AbortController();
|
|
675
|
+
* const { deleted } = await client.files.purge({ signal: controller.signal });
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
async purge(options = {}) {
|
|
679
|
+
const { signal, on_progress } = options;
|
|
680
|
+
const result = await this.list({ signal });
|
|
681
|
+
let deleted = 0;
|
|
682
|
+
for await (const file of result) {
|
|
683
|
+
signal?.throwIfAborted();
|
|
684
|
+
on_progress?.(file.toJSON(), deleted);
|
|
685
|
+
await this.delete(file, signal);
|
|
686
|
+
deleted++;
|
|
687
|
+
}
|
|
688
|
+
return { deleted };
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/async/models.ts
|
|
694
|
+
var SonioxModelsAPI = class {
|
|
695
|
+
constructor(http) {
|
|
696
|
+
this.http = http;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* List of available models and their attributes.
|
|
700
|
+
* @see https://soniox.com/docs/stt/api-reference/models/get_models
|
|
701
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
702
|
+
* @returns List of available models and their attributes.
|
|
703
|
+
*/
|
|
704
|
+
async list(signal) {
|
|
705
|
+
return (await this.http.request({
|
|
706
|
+
method: "GET",
|
|
707
|
+
path: "/v1/models",
|
|
708
|
+
...signal && { signal }
|
|
709
|
+
})).data.models;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/async/segments.ts
|
|
715
|
+
const DEFAULT_GROUP_BY = ["speaker", "language"];
|
|
716
|
+
function segmentTokens(tokens, options, buildSegment$2) {
|
|
717
|
+
if (tokens.length === 0) return [];
|
|
718
|
+
const groupBy = options?.group_by ?? DEFAULT_GROUP_BY;
|
|
719
|
+
const groupBySpeaker = groupBy.includes("speaker");
|
|
720
|
+
const groupByLanguage = groupBy.includes("language");
|
|
721
|
+
const segments = [];
|
|
722
|
+
let currentTokens = [];
|
|
723
|
+
let currentSpeaker;
|
|
724
|
+
let currentLanguage;
|
|
725
|
+
for (const token of tokens) {
|
|
726
|
+
const speakerChanged = groupBySpeaker && token.speaker !== currentSpeaker;
|
|
727
|
+
const languageChanged = groupByLanguage && token.language !== currentLanguage;
|
|
728
|
+
if (currentTokens.length > 0 && (speakerChanged || languageChanged)) {
|
|
729
|
+
segments.push(buildSegment$2(currentTokens, currentSpeaker, currentLanguage));
|
|
730
|
+
currentTokens = [];
|
|
731
|
+
}
|
|
732
|
+
currentTokens.push(token);
|
|
733
|
+
currentSpeaker = token.speaker;
|
|
734
|
+
currentLanguage = token.language;
|
|
735
|
+
}
|
|
736
|
+
if (currentTokens.length > 0) segments.push(buildSegment$2(currentTokens, currentSpeaker, currentLanguage));
|
|
737
|
+
return segments;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/async/stt.ts
|
|
742
|
+
/**
|
|
743
|
+
* Minimum polling interval in ms
|
|
744
|
+
*/
|
|
745
|
+
const MIN_POLL_INTERVAL_MS = 1e3;
|
|
746
|
+
/**
|
|
747
|
+
* Default polling interval in ms
|
|
748
|
+
*/
|
|
749
|
+
const DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
750
|
+
/**
|
|
751
|
+
* Default timeout for waiting in ms (5 minutes)
|
|
752
|
+
*/
|
|
753
|
+
const DEFAULT_TIMEOUT_MS$1 = 3e5;
|
|
754
|
+
/**
|
|
755
|
+
* Helper to sleep for a given number of ms, interruptible by abort signal
|
|
756
|
+
*/
|
|
757
|
+
function sleep(ms, signal) {
|
|
758
|
+
return new Promise((resolve, reject) => {
|
|
759
|
+
if (signal?.aborted) {
|
|
760
|
+
reject(/* @__PURE__ */ new Error("Transcription wait aborted"));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const timeoutId = setTimeout(() => {
|
|
764
|
+
signal?.removeEventListener("abort", onAbort);
|
|
765
|
+
resolve();
|
|
766
|
+
}, ms);
|
|
767
|
+
function onAbort() {
|
|
768
|
+
clearTimeout(timeoutId);
|
|
769
|
+
reject(/* @__PURE__ */ new Error("Transcription wait aborted"));
|
|
770
|
+
}
|
|
771
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Helper to extract transcription ID from a TranscriptionIdentifier
|
|
776
|
+
*/
|
|
777
|
+
function getTranscriptionId(transcription) {
|
|
778
|
+
return typeof transcription === "string" ? transcription : transcription.id;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Creates an AbortSignal that fires when either the timeout expires or the provided signal aborts
|
|
782
|
+
* Returns the signal and a cleanup function to clear the timeout
|
|
783
|
+
*/
|
|
784
|
+
function createTimeoutSignal(timeout_ms, signal) {
|
|
785
|
+
if (timeout_ms === void 0 && signal === void 0) return {
|
|
786
|
+
signal: void 0,
|
|
787
|
+
cleanup: () => {}
|
|
788
|
+
};
|
|
789
|
+
if (timeout_ms === void 0) return {
|
|
790
|
+
signal,
|
|
791
|
+
cleanup: () => {}
|
|
792
|
+
};
|
|
793
|
+
if (!Number.isFinite(timeout_ms) || timeout_ms <= 0) throw new Error("timeout_ms must be a finite positive number");
|
|
794
|
+
const timeoutController = new AbortController();
|
|
795
|
+
const timeoutId = setTimeout(() => {
|
|
796
|
+
timeoutController.abort(/* @__PURE__ */ new Error(`Operation timed out after ${timeout_ms}ms`));
|
|
797
|
+
}, timeout_ms);
|
|
798
|
+
const cleanup = () => clearTimeout(timeoutId);
|
|
799
|
+
if (signal === void 0) return {
|
|
800
|
+
signal: timeoutController.signal,
|
|
801
|
+
cleanup
|
|
802
|
+
};
|
|
803
|
+
const combinedController = new AbortController();
|
|
804
|
+
const onAbort = () => {
|
|
805
|
+
cleanup();
|
|
806
|
+
timeoutController.signal.removeEventListener("abort", onTimeout);
|
|
807
|
+
combinedController.abort(signal.reason ?? /* @__PURE__ */ new Error("Operation aborted"));
|
|
808
|
+
};
|
|
809
|
+
const onTimeout = () => {
|
|
810
|
+
signal.removeEventListener("abort", onAbort);
|
|
811
|
+
combinedController.abort(timeoutController.signal.reason);
|
|
812
|
+
};
|
|
813
|
+
if (signal.aborted) {
|
|
814
|
+
cleanup();
|
|
815
|
+
combinedController.abort(signal.reason ?? /* @__PURE__ */ new Error("Operation aborted"));
|
|
816
|
+
} else {
|
|
817
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
818
|
+
timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
signal: combinedController.signal,
|
|
822
|
+
cleanup: () => {
|
|
823
|
+
cleanup();
|
|
824
|
+
signal.removeEventListener("abort", onAbort);
|
|
825
|
+
timeoutController.signal.removeEventListener("abort", onTimeout);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Groups contiguous tokens into segments based on specified grouping keys.
|
|
831
|
+
* A new segment starts when any of the `group_by` fields changes
|
|
832
|
+
*
|
|
833
|
+
* @param tokens - Array of transcript tokens to segment
|
|
834
|
+
* @param options - Segmentation options
|
|
835
|
+
* @param options.group_by - Fields to group by (default: ['speaker', 'language'])
|
|
836
|
+
* @returns Array of segments with combined text and timing
|
|
837
|
+
*
|
|
838
|
+
* @example
|
|
839
|
+
* ```typescript
|
|
840
|
+
* const transcript = await transcription.getTranscript();
|
|
841
|
+
*
|
|
842
|
+
* // Group by both speaker and language (default)
|
|
843
|
+
* const segments = segmentTranscript(transcript.tokens);
|
|
844
|
+
*
|
|
845
|
+
* // Group by speaker only
|
|
846
|
+
* const bySpeaker = segmentTranscript(transcript.tokens, { group_by: ['speaker'] });
|
|
847
|
+
*
|
|
848
|
+
* // Group by language only
|
|
849
|
+
* const byLanguage = segmentTranscript(transcript.tokens, { group_by: ['language'] });
|
|
850
|
+
*
|
|
851
|
+
* for (const seg of segments) {
|
|
852
|
+
* console.log(`[Speaker ${seg.speaker}] ${seg.text}`);
|
|
853
|
+
* }
|
|
854
|
+
* ```
|
|
855
|
+
*/
|
|
856
|
+
function segmentTranscript(tokens, options = {}) {
|
|
857
|
+
return segmentTokens(tokens, options, buildSegment$1);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Helper to build a segment from a list of tokens
|
|
861
|
+
*/
|
|
862
|
+
function buildSegment$1(tokens, speaker, language) {
|
|
863
|
+
const firstToken = tokens[0];
|
|
864
|
+
const lastToken = tokens[tokens.length - 1];
|
|
865
|
+
if (!firstToken || !lastToken) throw new Error("Cannot build segment from an empty token array");
|
|
866
|
+
return {
|
|
867
|
+
text: tokens.map((t) => t.text).join(""),
|
|
868
|
+
start_ms: firstToken.start_ms,
|
|
869
|
+
end_ms: lastToken.end_ms,
|
|
870
|
+
...speaker != null && { speaker },
|
|
871
|
+
...language != null && { language },
|
|
872
|
+
tokens
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* A Transcript result containing the transcribed text and tokens.
|
|
877
|
+
*/
|
|
878
|
+
var SonioxTranscript = class {
|
|
879
|
+
/**
|
|
880
|
+
* Unique identifier of the transcription this transcript belongs to.
|
|
881
|
+
*/
|
|
882
|
+
id;
|
|
883
|
+
/**
|
|
884
|
+
* Complete transcribed text content.
|
|
885
|
+
*/
|
|
886
|
+
text;
|
|
887
|
+
/**
|
|
888
|
+
* List of detailed token information with timestamps and metadata.
|
|
889
|
+
*/
|
|
890
|
+
tokens;
|
|
891
|
+
constructor(data) {
|
|
892
|
+
this.id = data.id;
|
|
893
|
+
this.text = data.text;
|
|
894
|
+
this.tokens = data.tokens;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Groups tokens into segments based on specified grouping keys.
|
|
898
|
+
*
|
|
899
|
+
* A new segment starts when any of the `group_by` fields changes.
|
|
900
|
+
*
|
|
901
|
+
* @param options - Segmentation options
|
|
902
|
+
* @param options.group_by - Fields to group by (default: ['speaker', 'language'])
|
|
903
|
+
* @returns Array of segments with combined text and timing
|
|
904
|
+
*
|
|
905
|
+
* @example
|
|
906
|
+
* ```typescript
|
|
907
|
+
* const transcript = await transcription.getTranscript();
|
|
908
|
+
*
|
|
909
|
+
* // Group by both speaker and language (default)
|
|
910
|
+
* const segments = transcript.segments();
|
|
911
|
+
*
|
|
912
|
+
* // Group by speaker only
|
|
913
|
+
* const bySpeaker = transcript.segments({ group_by: ['speaker'] });
|
|
914
|
+
*
|
|
915
|
+
* for (const s of segments) {
|
|
916
|
+
* console.log(`[Speaker ${s.speaker}] ${s.text}`);
|
|
917
|
+
* }
|
|
918
|
+
* ```
|
|
919
|
+
*/
|
|
920
|
+
segments(options) {
|
|
921
|
+
return segmentTranscript(this.tokens, options);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
/**
|
|
925
|
+
* A Transcription instance
|
|
926
|
+
*/
|
|
927
|
+
var SonioxTranscription = class SonioxTranscription {
|
|
928
|
+
/**
|
|
929
|
+
* Unique identifier of the transcription.
|
|
930
|
+
*/
|
|
931
|
+
id;
|
|
932
|
+
/**
|
|
933
|
+
* Current status of the transcription.
|
|
934
|
+
*/
|
|
935
|
+
status;
|
|
936
|
+
/**
|
|
937
|
+
* UTC timestamp when the transcription was created.
|
|
938
|
+
*/
|
|
939
|
+
created_at;
|
|
940
|
+
/**
|
|
941
|
+
* Speech-to-text model used.
|
|
942
|
+
*/
|
|
943
|
+
model;
|
|
944
|
+
/**
|
|
945
|
+
* URL of the audio file being transcribed.
|
|
946
|
+
*/
|
|
947
|
+
audio_url;
|
|
948
|
+
/**
|
|
949
|
+
* ID of the uploaded file being transcribed.
|
|
950
|
+
*/
|
|
951
|
+
file_id;
|
|
952
|
+
/**
|
|
953
|
+
* Name of the file being transcribed.
|
|
954
|
+
*/
|
|
955
|
+
filename;
|
|
956
|
+
/**
|
|
957
|
+
* Expected languages in the audio.
|
|
958
|
+
*/
|
|
959
|
+
language_hints;
|
|
960
|
+
/**
|
|
961
|
+
* When true, speakers are identified and separated in the transcription output.
|
|
962
|
+
*/
|
|
963
|
+
enable_speaker_diarization;
|
|
964
|
+
/**
|
|
965
|
+
* When true, language is detected for each part of the transcription.
|
|
966
|
+
*/
|
|
967
|
+
enable_language_identification;
|
|
968
|
+
/**
|
|
969
|
+
* Duration of the audio in milliseconds. Only available after processing begins.
|
|
970
|
+
*/
|
|
971
|
+
audio_duration_ms;
|
|
972
|
+
/**
|
|
973
|
+
* Error type if transcription failed.
|
|
974
|
+
*/
|
|
975
|
+
error_type;
|
|
976
|
+
/**
|
|
977
|
+
* Error message if transcription failed.
|
|
978
|
+
*/
|
|
979
|
+
error_message;
|
|
980
|
+
/**
|
|
981
|
+
* URL to receive webhook notifications.
|
|
982
|
+
*/
|
|
983
|
+
webhook_url;
|
|
984
|
+
/**
|
|
985
|
+
* Name of the authentication header sent with webhook notifications.
|
|
986
|
+
*/
|
|
987
|
+
webhook_auth_header_name;
|
|
988
|
+
/**
|
|
989
|
+
* Authentication header value (masked).
|
|
990
|
+
*/
|
|
991
|
+
webhook_auth_header_value;
|
|
992
|
+
/**
|
|
993
|
+
* HTTP status code received when webhook was delivered.
|
|
994
|
+
*/
|
|
995
|
+
webhook_status_code;
|
|
996
|
+
/**
|
|
997
|
+
* Optional tracking identifier.
|
|
998
|
+
*/
|
|
999
|
+
client_reference_id;
|
|
1000
|
+
/**
|
|
1001
|
+
* Additional context provided for the transcription.
|
|
1002
|
+
*/
|
|
1003
|
+
context;
|
|
1004
|
+
/**
|
|
1005
|
+
* Pre-fetched transcript. Only available when using `transcribe()` with `wait: true`,
|
|
1006
|
+
* `fetch_transcript !== false`, and the transcription completed successfully
|
|
1007
|
+
*/
|
|
1008
|
+
transcript;
|
|
1009
|
+
constructor(data, _http, transcript) {
|
|
1010
|
+
this._http = _http;
|
|
1011
|
+
this.id = data.id;
|
|
1012
|
+
this.status = data.status;
|
|
1013
|
+
this.created_at = data.created_at;
|
|
1014
|
+
this.model = data.model;
|
|
1015
|
+
this.audio_url = data.audio_url;
|
|
1016
|
+
this.file_id = data.file_id;
|
|
1017
|
+
this.filename = data.filename;
|
|
1018
|
+
this.language_hints = data.language_hints ?? void 0;
|
|
1019
|
+
this.enable_speaker_diarization = data.enable_speaker_diarization;
|
|
1020
|
+
this.enable_language_identification = data.enable_language_identification;
|
|
1021
|
+
this.audio_duration_ms = data.audio_duration_ms;
|
|
1022
|
+
this.error_type = data.error_type;
|
|
1023
|
+
this.error_message = data.error_message;
|
|
1024
|
+
this.webhook_url = data.webhook_url;
|
|
1025
|
+
this.webhook_auth_header_name = data.webhook_auth_header_name;
|
|
1026
|
+
this.webhook_auth_header_value = data.webhook_auth_header_value;
|
|
1027
|
+
this.webhook_status_code = data.webhook_status_code;
|
|
1028
|
+
this.client_reference_id = data.client_reference_id;
|
|
1029
|
+
this.context = data.context;
|
|
1030
|
+
this.transcript = transcript;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Returns the raw data for this transcription.
|
|
1034
|
+
*/
|
|
1035
|
+
toJSON() {
|
|
1036
|
+
return {
|
|
1037
|
+
id: this.id,
|
|
1038
|
+
status: this.status,
|
|
1039
|
+
created_at: this.created_at,
|
|
1040
|
+
model: this.model,
|
|
1041
|
+
audio_url: this.audio_url,
|
|
1042
|
+
file_id: this.file_id,
|
|
1043
|
+
filename: this.filename,
|
|
1044
|
+
language_hints: this.language_hints,
|
|
1045
|
+
enable_speaker_diarization: this.enable_speaker_diarization,
|
|
1046
|
+
enable_language_identification: this.enable_language_identification,
|
|
1047
|
+
audio_duration_ms: this.audio_duration_ms,
|
|
1048
|
+
error_type: this.error_type,
|
|
1049
|
+
error_message: this.error_message,
|
|
1050
|
+
webhook_url: this.webhook_url,
|
|
1051
|
+
webhook_auth_header_name: this.webhook_auth_header_name,
|
|
1052
|
+
webhook_auth_header_value: this.webhook_auth_header_value,
|
|
1053
|
+
webhook_status_code: this.webhook_status_code,
|
|
1054
|
+
client_reference_id: this.client_reference_id,
|
|
1055
|
+
context: this.context
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Permanently deletes this transcription.
|
|
1060
|
+
* This operation is idempotent - succeeds even if the transcription doesn't exist.
|
|
1061
|
+
*
|
|
1062
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
1063
|
+
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* ```typescript
|
|
1066
|
+
* const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
|
|
1067
|
+
* await transcription.delete();
|
|
1068
|
+
* ```
|
|
1069
|
+
*/
|
|
1070
|
+
async delete() {
|
|
1071
|
+
try {
|
|
1072
|
+
await this._http.request({
|
|
1073
|
+
method: "DELETE",
|
|
1074
|
+
path: `/v1/transcriptions/${this.id}`
|
|
1075
|
+
});
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
if (!isNotFoundError(error)) throw error;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Permanently deletes this transcription and its associated file (if any).
|
|
1082
|
+
* This operation is idempotent - succeeds even if resources don't exist.
|
|
1083
|
+
*
|
|
1084
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
1085
|
+
*
|
|
1086
|
+
* @example
|
|
1087
|
+
* ```typescript
|
|
1088
|
+
* // Clean up both transcription and uploaded file
|
|
1089
|
+
* const transcription = await client.stt.transcribe({
|
|
1090
|
+
* model: 'stt-async-v4',
|
|
1091
|
+
* file: buffer,
|
|
1092
|
+
* wait: true,
|
|
1093
|
+
* });
|
|
1094
|
+
* // ... use transcription ...
|
|
1095
|
+
* await transcription.destroy(); // Deletes both transcription and file
|
|
1096
|
+
* ```
|
|
1097
|
+
*/
|
|
1098
|
+
async destroy() {
|
|
1099
|
+
await this.delete();
|
|
1100
|
+
if (this.file_id) try {
|
|
1101
|
+
await this._http.request({
|
|
1102
|
+
method: "DELETE",
|
|
1103
|
+
path: `/v1/files/${this.file_id}`
|
|
1104
|
+
});
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
if (!isNotFoundError(error)) throw error;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Retrieves the full transcript text and tokens for this transcription.
|
|
1111
|
+
* Only available for successfully completed transcriptions.
|
|
1112
|
+
*
|
|
1113
|
+
* Returns cached transcript if available (when using `transcribe()` with `wait: true`).
|
|
1114
|
+
* Use `force: true` to bypass the cache and fetch fresh data from the API.
|
|
1115
|
+
*
|
|
1116
|
+
* @param options - Optional settings
|
|
1117
|
+
* @param options.force - If true, bypasses cached transcript and fetches from API
|
|
1118
|
+
* @param options.signal - Optional AbortSignal for request cancellation
|
|
1119
|
+
* @returns The transcript with text and detailed tokens, or null if not found.
|
|
1120
|
+
* @throws {SonioxHttpError} On API errors (except 404).
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
|
|
1125
|
+
* if (transcription) {
|
|
1126
|
+
* const transcript = await transcription.getTranscript();
|
|
1127
|
+
* if (transcript) {
|
|
1128
|
+
* console.log(transcript.text);
|
|
1129
|
+
* }
|
|
1130
|
+
* }
|
|
1131
|
+
*
|
|
1132
|
+
* // Force re-fetch from API
|
|
1133
|
+
* const freshTranscript = await transcription.getTranscript({ force: true });
|
|
1134
|
+
* ```
|
|
1135
|
+
*/
|
|
1136
|
+
async getTranscript(options) {
|
|
1137
|
+
const { force, signal } = options ?? {};
|
|
1138
|
+
if (!force && this.transcript !== void 0) return this.transcript;
|
|
1139
|
+
try {
|
|
1140
|
+
return new SonioxTranscript((await this._http.request({
|
|
1141
|
+
method: "GET",
|
|
1142
|
+
path: `/v1/transcriptions/${this.id}/transcript`,
|
|
1143
|
+
...signal && { signal }
|
|
1144
|
+
})).data);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (isNotFoundError(error)) return null;
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Re-fetches this transcription to get the latest status.
|
|
1152
|
+
* @param signal - Optional AbortSignal for request cancellation.
|
|
1153
|
+
* @returns A new SonioxTranscription instance with updated data.
|
|
1154
|
+
* @throws {SonioxHttpError}
|
|
1155
|
+
*
|
|
1156
|
+
* @example
|
|
1157
|
+
* ```typescript
|
|
1158
|
+
* let transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
|
|
1159
|
+
* transcription = await transcription.refresh();
|
|
1160
|
+
* console.log(transcription.status);
|
|
1161
|
+
* ```
|
|
1162
|
+
*/
|
|
1163
|
+
async refresh(signal) {
|
|
1164
|
+
return new SonioxTranscription((await this._http.request({
|
|
1165
|
+
method: "GET",
|
|
1166
|
+
path: `/v1/transcriptions/${this.id}`,
|
|
1167
|
+
...signal && { signal }
|
|
1168
|
+
})).data, this._http);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Waits for the transcription to complete or fail.
|
|
1172
|
+
* Polls the API at the specified interval until the status is 'completed' or 'error'.
|
|
1173
|
+
*
|
|
1174
|
+
* @param options - Wait options including polling interval, timeout, and callbacks.
|
|
1175
|
+
* @returns The completed or errored transcription.
|
|
1176
|
+
* @throws {Error} If the wait times out or is aborted.
|
|
1177
|
+
* @throws {SonioxHttpError} On API errors.
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```typescript
|
|
1181
|
+
* const transcription = await client.stt.create({
|
|
1182
|
+
* model: 'stt-async-v4',
|
|
1183
|
+
* audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
|
|
1184
|
+
* });
|
|
1185
|
+
*
|
|
1186
|
+
* // Simple wait
|
|
1187
|
+
* const completed = await transcription.wait();
|
|
1188
|
+
*
|
|
1189
|
+
* // Wait with progress callback
|
|
1190
|
+
* const completed = await transcription.wait({
|
|
1191
|
+
* interval_ms: 2000,
|
|
1192
|
+
* on_status_change: (status) => console.log(`Status: ${status}`),
|
|
1193
|
+
* });
|
|
1194
|
+
* ```
|
|
1195
|
+
*/
|
|
1196
|
+
async wait(options = {}) {
|
|
1197
|
+
const { interval_ms: requestedInterval = DEFAULT_POLL_INTERVAL_MS, timeout_ms = DEFAULT_TIMEOUT_MS$1, on_status_change, signal } = options;
|
|
1198
|
+
const interval_ms = Math.max(requestedInterval, MIN_POLL_INTERVAL_MS);
|
|
1199
|
+
if (this.status !== "queued" && this.status !== "processing") return this;
|
|
1200
|
+
if (signal?.aborted) throw new Error("Transcription wait aborted");
|
|
1201
|
+
const startTime = Date.now();
|
|
1202
|
+
const checkTimeout = () => {
|
|
1203
|
+
if (Date.now() - startTime > timeout_ms) throw new Error(`Transcription wait timed out after ${timeout_ms}ms`);
|
|
1204
|
+
};
|
|
1205
|
+
checkTimeout();
|
|
1206
|
+
let lastStatus = this.status;
|
|
1207
|
+
let current = await this.refresh(signal);
|
|
1208
|
+
while (current.status === "queued" || current.status === "processing") {
|
|
1209
|
+
if (current.status !== lastStatus) {
|
|
1210
|
+
on_status_change?.(current.status, current.toJSON());
|
|
1211
|
+
lastStatus = current.status;
|
|
1212
|
+
}
|
|
1213
|
+
checkTimeout();
|
|
1214
|
+
await sleep(interval_ms, signal);
|
|
1215
|
+
checkTimeout();
|
|
1216
|
+
current = await current.refresh(signal);
|
|
1217
|
+
}
|
|
1218
|
+
if (current.status !== lastStatus) on_status_change?.(current.status, current.toJSON());
|
|
1219
|
+
return current;
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
/**
|
|
1223
|
+
* Result set for transcription listing.
|
|
1224
|
+
*/
|
|
1225
|
+
var TranscriptionListResult = class {
|
|
1226
|
+
/**
|
|
1227
|
+
* Transcriptions from the first page of results.
|
|
1228
|
+
*/
|
|
1229
|
+
transcriptions;
|
|
1230
|
+
/**
|
|
1231
|
+
* Pagination cursor for the next page. Null if no more pages.
|
|
1232
|
+
*/
|
|
1233
|
+
next_page_cursor;
|
|
1234
|
+
constructor(initialResponse, _http, _options) {
|
|
1235
|
+
this._http = _http;
|
|
1236
|
+
this._options = _options;
|
|
1237
|
+
this.transcriptions = initialResponse.transcriptions.map((data) => new SonioxTranscription(data, _http));
|
|
1238
|
+
this.next_page_cursor = initialResponse.next_page_cursor;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Returns the raw data for this list result
|
|
1242
|
+
*/
|
|
1243
|
+
toJSON() {
|
|
1244
|
+
return {
|
|
1245
|
+
transcriptions: this.transcriptions.map((t) => t.toJSON()),
|
|
1246
|
+
next_page_cursor: this.next_page_cursor
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Returns true if there are more pages of results beyond the first page.
|
|
1251
|
+
*/
|
|
1252
|
+
isPaged() {
|
|
1253
|
+
return this.next_page_cursor !== null;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Async iterator that automatically fetches all pages.
|
|
1257
|
+
* Use with `for await...of` to iterate through all transcriptions.
|
|
1258
|
+
*/
|
|
1259
|
+
async *[Symbol.asyncIterator]() {
|
|
1260
|
+
for (const transcription of this.transcriptions) yield transcription;
|
|
1261
|
+
let cursor = this.next_page_cursor;
|
|
1262
|
+
while (cursor !== null) {
|
|
1263
|
+
const response = await this._http.request({
|
|
1264
|
+
method: "GET",
|
|
1265
|
+
path: "/v1/transcriptions",
|
|
1266
|
+
query: {
|
|
1267
|
+
limit: this._options.limit,
|
|
1268
|
+
cursor
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
for (const data of response.data.transcriptions) yield new SonioxTranscription(data, this._http);
|
|
1272
|
+
cursor = response.data.next_page_cursor;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
var SonioxSttApi = class {
|
|
1277
|
+
constructor(http, filesApi) {
|
|
1278
|
+
this.http = http;
|
|
1279
|
+
this.filesApi = filesApi;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Creates a new transcription from audio_url or file_id
|
|
1283
|
+
*
|
|
1284
|
+
* @param options - Transcription options including model and audio source.
|
|
1285
|
+
* @returns The created transcription.
|
|
1286
|
+
* @throws {SonioxHttpError} On API errors.
|
|
1287
|
+
*
|
|
1288
|
+
* @example
|
|
1289
|
+
* ```typescript
|
|
1290
|
+
* // Transcribe from URL
|
|
1291
|
+
* const transcription = await client.stt.create({
|
|
1292
|
+
* model: 'stt-async-v4',
|
|
1293
|
+
* audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
|
|
1294
|
+
* });
|
|
1295
|
+
*
|
|
1296
|
+
* // Transcribe from uploaded file
|
|
1297
|
+
* const file = await client.files.upload(buffer);
|
|
1298
|
+
* const transcription = await client.stt.create({
|
|
1299
|
+
* model: 'stt-async-v4',
|
|
1300
|
+
* file_id: file.id,
|
|
1301
|
+
* });
|
|
1302
|
+
*
|
|
1303
|
+
* // With speaker diarization
|
|
1304
|
+
* const transcription = await client.stt.create({
|
|
1305
|
+
* model: 'stt-async-v4',
|
|
1306
|
+
* audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
|
|
1307
|
+
* enable_speaker_diarization: true,
|
|
1308
|
+
* });
|
|
1309
|
+
* ```
|
|
1310
|
+
*/
|
|
1311
|
+
async create(options, signal) {
|
|
1312
|
+
if (options.client_reference_id !== void 0 && options.client_reference_id.length > 256) throw new Error(`client_reference_id exceeds maximum length of 256 characters (got ${options.client_reference_id.length})`);
|
|
1313
|
+
return new SonioxTranscription((await this.http.request({
|
|
1314
|
+
method: "POST",
|
|
1315
|
+
path: "/v1/transcriptions",
|
|
1316
|
+
body: options,
|
|
1317
|
+
...signal && { signal }
|
|
1318
|
+
})).data, this.http);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Retrieves list of transcriptions
|
|
1322
|
+
*
|
|
1323
|
+
* The returned result is async iterable - use `for await...of` to iterate through all pages
|
|
1324
|
+
*
|
|
1325
|
+
* @param options - Optional pagination and filter parameters.
|
|
1326
|
+
* @returns TranscriptionListResult with async iteration support.
|
|
1327
|
+
* @throws {SonioxHttpError} On API errors.
|
|
1328
|
+
*
|
|
1329
|
+
* @example
|
|
1330
|
+
* ```typescript
|
|
1331
|
+
* const result = await client.stt.list();
|
|
1332
|
+
*
|
|
1333
|
+
* // Automatic paging - iterates through ALL transcriptions across all pages
|
|
1334
|
+
* for await (const transcription of result) {
|
|
1335
|
+
* console.log(transcription.id, transcription.status);
|
|
1336
|
+
* }
|
|
1337
|
+
*
|
|
1338
|
+
* // Or access just the first page
|
|
1339
|
+
* for (const transcription of result.transcriptions) {
|
|
1340
|
+
* console.log(transcription.id);
|
|
1341
|
+
* }
|
|
1342
|
+
*
|
|
1343
|
+
* // Check if there are more pages
|
|
1344
|
+
* if (result.isPaged()) {
|
|
1345
|
+
* console.log('More pages available');
|
|
1346
|
+
* }
|
|
1347
|
+
* ```
|
|
1348
|
+
*/
|
|
1349
|
+
async list(options = {}) {
|
|
1350
|
+
return new TranscriptionListResult((await this.http.request({
|
|
1351
|
+
method: "GET",
|
|
1352
|
+
path: "/v1/transcriptions",
|
|
1353
|
+
query: {
|
|
1354
|
+
limit: options.limit,
|
|
1355
|
+
cursor: options.cursor
|
|
1356
|
+
}
|
|
1357
|
+
})).data, this.http, options);
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Retrieves a transcription by ID
|
|
1361
|
+
*
|
|
1362
|
+
* @param id - The UUID of the transcription or a SonioxTranscription instance.
|
|
1363
|
+
* @returns The transcription, or null if not found.
|
|
1364
|
+
* @throws {SonioxHttpError} On API errors (except 404).
|
|
1365
|
+
*
|
|
1366
|
+
* @example
|
|
1367
|
+
* ```typescript
|
|
1368
|
+
* const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
|
|
1369
|
+
* if (transcription) {
|
|
1370
|
+
* console.log(transcription.status, transcription.model);
|
|
1371
|
+
* }
|
|
1372
|
+
* ```
|
|
1373
|
+
*/
|
|
1374
|
+
async get(id) {
|
|
1375
|
+
const transcription_id = getTranscriptionId(id);
|
|
1376
|
+
try {
|
|
1377
|
+
return new SonioxTranscription((await this.http.request({
|
|
1378
|
+
method: "GET",
|
|
1379
|
+
path: `/v1/transcriptions/${transcription_id}`
|
|
1380
|
+
})).data, this.http);
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
if (isNotFoundError(error)) return null;
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Permanently deletes a transcription.
|
|
1388
|
+
* This operation is idempotent - succeeds even if the transcription doesn't exist.
|
|
1389
|
+
*
|
|
1390
|
+
* @param id - The UUID of the transcription or a SonioxTranscription instance
|
|
1391
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* ```typescript
|
|
1395
|
+
* // Delete by ID
|
|
1396
|
+
* await client.stt.delete('550e8400-e29b-41d4-a716-446655440000');
|
|
1397
|
+
*
|
|
1398
|
+
* // Or delete a transcription instance
|
|
1399
|
+
* const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
|
|
1400
|
+
* if (transcription) {
|
|
1401
|
+
* await client.stt.delete(transcription);
|
|
1402
|
+
* }
|
|
1403
|
+
* ```
|
|
1404
|
+
*/
|
|
1405
|
+
async delete(id) {
|
|
1406
|
+
const transcription_id = getTranscriptionId(id);
|
|
1407
|
+
try {
|
|
1408
|
+
await this.http.request({
|
|
1409
|
+
method: "DELETE",
|
|
1410
|
+
path: `/v1/transcriptions/${transcription_id}`
|
|
1411
|
+
});
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
if (!isNotFoundError(error)) throw error;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Permanently deletes a transcription and its associated file (if any).
|
|
1418
|
+
* This operation is idempotent - succeeds even if resources don't exist.
|
|
1419
|
+
*
|
|
1420
|
+
* @param id - The UUID of the transcription or a SonioxTranscription instance
|
|
1421
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
1422
|
+
*
|
|
1423
|
+
* @example
|
|
1424
|
+
* ```typescript
|
|
1425
|
+
* // Clean up both transcription and uploaded file
|
|
1426
|
+
* const transcription = await client.stt.transcribe({
|
|
1427
|
+
* model: 'stt-async-v4',
|
|
1428
|
+
* file: buffer,
|
|
1429
|
+
* wait: true,
|
|
1430
|
+
* });
|
|
1431
|
+
* // ... use transcription ...
|
|
1432
|
+
* await client.stt.destroy(transcription); // Deletes both
|
|
1433
|
+
*
|
|
1434
|
+
* // Or by ID
|
|
1435
|
+
* await client.stt.destroy('550e8400-e29b-41d4-a716-446655440000');
|
|
1436
|
+
* ```
|
|
1437
|
+
*/
|
|
1438
|
+
async destroy(id) {
|
|
1439
|
+
const transcription = await this.get(id);
|
|
1440
|
+
if (!transcription) return;
|
|
1441
|
+
await this.delete(transcription);
|
|
1442
|
+
if (transcription.file_id) try {
|
|
1443
|
+
await this.filesApi.delete(transcription.file_id);
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
if (!isNotFoundError(error)) throw error;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Retrieves the full transcript text and tokens for a completed transcription.
|
|
1450
|
+
* Only available for successfully completed transcriptions.
|
|
1451
|
+
*
|
|
1452
|
+
* @param id - The UUID of the transcription or a SonioxTranscription instance
|
|
1453
|
+
* @returns The transcript with text and detailed tokens, or null if not found
|
|
1454
|
+
* @throws {SonioxHttpError} On API errors (except 404)
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* ```typescript
|
|
1458
|
+
* const transcript = await client.stt.getTranscript('550e8400-e29b-41d4-a716-446655440000');
|
|
1459
|
+
* if (transcript) {
|
|
1460
|
+
* console.log(transcript.text);
|
|
1461
|
+
* for (const token of transcript.tokens) {
|
|
1462
|
+
* console.log(token.text, token.start_ms, token.end_ms, token.confidence);
|
|
1463
|
+
* }
|
|
1464
|
+
* }
|
|
1465
|
+
* ```
|
|
1466
|
+
*/
|
|
1467
|
+
async getTranscript(id, signal) {
|
|
1468
|
+
const transcription_id = getTranscriptionId(id);
|
|
1469
|
+
try {
|
|
1470
|
+
return new SonioxTranscript((await this.http.request({
|
|
1471
|
+
method: "GET",
|
|
1472
|
+
path: `/v1/transcriptions/${transcription_id}/transcript`,
|
|
1473
|
+
...signal && { signal }
|
|
1474
|
+
})).data);
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
if (isNotFoundError(error)) return null;
|
|
1477
|
+
throw error;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Waits for a transcription to complete
|
|
1482
|
+
*
|
|
1483
|
+
* @param id - The UUID of the transcription or a SonioxTranscription instance.
|
|
1484
|
+
* @param options - Wait options including polling interval, timeout, and callbacks.
|
|
1485
|
+
* @returns The completed or errored transcription.
|
|
1486
|
+
* @throws {Error} If the wait times out or is aborted.
|
|
1487
|
+
* @throws {SonioxHttpError} On API errors.
|
|
1488
|
+
*
|
|
1489
|
+
* @example
|
|
1490
|
+
* ```typescript
|
|
1491
|
+
* const completed = await client.stt.wait('550e8400-e29b-41d4-a716-446655440000');
|
|
1492
|
+
*
|
|
1493
|
+
* // With progress callback
|
|
1494
|
+
* const completed = await client.stt.wait('id', {
|
|
1495
|
+
* interval_ms: 2000,
|
|
1496
|
+
* on_status_change: (status) => console.log(`Status: ${status}`),
|
|
1497
|
+
* });
|
|
1498
|
+
* ```
|
|
1499
|
+
*/
|
|
1500
|
+
async wait(id, options) {
|
|
1501
|
+
const transcription = await this.get(id);
|
|
1502
|
+
if (!transcription) throw new Error(`Transcription not found: ${getTranscriptionId(id)}`);
|
|
1503
|
+
return transcription.wait(options);
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Wrapper to transcribe from a URL.
|
|
1507
|
+
*
|
|
1508
|
+
* @param audio_url - Publicly accessible audio URL
|
|
1509
|
+
* @param options - Transcription options (excluding audio_url)
|
|
1510
|
+
* @returns The transcription (completed if wait=true, otherwise in queued/processing state).
|
|
1511
|
+
*/
|
|
1512
|
+
async transcribeFromUrl(audio_url, options) {
|
|
1513
|
+
return this.transcribe({
|
|
1514
|
+
...options,
|
|
1515
|
+
audio_url
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Wrapper to transcribe from an uploaded file ID.
|
|
1520
|
+
*
|
|
1521
|
+
* @param file_id - ID of a previously uploaded file
|
|
1522
|
+
* @param options - Transcription options (excluding file_id)
|
|
1523
|
+
* @returns The transcription (completed if wait=true, otherwise in queued/processing state).
|
|
1524
|
+
*/
|
|
1525
|
+
async transcribeFromFileId(file_id, options) {
|
|
1526
|
+
return this.transcribe({
|
|
1527
|
+
...options,
|
|
1528
|
+
file_id
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Wrapper to transcribe from raw file data.
|
|
1533
|
+
*
|
|
1534
|
+
* @param file - Buffer, Uint8Array, Blob, or ReadableStream
|
|
1535
|
+
* @param options - Transcription options (excluding file)
|
|
1536
|
+
* @returns The transcription (completed if wait=true, otherwise in queued/processing state).
|
|
1537
|
+
*/
|
|
1538
|
+
async transcribeFromFile(file, options) {
|
|
1539
|
+
return this.transcribe({
|
|
1540
|
+
...options,
|
|
1541
|
+
file
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Unified transcribe method - supports direct file upload
|
|
1546
|
+
*
|
|
1547
|
+
* When `file` is provided, uploads it first then creates a transcription
|
|
1548
|
+
* When `wait: true`, waits for completion before returning
|
|
1549
|
+
* When `cleanup` is specified (requires `wait: true`), cleans up resources after completion or on error/timeout
|
|
1550
|
+
*
|
|
1551
|
+
* @param options - Transcribe options including model, audio source, and wait settings.
|
|
1552
|
+
* @returns The transcription (completed if wait=true, otherwise in queued/processing state).
|
|
1553
|
+
* @throws {SonioxHttpError} On API errors.
|
|
1554
|
+
* @throws {Error} On validation errors or wait timeout.
|
|
1555
|
+
*
|
|
1556
|
+
* @example
|
|
1557
|
+
* ```typescript
|
|
1558
|
+
* // Transcribe from URL and wait for completion
|
|
1559
|
+
* const result = await client.stt.transcribe({
|
|
1560
|
+
* model: 'stt-async-v4',
|
|
1561
|
+
* audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
|
|
1562
|
+
* wait: true,
|
|
1563
|
+
* });
|
|
1564
|
+
*
|
|
1565
|
+
* // Upload file and transcribe in one call
|
|
1566
|
+
* const result = await client.stt.transcribe({
|
|
1567
|
+
* model: 'stt-async-v4',
|
|
1568
|
+
* file: buffer, // or Blob, ReadableStream
|
|
1569
|
+
* filename: 'meeting.mp3',
|
|
1570
|
+
* enable_speaker_diarization: true,
|
|
1571
|
+
* wait: true,
|
|
1572
|
+
* });
|
|
1573
|
+
*
|
|
1574
|
+
* // With wait progress callback
|
|
1575
|
+
* const result = await client.stt.transcribe({
|
|
1576
|
+
* model: 'stt-async-v4',
|
|
1577
|
+
* file: buffer,
|
|
1578
|
+
* wait: true,
|
|
1579
|
+
* wait_options: {
|
|
1580
|
+
* interval_ms: 2000,
|
|
1581
|
+
* on_status_change: (status) => console.log(`Status: ${status}`),
|
|
1582
|
+
* },
|
|
1583
|
+
* });
|
|
1584
|
+
*
|
|
1585
|
+
* // Auto-cleanup uploaded file after transcription
|
|
1586
|
+
* const result = await client.stt.transcribe({
|
|
1587
|
+
* model: 'stt-async-v4',
|
|
1588
|
+
* file: buffer,
|
|
1589
|
+
* wait: true,
|
|
1590
|
+
* cleanup: ['file'], // Deletes uploaded file, keeps transcription record
|
|
1591
|
+
* });
|
|
1592
|
+
*
|
|
1593
|
+
* // Auto-cleanup everything after transcription
|
|
1594
|
+
* const result = await client.stt.transcribe({
|
|
1595
|
+
* model: 'stt-async-v4',
|
|
1596
|
+
* file: buffer,
|
|
1597
|
+
* wait: true,
|
|
1598
|
+
* cleanup: ['file', 'transcription'], // Deletes both file and transcription record
|
|
1599
|
+
* });
|
|
1600
|
+
* ```
|
|
1601
|
+
*/
|
|
1602
|
+
async transcribe(options) {
|
|
1603
|
+
const sourceCount = [
|
|
1604
|
+
options.file !== void 0,
|
|
1605
|
+
options.file_id !== void 0,
|
|
1606
|
+
options.audio_url !== void 0
|
|
1607
|
+
].filter(Boolean).length;
|
|
1608
|
+
if (sourceCount === 0) throw new Error("One of file, file_id, or audio_url must be provided");
|
|
1609
|
+
if (sourceCount > 1) throw new Error("Only one of file, file_id, or audio_url can be provided");
|
|
1610
|
+
if (options.audio_url !== void 0) {
|
|
1611
|
+
if (!/^https?:\/\/[^\s]+$/.test(options.audio_url)) throw new Error("audio_url must be a valid HTTP or HTTPS URL");
|
|
1612
|
+
}
|
|
1613
|
+
if (options.webhook_auth_header_name !== void 0 !== (options.webhook_auth_header_value !== void 0)) throw new Error("webhook_auth_header_name and webhook_auth_header_value must be provided together");
|
|
1614
|
+
if (options.client_reference_id !== void 0 && options.client_reference_id.length > 256) throw new Error(`client_reference_id exceeds maximum length of 256 characters (got ${options.client_reference_id.length})`);
|
|
1615
|
+
if (options.cleanup?.length && !options.wait) throw new Error("cleanup can only be used when wait=true");
|
|
1616
|
+
const { signal: combinedSignal, cleanup: cleanupSignal } = createTimeoutSignal(options.timeout_ms, options.signal);
|
|
1617
|
+
let fileIdToCleanup;
|
|
1618
|
+
let transcriptionId;
|
|
1619
|
+
const performCleanup = async (finalFileId) => {
|
|
1620
|
+
if (!options.cleanup?.length) return;
|
|
1621
|
+
const fileId = finalFileId ?? fileIdToCleanup;
|
|
1622
|
+
if (options.cleanup.includes("file") && fileId) try {
|
|
1623
|
+
await this.filesApi.delete(fileId);
|
|
1624
|
+
} catch {}
|
|
1625
|
+
if (options.cleanup.includes("transcription") && transcriptionId) try {
|
|
1626
|
+
await this.delete(transcriptionId);
|
|
1627
|
+
} catch {}
|
|
1628
|
+
};
|
|
1629
|
+
try {
|
|
1630
|
+
let file_id = options.file_id;
|
|
1631
|
+
if (options.file) {
|
|
1632
|
+
const uploaded = await this.filesApi.upload(options.file, {
|
|
1633
|
+
filename: options.filename,
|
|
1634
|
+
client_reference_id: options.client_reference_id,
|
|
1635
|
+
signal: combinedSignal
|
|
1636
|
+
});
|
|
1637
|
+
file_id = uploaded.id;
|
|
1638
|
+
fileIdToCleanup = uploaded.id;
|
|
1639
|
+
} else if (options.file_id) fileIdToCleanup = options.file_id;
|
|
1640
|
+
let webhook_url = options.webhook_url;
|
|
1641
|
+
if (webhook_url && options.webhook_query) {
|
|
1642
|
+
const url = new URL(webhook_url);
|
|
1643
|
+
(options.webhook_query instanceof URLSearchParams ? options.webhook_query : typeof options.webhook_query === "string" ? new URLSearchParams(options.webhook_query) : new URLSearchParams(options.webhook_query)).forEach((value, key) => {
|
|
1644
|
+
url.searchParams.append(key, value);
|
|
1645
|
+
});
|
|
1646
|
+
webhook_url = url.toString();
|
|
1647
|
+
}
|
|
1648
|
+
const createOptions = {
|
|
1649
|
+
model: options.model,
|
|
1650
|
+
audio_url: options.audio_url,
|
|
1651
|
+
file_id,
|
|
1652
|
+
language_hints: options.language_hints,
|
|
1653
|
+
language_hints_strict: options.language_hints_strict,
|
|
1654
|
+
enable_language_identification: options.enable_language_identification,
|
|
1655
|
+
enable_speaker_diarization: options.enable_speaker_diarization,
|
|
1656
|
+
context: options.context,
|
|
1657
|
+
translation: options.translation,
|
|
1658
|
+
webhook_url,
|
|
1659
|
+
webhook_auth_header_name: options.webhook_auth_header_name,
|
|
1660
|
+
webhook_auth_header_value: options.webhook_auth_header_value,
|
|
1661
|
+
client_reference_id: options.client_reference_id
|
|
1662
|
+
};
|
|
1663
|
+
const transcription = await this.create(createOptions, combinedSignal);
|
|
1664
|
+
transcriptionId = transcription.id;
|
|
1665
|
+
if (transcription.file_id) fileIdToCleanup = transcription.file_id;
|
|
1666
|
+
if (options.wait) {
|
|
1667
|
+
const waitOptions = {
|
|
1668
|
+
...options.wait_options,
|
|
1669
|
+
signal: combinedSignal ?? options.wait_options?.signal
|
|
1670
|
+
};
|
|
1671
|
+
const completed = await transcription.wait(waitOptions);
|
|
1672
|
+
const shouldFetchTranscript = options.fetch_transcript !== false;
|
|
1673
|
+
let transcript;
|
|
1674
|
+
if (completed.status === "completed") {
|
|
1675
|
+
if (shouldFetchTranscript) transcript = await completed.getTranscript(combinedSignal ? { signal: combinedSignal } : void 0);
|
|
1676
|
+
} else transcript = null;
|
|
1677
|
+
const result = new SonioxTranscription(completed.toJSON(), this.http, transcript);
|
|
1678
|
+
await performCleanup(completed.file_id);
|
|
1679
|
+
return result;
|
|
1680
|
+
}
|
|
1681
|
+
return transcription;
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
if (options.wait) await performCleanup();
|
|
1684
|
+
throw error;
|
|
1685
|
+
} finally {
|
|
1686
|
+
cleanupSignal();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Permanently deletes all transcriptions.
|
|
1691
|
+
* Iterates through all pages of transcriptions and deletes each one.
|
|
1692
|
+
*
|
|
1693
|
+
* @param options - Optional signal and progress callback.
|
|
1694
|
+
* @returns The number of transcriptions deleted.
|
|
1695
|
+
* @throws {SonioxHttpError} On API errors.
|
|
1696
|
+
* @throws {Error} If the operation is aborted via signal.
|
|
1697
|
+
*
|
|
1698
|
+
* @example
|
|
1699
|
+
* ```typescript
|
|
1700
|
+
* // Delete all transcriptions
|
|
1701
|
+
* const { deleted } = await client.stt.purge();
|
|
1702
|
+
* console.log(`Deleted ${deleted} transcriptions.`);
|
|
1703
|
+
*
|
|
1704
|
+
* // With progress logging
|
|
1705
|
+
* const { deleted } = await client.stt.purge({
|
|
1706
|
+
* on_progress: (transcription, index) => {
|
|
1707
|
+
* console.log(`Deleting transcription: ${transcription.id} (${index + 1})`);
|
|
1708
|
+
* },
|
|
1709
|
+
* });
|
|
1710
|
+
*
|
|
1711
|
+
* // With cancellation
|
|
1712
|
+
* const controller = new AbortController();
|
|
1713
|
+
* const { deleted } = await client.stt.purge({ signal: controller.signal });
|
|
1714
|
+
* ```
|
|
1715
|
+
*/
|
|
1716
|
+
async purge(options = {}) {
|
|
1717
|
+
const { signal, on_progress } = options;
|
|
1718
|
+
const result = await this.list();
|
|
1719
|
+
let deleted = 0;
|
|
1720
|
+
for await (const transcription of result) {
|
|
1721
|
+
signal?.throwIfAborted();
|
|
1722
|
+
on_progress?.(transcription.toJSON(), deleted);
|
|
1723
|
+
await this.delete(transcription);
|
|
1724
|
+
deleted++;
|
|
1725
|
+
}
|
|
1726
|
+
return { deleted };
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
//#endregion
|
|
1731
|
+
//#region src/async/webhooks.ts
|
|
1732
|
+
const VALID_STATUSES = ["completed", "error"];
|
|
1733
|
+
/**
|
|
1734
|
+
* Get webhook authentication configuration from environment variables.
|
|
1735
|
+
*
|
|
1736
|
+
* Reads `SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET` environment variables.
|
|
1737
|
+
* Returns undefined if either variable is not set (both are required for authentication).
|
|
1738
|
+
*
|
|
1739
|
+
* @returns WebhookAuthConfig if both env vars are set, undefined otherwise
|
|
1740
|
+
*
|
|
1741
|
+
* @example
|
|
1742
|
+
* ```typescript
|
|
1743
|
+
* // Set environment variables:
|
|
1744
|
+
* // SONIOX_API_WEBHOOK_HEADER=X-Webhook-Secret
|
|
1745
|
+
* // SONIOX_API_WEBHOOK_SECRET=my-secret-token
|
|
1746
|
+
*
|
|
1747
|
+
* const auth = getWebhookAuthFromEnv();
|
|
1748
|
+
* // Returns: { name: 'X-Webhook-Secret', value: 'my-secret-token' }
|
|
1749
|
+
* ```
|
|
1750
|
+
*/
|
|
1751
|
+
function getWebhookAuthFromEnv() {
|
|
1752
|
+
const headerName = process.env[SONIOX_API_WEBHOOK_HEADER_ENV];
|
|
1753
|
+
const headerValue = process.env[SONIOX_API_WEBHOOK_SECRET_ENV];
|
|
1754
|
+
if (headerName && headerValue) return {
|
|
1755
|
+
name: headerName,
|
|
1756
|
+
value: headerValue
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Resolve webhook authentication configuration.
|
|
1761
|
+
*
|
|
1762
|
+
* If explicit auth is provided, it is used. Otherwise, attempts to read from
|
|
1763
|
+
* environment variables (`SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET`).
|
|
1764
|
+
*
|
|
1765
|
+
* @param auth - Explicit authentication configuration (takes precedence)
|
|
1766
|
+
* @returns Resolved WebhookAuthConfig or undefined if not configured
|
|
1767
|
+
*/
|
|
1768
|
+
function resolveWebhookAuth(auth) {
|
|
1769
|
+
return auth ?? getWebhookAuthFromEnv();
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Type guard to check if a value is a valid WebhookEvent
|
|
1773
|
+
*
|
|
1774
|
+
* @param payload - Value to check
|
|
1775
|
+
* @returns True if payload is a valid WebhookEvent
|
|
1776
|
+
*
|
|
1777
|
+
* @example
|
|
1778
|
+
* ```typescript
|
|
1779
|
+
* if (isWebhookEvent(body)) {
|
|
1780
|
+
* console.log(body.id, body.status);
|
|
1781
|
+
* }
|
|
1782
|
+
* ```
|
|
1783
|
+
*/
|
|
1784
|
+
function isWebhookEvent(payload) {
|
|
1785
|
+
if (typeof payload !== "object" || payload === null) return false;
|
|
1786
|
+
const obj = payload;
|
|
1787
|
+
if (typeof obj.id !== "string" || obj.id.length === 0) return false;
|
|
1788
|
+
if (!VALID_STATUSES.includes(obj.status)) return false;
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Parse and validate a webhook event payload
|
|
1793
|
+
*
|
|
1794
|
+
* @param payload - Raw payload to parse (object or JSON string)
|
|
1795
|
+
* @returns Validated WebhookEvent
|
|
1796
|
+
* @throws Error with descriptive message if payload is invalid
|
|
1797
|
+
*
|
|
1798
|
+
* @example
|
|
1799
|
+
* ```typescript
|
|
1800
|
+
* try {
|
|
1801
|
+
* const event = parseWebhookEvent(req.body);
|
|
1802
|
+
* console.log(event.id, event.status);
|
|
1803
|
+
* } catch (error) {
|
|
1804
|
+
* console.error('Invalid webhook payload:', error.message);
|
|
1805
|
+
* }
|
|
1806
|
+
* ```
|
|
1807
|
+
*/
|
|
1808
|
+
function parseWebhookEvent(payload) {
|
|
1809
|
+
if (typeof payload === "string") try {
|
|
1810
|
+
payload = JSON.parse(payload);
|
|
1811
|
+
} catch {
|
|
1812
|
+
throw new Error("Invalid webhook payload: not valid JSON");
|
|
1813
|
+
}
|
|
1814
|
+
if (typeof payload !== "object" || payload === null) throw new Error("Invalid webhook payload: expected an object");
|
|
1815
|
+
const obj = payload;
|
|
1816
|
+
if (typeof obj.id !== "string") throw new Error("Invalid webhook payload: missing or invalid \"id\" field");
|
|
1817
|
+
if (obj.id.length === 0) throw new Error("Invalid webhook payload: \"id\" field cannot be empty");
|
|
1818
|
+
if (!VALID_STATUSES.includes(obj.status)) throw new Error(`Invalid webhook payload: "status" must be "completed" or "error", got "${String(obj.status)}"`);
|
|
1819
|
+
return {
|
|
1820
|
+
id: obj.id,
|
|
1821
|
+
status: obj.status
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Get a header value from various header formats (case-insensitive)
|
|
1826
|
+
*/
|
|
1827
|
+
function getHeaderValue(headers, name) {
|
|
1828
|
+
const lowerName = name.toLowerCase();
|
|
1829
|
+
if (headers instanceof Headers) return headers.get(lowerName);
|
|
1830
|
+
if (typeof headers.get === "function") return headers.get(lowerName);
|
|
1831
|
+
const record = headers;
|
|
1832
|
+
if (lowerName in record) {
|
|
1833
|
+
const value = record[lowerName];
|
|
1834
|
+
return Array.isArray(value) ? value[0] ?? null : value ?? null;
|
|
1835
|
+
}
|
|
1836
|
+
for (const key of Object.keys(record)) if (key.toLowerCase() === lowerName) {
|
|
1837
|
+
const value = record[key];
|
|
1838
|
+
return Array.isArray(value) ? value[0] ?? null : value ?? null;
|
|
1839
|
+
}
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Verify webhook authentication header
|
|
1844
|
+
*
|
|
1845
|
+
* @param headers - Request headers
|
|
1846
|
+
* @param auth - Authentication configuration with expected header name and value
|
|
1847
|
+
* @returns True if authentication passes, false otherwise
|
|
1848
|
+
*
|
|
1849
|
+
* @example
|
|
1850
|
+
* ```typescript
|
|
1851
|
+
* const isValid = verifyWebhookAuth(req.headers, {
|
|
1852
|
+
* name: 'X-Webhook-Secret',
|
|
1853
|
+
* value: process.env.WEBHOOK_SECRET,
|
|
1854
|
+
* });
|
|
1855
|
+
*
|
|
1856
|
+
* if (!isValid) {
|
|
1857
|
+
* return res.status(401).send('Unauthorized');
|
|
1858
|
+
* }
|
|
1859
|
+
* ```
|
|
1860
|
+
*/
|
|
1861
|
+
function verifyWebhookAuth(headers, auth) {
|
|
1862
|
+
return getHeaderValue(headers, auth.name) === auth.value;
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Framework-agnostic webhook handler
|
|
1866
|
+
*
|
|
1867
|
+
* Validates the HTTP method, authentication (if configured), and parses the webhook payload.
|
|
1868
|
+
* Returns a result object that can be used to construct an HTTP response.
|
|
1869
|
+
*
|
|
1870
|
+
* Authentication is resolved in this order:
|
|
1871
|
+
* 1. Explicit `auth` option if provided
|
|
1872
|
+
* 2. Environment variables `SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET`
|
|
1873
|
+
* 3. No authentication if neither is configured
|
|
1874
|
+
*
|
|
1875
|
+
* @param options - Handler options including method, headers, body, and optional auth
|
|
1876
|
+
* @returns Result with ok status, HTTP status code, and either event or error
|
|
1877
|
+
*
|
|
1878
|
+
* @example
|
|
1879
|
+
* ```typescript
|
|
1880
|
+
* // Option 1: Set environment variables (recommended)
|
|
1881
|
+
* // SONIOX_API_WEBHOOK_HEADER=X-Webhook-Secret
|
|
1882
|
+
* // SONIOX_API_WEBHOOK_SECRET=my-secret
|
|
1883
|
+
* const result = handleWebhook({
|
|
1884
|
+
* method: req.method,
|
|
1885
|
+
* headers: req.headers,
|
|
1886
|
+
* body: req.body,
|
|
1887
|
+
* });
|
|
1888
|
+
*
|
|
1889
|
+
* // Option 2: Explicit auth config (overrides env vars)
|
|
1890
|
+
* const result = handleWebhook({
|
|
1891
|
+
* method: req.method,
|
|
1892
|
+
* headers: req.headers,
|
|
1893
|
+
* body: req.body,
|
|
1894
|
+
* auth: {
|
|
1895
|
+
* name: 'X-Webhook-Secret',
|
|
1896
|
+
* value: process.env.WEBHOOK_SECRET,
|
|
1897
|
+
* },
|
|
1898
|
+
* });
|
|
1899
|
+
*
|
|
1900
|
+
* if (result.ok) {
|
|
1901
|
+
* console.log('Transcription completed:', result.event.id);
|
|
1902
|
+
* }
|
|
1903
|
+
*
|
|
1904
|
+
* res.status(result.status).json(result.ok ? { received: true } : { error: result.error });
|
|
1905
|
+
* ```
|
|
1906
|
+
*/
|
|
1907
|
+
function handleWebhook(options) {
|
|
1908
|
+
const { method, headers, body, auth } = options;
|
|
1909
|
+
if (method.toUpperCase() !== "POST") return {
|
|
1910
|
+
ok: false,
|
|
1911
|
+
status: 405,
|
|
1912
|
+
error: "Method not allowed"
|
|
1913
|
+
};
|
|
1914
|
+
const resolvedAuth = resolveWebhookAuth(auth);
|
|
1915
|
+
if (resolvedAuth) {
|
|
1916
|
+
if (!verifyWebhookAuth(headers, resolvedAuth)) return {
|
|
1917
|
+
ok: false,
|
|
1918
|
+
status: 401,
|
|
1919
|
+
error: "Unauthorized"
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
try {
|
|
1923
|
+
return {
|
|
1924
|
+
ok: true,
|
|
1925
|
+
status: 200,
|
|
1926
|
+
event: parseWebhookEvent(body)
|
|
1927
|
+
};
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
return {
|
|
1930
|
+
ok: false,
|
|
1931
|
+
status: 400,
|
|
1932
|
+
error: error instanceof Error ? error.message : "Invalid webhook payload"
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Handle a webhook from a Fetch API Request (Bun/Deno/Node 18+)
|
|
1938
|
+
*
|
|
1939
|
+
* @param request - Fetch API Request object
|
|
1940
|
+
* @param auth - Optional authentication configuration
|
|
1941
|
+
* @returns Result with ok status, HTTP status code, and either event or error
|
|
1942
|
+
*
|
|
1943
|
+
* @example
|
|
1944
|
+
* ```typescript
|
|
1945
|
+
* // Bun.serve handler
|
|
1946
|
+
* Bun.serve({
|
|
1947
|
+
* async fetch(req) {
|
|
1948
|
+
* if (new URL(req.url).pathname === '/webhook') {
|
|
1949
|
+
* const result = await handleWebhookRequest(req);
|
|
1950
|
+
*
|
|
1951
|
+
* if (result.ok) {
|
|
1952
|
+
* console.log('Received webhook:', result.event.id);
|
|
1953
|
+
* }
|
|
1954
|
+
*
|
|
1955
|
+
* return new Response(
|
|
1956
|
+
* JSON.stringify(result.ok ? { received: true } : { error: result.error }),
|
|
1957
|
+
* { status: result.status, headers: { 'Content-Type': 'application/json' } }
|
|
1958
|
+
* );
|
|
1959
|
+
* }
|
|
1960
|
+
* },
|
|
1961
|
+
* });
|
|
1962
|
+
* ```
|
|
1963
|
+
*/
|
|
1964
|
+
async function handleWebhookRequest(request, auth) {
|
|
1965
|
+
if (request.method.toUpperCase() !== "POST") return {
|
|
1966
|
+
ok: false,
|
|
1967
|
+
status: 405,
|
|
1968
|
+
error: "Method not allowed"
|
|
1969
|
+
};
|
|
1970
|
+
let body;
|
|
1971
|
+
try {
|
|
1972
|
+
body = await request.json();
|
|
1973
|
+
} catch {
|
|
1974
|
+
return {
|
|
1975
|
+
ok: false,
|
|
1976
|
+
status: 400,
|
|
1977
|
+
error: "Invalid webhook payload: not valid JSON"
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
const options = {
|
|
1981
|
+
method: request.method,
|
|
1982
|
+
headers: request.headers,
|
|
1983
|
+
body
|
|
1984
|
+
};
|
|
1985
|
+
if (auth) options.auth = auth;
|
|
1986
|
+
return handleWebhook(options);
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Handle a webhook from an Express-like request
|
|
1990
|
+
*
|
|
1991
|
+
* @param req - Express request object
|
|
1992
|
+
* @param auth - Optional authentication configuration
|
|
1993
|
+
* @returns Result with ok status, HTTP status code, and either event or error
|
|
1994
|
+
*
|
|
1995
|
+
* @example
|
|
1996
|
+
* ```typescript
|
|
1997
|
+
* import express from 'express';
|
|
1998
|
+
* import { handleWebhookExpress } from '@soniox/node';
|
|
1999
|
+
*
|
|
2000
|
+
* const app = express();
|
|
2001
|
+
* app.use(express.json());
|
|
2002
|
+
*
|
|
2003
|
+
* app.post('/webhook', (req, res) => {
|
|
2004
|
+
* const result = handleWebhookExpress(req);
|
|
2005
|
+
*
|
|
2006
|
+
* if (result.ok) {
|
|
2007
|
+
* console.log('Received webhook:', result.event.id);
|
|
2008
|
+
* }
|
|
2009
|
+
*
|
|
2010
|
+
* res.status(result.status).json(
|
|
2011
|
+
* result.ok ? { received: true } : { error: result.error }
|
|
2012
|
+
* );
|
|
2013
|
+
* });
|
|
2014
|
+
* ```
|
|
2015
|
+
*/
|
|
2016
|
+
function handleWebhookExpress(req, auth) {
|
|
2017
|
+
const options = {
|
|
2018
|
+
method: req.method,
|
|
2019
|
+
headers: req.headers,
|
|
2020
|
+
body: req.body
|
|
2021
|
+
};
|
|
2022
|
+
if (auth) options.auth = auth;
|
|
2023
|
+
return handleWebhook(options);
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Handle a webhook from a Fastify request
|
|
2027
|
+
*
|
|
2028
|
+
* @param req - Fastify request object
|
|
2029
|
+
* @param auth - Optional authentication configuration
|
|
2030
|
+
* @returns Result with ok status, HTTP status code, and either event or error
|
|
2031
|
+
*
|
|
2032
|
+
* @example
|
|
2033
|
+
* ```typescript
|
|
2034
|
+
* import Fastify from 'fastify';
|
|
2035
|
+
* import { handleWebhookFastify } from '@soniox/node';
|
|
2036
|
+
*
|
|
2037
|
+
* const fastify = Fastify();
|
|
2038
|
+
*
|
|
2039
|
+
* fastify.post('/webhook', async (req, reply) => {
|
|
2040
|
+
* const result = handleWebhookFastify(req);
|
|
2041
|
+
*
|
|
2042
|
+
* if (result.ok) {
|
|
2043
|
+
* console.log('Received webhook:', result.event.id);
|
|
2044
|
+
* }
|
|
2045
|
+
*
|
|
2046
|
+
* return reply.status(result.status).send(
|
|
2047
|
+
* result.ok ? { received: true } : { error: result.error }
|
|
2048
|
+
* );
|
|
2049
|
+
* });
|
|
2050
|
+
* ```
|
|
2051
|
+
*/
|
|
2052
|
+
function handleWebhookFastify(req, auth) {
|
|
2053
|
+
const options = {
|
|
2054
|
+
method: req.method,
|
|
2055
|
+
headers: req.headers,
|
|
2056
|
+
body: req.body
|
|
2057
|
+
};
|
|
2058
|
+
if (auth) options.auth = auth;
|
|
2059
|
+
return handleWebhook(options);
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Handle a webhook from a NestJS request
|
|
2063
|
+
*
|
|
2064
|
+
* Works with NestJS using either Express or Fastify adapter.
|
|
2065
|
+
*
|
|
2066
|
+
* @param req - NestJS request object (injected via @Req() decorator)
|
|
2067
|
+
* @param auth - Optional authentication configuration (overrides env vars)
|
|
2068
|
+
* @returns Result with ok status, HTTP status code, and either event or error
|
|
2069
|
+
*
|
|
2070
|
+
* @example
|
|
2071
|
+
* ```typescript
|
|
2072
|
+
* import { Controller, Post, Req, Res, HttpStatus } from '@nestjs/common';
|
|
2073
|
+
* import { Request, Response } from 'express';
|
|
2074
|
+
* import { handleWebhookNestJS } from '@soniox/node';
|
|
2075
|
+
*
|
|
2076
|
+
* @Controller('webhook')
|
|
2077
|
+
* export class WebhookController {
|
|
2078
|
+
* @Post()
|
|
2079
|
+
* handleWebhook(@Req() req: Request, @Res() res: Response) {
|
|
2080
|
+
* const result = handleWebhookNestJS(req);
|
|
2081
|
+
*
|
|
2082
|
+
* if (result.ok) {
|
|
2083
|
+
* console.log('Received webhook:', result.event.id);
|
|
2084
|
+
* }
|
|
2085
|
+
*
|
|
2086
|
+
* return res.status(result.status).json(
|
|
2087
|
+
* result.ok ? { received: true } : { error: result.error }
|
|
2088
|
+
* );
|
|
2089
|
+
* }
|
|
2090
|
+
* }
|
|
2091
|
+
* ```
|
|
2092
|
+
*/
|
|
2093
|
+
function handleWebhookNestJS(req, auth) {
|
|
2094
|
+
const options = {
|
|
2095
|
+
method: req.method,
|
|
2096
|
+
headers: req.headers,
|
|
2097
|
+
body: req.body
|
|
2098
|
+
};
|
|
2099
|
+
if (auth) options.auth = auth;
|
|
2100
|
+
return handleWebhook(options);
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Handle a webhook from a Hono context
|
|
2104
|
+
*
|
|
2105
|
+
* @param c - Hono context object
|
|
2106
|
+
* @param auth - Optional authentication configuration
|
|
2107
|
+
* @returns Result with ok status, HTTP status code, and either event or error
|
|
2108
|
+
*
|
|
2109
|
+
* @example
|
|
2110
|
+
* ```typescript
|
|
2111
|
+
* import { Hono } from 'hono';
|
|
2112
|
+
* import { handleWebhookHono } from '@soniox/node';
|
|
2113
|
+
*
|
|
2114
|
+
* const app = new Hono();
|
|
2115
|
+
*
|
|
2116
|
+
* app.post('/webhook', async (c) => {
|
|
2117
|
+
* const result = await handleWebhookHono(c);
|
|
2118
|
+
*
|
|
2119
|
+
* if (result.ok) {
|
|
2120
|
+
* console.log('Received webhook:', result.event.id);
|
|
2121
|
+
* }
|
|
2122
|
+
*
|
|
2123
|
+
* return c.json(
|
|
2124
|
+
* result.ok ? { received: true } : { error: result.error },
|
|
2125
|
+
* result.status
|
|
2126
|
+
* );
|
|
2127
|
+
* });
|
|
2128
|
+
* ```
|
|
2129
|
+
*/
|
|
2130
|
+
async function handleWebhookHono(c, auth) {
|
|
2131
|
+
let body;
|
|
2132
|
+
try {
|
|
2133
|
+
body = await c.req.json();
|
|
2134
|
+
} catch {
|
|
2135
|
+
return {
|
|
2136
|
+
ok: false,
|
|
2137
|
+
status: 400,
|
|
2138
|
+
error: "Invalid webhook payload: not valid JSON"
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
const options = {
|
|
2142
|
+
method: c.req.method,
|
|
2143
|
+
headers: { get(name) {
|
|
2144
|
+
return c.req.header(name) ?? null;
|
|
2145
|
+
} },
|
|
2146
|
+
body
|
|
2147
|
+
};
|
|
2148
|
+
if (auth) options.auth = auth;
|
|
2149
|
+
return handleWebhook(options);
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Webhook utilities API accessible via client.webhooks
|
|
2153
|
+
*
|
|
2154
|
+
* Provides methods for handling incoming Soniox webhook requests.
|
|
2155
|
+
* When used via the client, results include lazy fetch helpers for transcripts.
|
|
2156
|
+
*/
|
|
2157
|
+
var SonioxWebhooksAPI = class {
|
|
2158
|
+
stt;
|
|
2159
|
+
/**
|
|
2160
|
+
* @internal
|
|
2161
|
+
*/
|
|
2162
|
+
constructor(stt) {
|
|
2163
|
+
this.stt = stt;
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Enhance a webhook result with fetch helpers
|
|
2167
|
+
*/
|
|
2168
|
+
withFetchHelpers(result) {
|
|
2169
|
+
const stt = this.stt;
|
|
2170
|
+
const event = result.event;
|
|
2171
|
+
if (!stt || !event) return {
|
|
2172
|
+
...result,
|
|
2173
|
+
fetchTranscript: void 0,
|
|
2174
|
+
fetchTranscription: void 0
|
|
2175
|
+
};
|
|
2176
|
+
const transcriptionId = event.id;
|
|
2177
|
+
return {
|
|
2178
|
+
...result,
|
|
2179
|
+
fetchTranscript: event.status === "completed" ? () => stt.getTranscript(transcriptionId) : void 0,
|
|
2180
|
+
fetchTranscription: () => stt.get(transcriptionId)
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Get webhook authentication configuration from environment variables.
|
|
2185
|
+
*
|
|
2186
|
+
* Reads `SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET` environment variables.
|
|
2187
|
+
* Returns undefined if either variable is not set (both are required for authentication).
|
|
2188
|
+
*/
|
|
2189
|
+
getAuthFromEnv() {
|
|
2190
|
+
return getWebhookAuthFromEnv();
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Type guard to check if a value is a valid WebhookEvent
|
|
2194
|
+
*/
|
|
2195
|
+
isEvent(payload) {
|
|
2196
|
+
return isWebhookEvent(payload);
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Parse and validate a webhook event payload
|
|
2200
|
+
*/
|
|
2201
|
+
parseEvent(payload) {
|
|
2202
|
+
return parseWebhookEvent(payload);
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Verify webhook authentication header
|
|
2206
|
+
*/
|
|
2207
|
+
verifyAuth(headers, auth) {
|
|
2208
|
+
return verifyWebhookAuth(headers, auth);
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Framework-agnostic webhook handler
|
|
2212
|
+
*/
|
|
2213
|
+
handle(options) {
|
|
2214
|
+
return this.withFetchHelpers(handleWebhook(options));
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Handle a webhook from a Fetch API Request
|
|
2218
|
+
*/
|
|
2219
|
+
async handleRequest(request, auth) {
|
|
2220
|
+
const result = await handleWebhookRequest(request, auth);
|
|
2221
|
+
return this.withFetchHelpers(result);
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Handle a webhook from an Express-like request
|
|
2225
|
+
*
|
|
2226
|
+
* @example
|
|
2227
|
+
* ```typescript
|
|
2228
|
+
* app.post('/webhook', async (req, res) => {
|
|
2229
|
+
* const result = soniox.webhooks.handleExpress(req);
|
|
2230
|
+
*
|
|
2231
|
+
* if (result.ok && result.event.status === 'completed') {
|
|
2232
|
+
* const transcript = await result.fetchTranscript();
|
|
2233
|
+
* console.log(transcript?.text);
|
|
2234
|
+
* }
|
|
2235
|
+
*
|
|
2236
|
+
* res.status(result.status).json({ received: true });
|
|
2237
|
+
* });
|
|
2238
|
+
* ```
|
|
2239
|
+
*/
|
|
2240
|
+
handleExpress(req, auth) {
|
|
2241
|
+
return this.withFetchHelpers(handleWebhookExpress(req, auth));
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Handle a webhook from a Fastify request
|
|
2245
|
+
*/
|
|
2246
|
+
handleFastify(req, auth) {
|
|
2247
|
+
return this.withFetchHelpers(handleWebhookFastify(req, auth));
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Handle a webhook from a NestJS request
|
|
2251
|
+
*/
|
|
2252
|
+
handleNestJS(req, auth) {
|
|
2253
|
+
return this.withFetchHelpers(handleWebhookNestJS(req, auth));
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Handle a webhook from a Hono context
|
|
2257
|
+
*/
|
|
2258
|
+
async handleHono(c, auth) {
|
|
2259
|
+
const result = await handleWebhookHono(c, auth);
|
|
2260
|
+
return this.withFetchHelpers(result);
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
//#endregion
|
|
2265
|
+
//#region src/http/url.ts
|
|
2266
|
+
/**
|
|
2267
|
+
* Builds a complete URL from base URL, path, and query parameters
|
|
2268
|
+
*/
|
|
2269
|
+
function buildUrl(baseUrl, path, query) {
|
|
2270
|
+
let url = joinUrl(baseUrl ?? "", path);
|
|
2271
|
+
if (query) {
|
|
2272
|
+
const params = new URLSearchParams();
|
|
2273
|
+
for (const [key, value] of Object.entries(query)) if (value !== void 0) params.append(key, String(value));
|
|
2274
|
+
const queryString = params.toString();
|
|
2275
|
+
if (queryString) url += (url.includes("?") ? "&" : "?") + queryString;
|
|
2276
|
+
}
|
|
2277
|
+
return url;
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Joins a base URL with a path
|
|
2281
|
+
*/
|
|
2282
|
+
function joinUrl(baseUrl, path) {
|
|
2283
|
+
if (!baseUrl) return path;
|
|
2284
|
+
if (!path) return baseUrl;
|
|
2285
|
+
if (/^https?:\/\//i.test(path)) return path;
|
|
2286
|
+
return (baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl) + (path.startsWith("/") ? path : `/${path}`);
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Normalizes fetch Headers to a plain object with lowercase keys
|
|
2290
|
+
*/
|
|
2291
|
+
function normalizeHeaders(headers) {
|
|
2292
|
+
const result = {};
|
|
2293
|
+
headers.forEach((value, key) => {
|
|
2294
|
+
result[key.toLowerCase()] = value;
|
|
2295
|
+
});
|
|
2296
|
+
return result;
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Merges header objects
|
|
2300
|
+
*/
|
|
2301
|
+
function mergeHeaders(...headerObjects) {
|
|
2302
|
+
const result = {};
|
|
2303
|
+
for (const headers of headerObjects) if (headers) for (const [key, value] of Object.entries(headers)) result[key.toLowerCase()] = value;
|
|
2304
|
+
return result;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
//#endregion
|
|
2308
|
+
//#region src/http/fetch-adapter.ts
|
|
2309
|
+
/** Default timeout in milliseconds (30 seconds) TODO: Move to constants? */
|
|
2310
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
2311
|
+
/**
|
|
2312
|
+
* Determines the Content-Type header based on the body type.
|
|
2313
|
+
*/
|
|
2314
|
+
function getContentTypeForBody(body) {
|
|
2315
|
+
if (body === null || body === void 0) return;
|
|
2316
|
+
if (typeof body === "string") return "text/plain; charset=utf-8";
|
|
2317
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return;
|
|
2318
|
+
if (body instanceof ArrayBuffer || body instanceof Uint8Array) return "application/octet-stream";
|
|
2319
|
+
return "application/json";
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Prepares the request body for fetch.
|
|
2323
|
+
*/
|
|
2324
|
+
function prepareBody(body) {
|
|
2325
|
+
if (body === null || body === void 0) return;
|
|
2326
|
+
if (typeof body === "string") return body;
|
|
2327
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return body;
|
|
2328
|
+
if (body instanceof ArrayBuffer) return body;
|
|
2329
|
+
if (body instanceof Uint8Array) return body;
|
|
2330
|
+
return JSON.stringify(body);
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Parses the response based on the expected type.
|
|
2334
|
+
*/
|
|
2335
|
+
async function parseResponse(response, responseType, url, method) {
|
|
2336
|
+
const contentLength = response.headers.get("content-length");
|
|
2337
|
+
if (response.status === 204 || contentLength === "0") switch (responseType) {
|
|
2338
|
+
case "arrayBuffer": return /* @__PURE__ */ new ArrayBuffer(0);
|
|
2339
|
+
case "text": return "";
|
|
2340
|
+
case "json":
|
|
2341
|
+
default: return null;
|
|
2342
|
+
}
|
|
2343
|
+
switch (responseType) {
|
|
2344
|
+
case "text": return await response.text();
|
|
2345
|
+
case "arrayBuffer": return await response.arrayBuffer();
|
|
2346
|
+
case "json":
|
|
2347
|
+
default: {
|
|
2348
|
+
const text = await response.text();
|
|
2349
|
+
if (!text) return null;
|
|
2350
|
+
try {
|
|
2351
|
+
return JSON.parse(text);
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
throw createParseError(url, method, text, error);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Default fetch-based HTTP client.
|
|
2360
|
+
*
|
|
2361
|
+
* @example Basic usage
|
|
2362
|
+
* ```typescript
|
|
2363
|
+
* const client = new FetchHttpClient({
|
|
2364
|
+
* base_url: 'https://api.example.com/v1',
|
|
2365
|
+
* default_headers: {
|
|
2366
|
+
* 'Authorization': 'Bearer token',
|
|
2367
|
+
* },
|
|
2368
|
+
* });
|
|
2369
|
+
*
|
|
2370
|
+
* const response = await client.request<{ users: User[] }>({
|
|
2371
|
+
* method: 'GET',
|
|
2372
|
+
* path: '/users',
|
|
2373
|
+
* query: { active: true },
|
|
2374
|
+
* });
|
|
2375
|
+
* ```
|
|
2376
|
+
*
|
|
2377
|
+
* @example With custom fetch (e.g., for testing)
|
|
2378
|
+
* ```typescript
|
|
2379
|
+
* const mockFetch = vi.fn().mockResolvedValue(new Response('{}'));
|
|
2380
|
+
* const client = new FetchHttpClient({
|
|
2381
|
+
* base_url: 'https://api.example.com',
|
|
2382
|
+
* fetch: mockFetch,
|
|
2383
|
+
* });
|
|
2384
|
+
* ```
|
|
2385
|
+
*
|
|
2386
|
+
* @example Sending different body types
|
|
2387
|
+
* ```typescript
|
|
2388
|
+
* // JSON body (default for objects)
|
|
2389
|
+
* await client.request({
|
|
2390
|
+
* method: 'POST',
|
|
2391
|
+
* path: '/data',
|
|
2392
|
+
* body: { name: 'test', value: 123 },
|
|
2393
|
+
* });
|
|
2394
|
+
*
|
|
2395
|
+
* // Text body
|
|
2396
|
+
* await client.request({
|
|
2397
|
+
* method: 'POST',
|
|
2398
|
+
* path: '/text',
|
|
2399
|
+
* body: 'plain text content',
|
|
2400
|
+
* headers: { 'Content-Type': 'text/plain' },
|
|
2401
|
+
* });
|
|
2402
|
+
*
|
|
2403
|
+
* // Binary body
|
|
2404
|
+
* await client.request({
|
|
2405
|
+
* method: 'POST',
|
|
2406
|
+
* path: '/upload',
|
|
2407
|
+
* body: new Uint8Array([1, 2, 3]),
|
|
2408
|
+
* });
|
|
2409
|
+
*
|
|
2410
|
+
* // FormData (for file uploads)
|
|
2411
|
+
* const formData = new FormData();
|
|
2412
|
+
* formData.append('file', fileBlob, 'document.pdf');
|
|
2413
|
+
* await client.request({
|
|
2414
|
+
* method: 'POST',
|
|
2415
|
+
* path: '/files',
|
|
2416
|
+
* body: formData,
|
|
2417
|
+
* });
|
|
2418
|
+
* ```
|
|
2419
|
+
*/
|
|
2420
|
+
var FetchHttpClient = class {
|
|
2421
|
+
baseUrl;
|
|
2422
|
+
defaultHeaders;
|
|
2423
|
+
defaultTimeoutMs;
|
|
2424
|
+
hooks;
|
|
2425
|
+
fetchImpl;
|
|
2426
|
+
constructor(options = {}) {
|
|
2427
|
+
this.baseUrl = options.base_url;
|
|
2428
|
+
this.defaultHeaders = options.default_headers ?? {};
|
|
2429
|
+
this.defaultTimeoutMs = options.default_timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
2430
|
+
this.hooks = options.hooks ?? {};
|
|
2431
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2432
|
+
if (!this.fetchImpl) throw new Error("fetch is not available. Please provide a fetch implementation via options.fetch");
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Performs an HTTP request.
|
|
2436
|
+
*
|
|
2437
|
+
* @param request - Request configuration
|
|
2438
|
+
* @returns Promise resolving to the response
|
|
2439
|
+
* @throws {SonioxHttpError}
|
|
2440
|
+
*/
|
|
2441
|
+
async request(request) {
|
|
2442
|
+
const startTime = Date.now();
|
|
2443
|
+
const url = buildUrl(this.baseUrl, request.path, request.query);
|
|
2444
|
+
const method = request.method;
|
|
2445
|
+
const responseType = request.responseType ?? "json";
|
|
2446
|
+
const contentTypeHeader = getContentTypeForBody(request.body);
|
|
2447
|
+
const headers = mergeHeaders(typeof FormData !== "undefined" && request.body instanceof FormData ? Object.fromEntries(Object.entries(this.defaultHeaders).filter(([key]) => key.toLowerCase() !== "content-type")) : this.defaultHeaders, contentTypeHeader ? { "Content-Type": contentTypeHeader } : void 0, request.headers);
|
|
2448
|
+
const requestMeta = {
|
|
2449
|
+
startTime,
|
|
2450
|
+
url,
|
|
2451
|
+
method,
|
|
2452
|
+
headers
|
|
2453
|
+
};
|
|
2454
|
+
this.hooks.onRequest?.(request, requestMeta);
|
|
2455
|
+
const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
|
|
2456
|
+
const timeoutController = new AbortController();
|
|
2457
|
+
let timeoutId;
|
|
2458
|
+
if (timeoutMs > 0) timeoutId = setTimeout(() => {
|
|
2459
|
+
timeoutController.abort(/* @__PURE__ */ new Error("Request timeout"));
|
|
2460
|
+
}, timeoutMs);
|
|
2461
|
+
const combined = request.signal ? combineAbortSignals(timeoutController.signal, request.signal) : null;
|
|
2462
|
+
const signal = combined ? combined.signal : timeoutController.signal;
|
|
2463
|
+
try {
|
|
2464
|
+
const preparedBody = prepareBody(request.body);
|
|
2465
|
+
const response = await this.fetchImpl(url, {
|
|
2466
|
+
method,
|
|
2467
|
+
headers,
|
|
2468
|
+
signal,
|
|
2469
|
+
...preparedBody !== void 0 && { body: preparedBody }
|
|
2470
|
+
});
|
|
2471
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
2472
|
+
const responseHeaders = normalizeHeaders(response.headers);
|
|
2473
|
+
if (!response.ok) {
|
|
2474
|
+
const bodyText = await response.text().catch(() => "");
|
|
2475
|
+
throw createHttpError(url, method, response.status, responseHeaders, bodyText);
|
|
2476
|
+
}
|
|
2477
|
+
const data = await parseResponse(response, responseType, url, method);
|
|
2478
|
+
const result = {
|
|
2479
|
+
status: response.status,
|
|
2480
|
+
headers: responseHeaders,
|
|
2481
|
+
data
|
|
2482
|
+
};
|
|
2483
|
+
const responseMeta = {
|
|
2484
|
+
...requestMeta,
|
|
2485
|
+
durationMs: Date.now() - startTime,
|
|
2486
|
+
status: response.status
|
|
2487
|
+
};
|
|
2488
|
+
this.hooks.onResponse?.(result, responseMeta);
|
|
2489
|
+
return result;
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
2492
|
+
const errorMeta = {
|
|
2493
|
+
...requestMeta,
|
|
2494
|
+
durationMs: Date.now() - startTime
|
|
2495
|
+
};
|
|
2496
|
+
const normalizedError = this.normalizeError(error, url, method, timeoutMs, timeoutController);
|
|
2497
|
+
this.hooks.onError?.(normalizedError, errorMeta);
|
|
2498
|
+
throw normalizedError;
|
|
2499
|
+
} finally {
|
|
2500
|
+
combined?.cleanup();
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Normalizes various error types into SonioxHttpError.
|
|
2505
|
+
*/
|
|
2506
|
+
normalizeError(error, url, method, timeoutMs, timeoutController) {
|
|
2507
|
+
if (error instanceof SonioxHttpError) return error;
|
|
2508
|
+
if (timeoutController.signal.aborted && isAbortError(error)) return createTimeoutError(url, method, timeoutMs);
|
|
2509
|
+
if (isAbortError(error)) return createAbortError(url, method, error);
|
|
2510
|
+
if (error instanceof TypeError) return createNetworkError(url, method, error);
|
|
2511
|
+
return createNetworkError(url, method, error);
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
/**
|
|
2515
|
+
* Combines multiple AbortSignals into one.
|
|
2516
|
+
* The resulting signal will abort if any of the input signals abort.
|
|
2517
|
+
* Returns the combined signal and a cleanup function to remove listeners.
|
|
2518
|
+
*/
|
|
2519
|
+
function combineAbortSignals(...signals) {
|
|
2520
|
+
const controller = new AbortController();
|
|
2521
|
+
const handlers = [];
|
|
2522
|
+
const cleanup = () => {
|
|
2523
|
+
for (const { signal, handler } of handlers) signal.removeEventListener("abort", handler);
|
|
2524
|
+
handlers.length = 0;
|
|
2525
|
+
};
|
|
2526
|
+
for (const signal of signals) {
|
|
2527
|
+
if (signal.aborted) {
|
|
2528
|
+
controller.abort(signal.reason);
|
|
2529
|
+
return {
|
|
2530
|
+
signal: controller.signal,
|
|
2531
|
+
cleanup
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
const handler = () => {
|
|
2535
|
+
controller.abort(signal.reason);
|
|
2536
|
+
};
|
|
2537
|
+
handlers.push({
|
|
2538
|
+
signal,
|
|
2539
|
+
handler
|
|
2540
|
+
});
|
|
2541
|
+
signal.addEventListener("abort", handler, { once: true });
|
|
2542
|
+
}
|
|
2543
|
+
return {
|
|
2544
|
+
signal: controller.signal,
|
|
2545
|
+
cleanup
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
//#endregion
|
|
2550
|
+
//#region src/realtime/async-queue.ts
|
|
2551
|
+
/**
|
|
2552
|
+
* Generic async event queue that supports iteration with proper error propagation.
|
|
2553
|
+
*
|
|
2554
|
+
* This utility enables `for await...of` consumption of events while properly
|
|
2555
|
+
* surfacing errors to consumers instead of silently ending iteration.
|
|
2556
|
+
*/
|
|
2557
|
+
var AsyncEventQueue = class {
|
|
2558
|
+
queue = [];
|
|
2559
|
+
waiters = [];
|
|
2560
|
+
done = false;
|
|
2561
|
+
error = null;
|
|
2562
|
+
/**
|
|
2563
|
+
* Push an event to the queue.
|
|
2564
|
+
* If there are waiting consumers, delivers immediately.
|
|
2565
|
+
*/
|
|
2566
|
+
push(event) {
|
|
2567
|
+
if (this.done) return;
|
|
2568
|
+
const waiter = this.waiters.shift();
|
|
2569
|
+
if (waiter) waiter.resolve({
|
|
2570
|
+
value: event,
|
|
2571
|
+
done: false
|
|
2572
|
+
});
|
|
2573
|
+
else this.queue.push(event);
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* End the queue normally.
|
|
2577
|
+
* Waiting consumers will receive `{ done: true }`.
|
|
2578
|
+
*/
|
|
2579
|
+
end() {
|
|
2580
|
+
if (this.done) return;
|
|
2581
|
+
this.done = true;
|
|
2582
|
+
this.flushWaiters();
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* End the queue with an error.
|
|
2586
|
+
* Waiting consumers will have their promises rejected.
|
|
2587
|
+
* Future `next()` calls will also reject with this error.
|
|
2588
|
+
* Any queued events are discarded.
|
|
2589
|
+
*/
|
|
2590
|
+
abort(error) {
|
|
2591
|
+
if (this.done) return;
|
|
2592
|
+
this.done = true;
|
|
2593
|
+
this.error = error;
|
|
2594
|
+
this.queue = [];
|
|
2595
|
+
this.flushWaiters();
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Whether the queue has ended (normally or with error).
|
|
2599
|
+
*/
|
|
2600
|
+
get isDone() {
|
|
2601
|
+
return this.done;
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Async iterator implementation.
|
|
2605
|
+
*/
|
|
2606
|
+
[Symbol.asyncIterator]() {
|
|
2607
|
+
return { next: () => this.next() };
|
|
2608
|
+
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Get the next event from the queue.
|
|
2611
|
+
*/
|
|
2612
|
+
next() {
|
|
2613
|
+
const error = this.error;
|
|
2614
|
+
if (error) return Promise.reject(error);
|
|
2615
|
+
const event = this.queue.shift();
|
|
2616
|
+
if (event !== void 0) return Promise.resolve({
|
|
2617
|
+
value: event,
|
|
2618
|
+
done: false
|
|
2619
|
+
});
|
|
2620
|
+
if (this.done) return Promise.resolve({
|
|
2621
|
+
value: void 0,
|
|
2622
|
+
done: true
|
|
2623
|
+
});
|
|
2624
|
+
return new Promise((resolve, reject) => {
|
|
2625
|
+
this.waiters.push({
|
|
2626
|
+
resolve,
|
|
2627
|
+
reject
|
|
2628
|
+
});
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Flush all waiting consumers when queue ends.
|
|
2633
|
+
*/
|
|
2634
|
+
flushWaiters() {
|
|
2635
|
+
for (const { resolve, reject } of this.waiters) if (this.error) reject(this.error);
|
|
2636
|
+
else resolve({
|
|
2637
|
+
value: void 0,
|
|
2638
|
+
done: true
|
|
2639
|
+
});
|
|
2640
|
+
this.waiters = [];
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
|
|
2644
|
+
//#endregion
|
|
2645
|
+
//#region src/realtime/emitter.ts
|
|
2646
|
+
/**
|
|
2647
|
+
* A minimal, runtime-agnostic typed event emitter.
|
|
2648
|
+
* Does not depend on Node.js EventEmitter.
|
|
2649
|
+
*/
|
|
2650
|
+
var TypedEmitter = class {
|
|
2651
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2652
|
+
errorEvent = "error";
|
|
2653
|
+
/**
|
|
2654
|
+
* Register an event handler.
|
|
2655
|
+
*/
|
|
2656
|
+
on(event, handler) {
|
|
2657
|
+
let handlers = this.listeners.get(event);
|
|
2658
|
+
if (!handlers) {
|
|
2659
|
+
handlers = /* @__PURE__ */ new Set();
|
|
2660
|
+
this.listeners.set(event, handlers);
|
|
2661
|
+
}
|
|
2662
|
+
handlers.add(handler);
|
|
2663
|
+
return this;
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Register a one-time event handler.
|
|
2667
|
+
*/
|
|
2668
|
+
once(event, handler) {
|
|
2669
|
+
const wrapper = ((...args) => {
|
|
2670
|
+
this.off(event, wrapper);
|
|
2671
|
+
handler(...args);
|
|
2672
|
+
});
|
|
2673
|
+
return this.on(event, wrapper);
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* Remove an event handler.
|
|
2677
|
+
*/
|
|
2678
|
+
off(event, handler) {
|
|
2679
|
+
const handlers = this.listeners.get(event);
|
|
2680
|
+
if (handlers) {
|
|
2681
|
+
handlers.delete(handler);
|
|
2682
|
+
if (handlers.size === 0) this.listeners.delete(event);
|
|
2683
|
+
}
|
|
2684
|
+
return this;
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Emit an event to all registered handlers.
|
|
2688
|
+
* Handler errors do not prevent other handlers from running.
|
|
2689
|
+
* Errors are reported to an `error` event if present, otherwise rethrown async.
|
|
2690
|
+
*/
|
|
2691
|
+
emit(event, ...args) {
|
|
2692
|
+
const handlers = this.listeners.get(event);
|
|
2693
|
+
if (handlers) for (const handler of [...handlers]) try {
|
|
2694
|
+
handler(...args);
|
|
2695
|
+
} catch (error) {
|
|
2696
|
+
if (event === this.errorEvent) this.scheduleThrow(this.normalizeError(error));
|
|
2697
|
+
else this.reportListenerError(error);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Remove all event handlers.
|
|
2702
|
+
*/
|
|
2703
|
+
removeAllListeners(event) {
|
|
2704
|
+
if (event !== void 0) this.listeners.delete(event);
|
|
2705
|
+
else this.listeners.clear();
|
|
2706
|
+
}
|
|
2707
|
+
reportListenerError(error) {
|
|
2708
|
+
const normalizedError = this.normalizeError(error);
|
|
2709
|
+
const handlers = this.listeners.get(this.errorEvent);
|
|
2710
|
+
if (!handlers || handlers.size === 0) {
|
|
2711
|
+
this.scheduleThrow(normalizedError);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
for (const handler of [...handlers]) try {
|
|
2715
|
+
handler(normalizedError);
|
|
2716
|
+
} catch (handlerError) {
|
|
2717
|
+
this.scheduleThrow(this.normalizeError(handlerError));
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
normalizeError(error) {
|
|
2721
|
+
if (error instanceof Error) return error;
|
|
2722
|
+
return new Error(String(error));
|
|
2723
|
+
}
|
|
2724
|
+
scheduleThrow(error) {
|
|
2725
|
+
setTimeout(() => {
|
|
2726
|
+
throw error;
|
|
2727
|
+
}, 0);
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/realtime/errors.ts
|
|
2733
|
+
/**
|
|
2734
|
+
* Real-time (WebSocket) API error classes for the Soniox SDK
|
|
2735
|
+
* All real-time errors extend SonioxError
|
|
2736
|
+
*/
|
|
2737
|
+
/**
|
|
2738
|
+
* Base error class for all real-time (WebSocket) SDK errors
|
|
2739
|
+
*/
|
|
2740
|
+
var RealtimeError = class extends SonioxError {
|
|
2741
|
+
/**
|
|
2742
|
+
* Original response payload for debugging.
|
|
2743
|
+
* Contains the raw WebSocket message that caused the error.
|
|
2744
|
+
*/
|
|
2745
|
+
raw;
|
|
2746
|
+
constructor(message, code = "realtime_error", statusCode, raw) {
|
|
2747
|
+
super(message, code, statusCode);
|
|
2748
|
+
this.name = "RealtimeError";
|
|
2749
|
+
this.raw = raw;
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Creates a human-readable string representation
|
|
2753
|
+
*/
|
|
2754
|
+
toString() {
|
|
2755
|
+
const parts = [`${this.name} [${this.code}]: ${this.message}`];
|
|
2756
|
+
if (this.statusCode !== void 0) parts.push(` Status: ${this.statusCode}`);
|
|
2757
|
+
return parts.join("\n");
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Converts to a plain object for logging/serialization
|
|
2761
|
+
*/
|
|
2762
|
+
toJSON() {
|
|
2763
|
+
return {
|
|
2764
|
+
name: this.name,
|
|
2765
|
+
code: this.code,
|
|
2766
|
+
message: this.message,
|
|
2767
|
+
...this.statusCode !== void 0 && { statusCode: this.statusCode },
|
|
2768
|
+
...this.raw !== void 0 && { raw: this.raw }
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
/**
|
|
2773
|
+
* Authentication error (401).
|
|
2774
|
+
* Thrown when the API key is invalid or expired.
|
|
2775
|
+
*/
|
|
2776
|
+
var AuthError = class extends RealtimeError {
|
|
2777
|
+
constructor(message, statusCode, raw) {
|
|
2778
|
+
super(message, "auth_error", statusCode, raw);
|
|
2779
|
+
this.name = "AuthError";
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
/**
|
|
2783
|
+
* Bad request error (400).
|
|
2784
|
+
* Thrown for invalid configuration or parameters.
|
|
2785
|
+
*/
|
|
2786
|
+
var BadRequestError = class extends RealtimeError {
|
|
2787
|
+
constructor(message, statusCode, raw) {
|
|
2788
|
+
super(message, "bad_request", statusCode, raw);
|
|
2789
|
+
this.name = "BadRequestError";
|
|
2790
|
+
}
|
|
2791
|
+
};
|
|
2792
|
+
/**
|
|
2793
|
+
* Quota error (402, 429).
|
|
2794
|
+
* Thrown when rate limits are exceeded or quota is exhausted.
|
|
2795
|
+
*/
|
|
2796
|
+
var QuotaError = class extends RealtimeError {
|
|
2797
|
+
constructor(message, statusCode, raw) {
|
|
2798
|
+
super(message, "quota_exceeded", statusCode, raw);
|
|
2799
|
+
this.name = "QuotaError";
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
/**
|
|
2803
|
+
* Connection error.
|
|
2804
|
+
* Thrown for WebSocket connection failures and transport errors.
|
|
2805
|
+
*/
|
|
2806
|
+
var ConnectionError = class extends RealtimeError {
|
|
2807
|
+
constructor(message, raw) {
|
|
2808
|
+
super(message, "connection_error", void 0, raw);
|
|
2809
|
+
this.name = "ConnectionError";
|
|
2810
|
+
}
|
|
2811
|
+
};
|
|
2812
|
+
/**
|
|
2813
|
+
* Network error.
|
|
2814
|
+
* Thrown for server-side network issues (408, 500, 503).
|
|
2815
|
+
*/
|
|
2816
|
+
var NetworkError = class extends RealtimeError {
|
|
2817
|
+
constructor(message, statusCode, raw) {
|
|
2818
|
+
super(message, "network_error", statusCode, raw);
|
|
2819
|
+
this.name = "NetworkError";
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
/**
|
|
2823
|
+
* Abort error.
|
|
2824
|
+
* Thrown when an operation is cancelled via AbortSignal.
|
|
2825
|
+
*/
|
|
2826
|
+
var AbortError = class extends RealtimeError {
|
|
2827
|
+
constructor(message = "Operation aborted") {
|
|
2828
|
+
super(message, "aborted");
|
|
2829
|
+
this.name = "AbortError";
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
/**
|
|
2833
|
+
* State error.
|
|
2834
|
+
* Thrown when an operation is attempted in an invalid state.
|
|
2835
|
+
*/
|
|
2836
|
+
var StateError = class extends RealtimeError {
|
|
2837
|
+
constructor(message) {
|
|
2838
|
+
super(message, "state_error");
|
|
2839
|
+
this.name = "StateError";
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
/**
|
|
2843
|
+
* Map a Soniox error response to a typed error class.
|
|
2844
|
+
*
|
|
2845
|
+
* @param response - Error response from the WebSocket
|
|
2846
|
+
* @returns Appropriate error subclass
|
|
2847
|
+
*/
|
|
2848
|
+
function mapErrorResponse(response) {
|
|
2849
|
+
const { error_code, error_message } = response;
|
|
2850
|
+
const message = error_message ?? "Unknown error";
|
|
2851
|
+
switch (error_code) {
|
|
2852
|
+
case 401: return new AuthError(message, error_code, response);
|
|
2853
|
+
case 400: return new BadRequestError(message, error_code, response);
|
|
2854
|
+
case 402:
|
|
2855
|
+
case 429: return new QuotaError(message, error_code, response);
|
|
2856
|
+
case 408:
|
|
2857
|
+
case 500:
|
|
2858
|
+
case 503: return new NetworkError(message, error_code, response);
|
|
2859
|
+
default: return new RealtimeError(message, "realtime_error", error_code, response);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
//#endregion
|
|
2864
|
+
//#region src/realtime/stt.ts
|
|
2865
|
+
const DEFAULT_KEEPALIVE_INTERVAL_MS = 5e3;
|
|
2866
|
+
/**
|
|
2867
|
+
* Convert audio data to Uint8Array
|
|
2868
|
+
* Handles Buffer, Uint8Array, and ArrayBuffer
|
|
2869
|
+
*/
|
|
2870
|
+
function toUint8Array(data) {
|
|
2871
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
2872
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
2873
|
+
}
|
|
2874
|
+
/**
|
|
2875
|
+
* Build the configuration message to send after WebSocket connection
|
|
2876
|
+
*/
|
|
2877
|
+
function buildConfigMessage(config, apiKey) {
|
|
2878
|
+
return {
|
|
2879
|
+
api_key: apiKey,
|
|
2880
|
+
model: config.model,
|
|
2881
|
+
audio_format: config.audio_format ?? "auto",
|
|
2882
|
+
sample_rate: config.sample_rate,
|
|
2883
|
+
num_channels: config.num_channels,
|
|
2884
|
+
language_hints: config.language_hints,
|
|
2885
|
+
language_hints_strict: config.language_hints_strict,
|
|
2886
|
+
enable_speaker_diarization: config.enable_speaker_diarization,
|
|
2887
|
+
enable_language_identification: config.enable_language_identification,
|
|
2888
|
+
enable_endpoint_detection: config.enable_endpoint_detection,
|
|
2889
|
+
client_reference_id: config.client_reference_id,
|
|
2890
|
+
context: config.context,
|
|
2891
|
+
translation: config.translation
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Parse a result message from the WebSocket
|
|
2896
|
+
*/
|
|
2897
|
+
function parseResultMessage(data) {
|
|
2898
|
+
const raw = JSON.parse(data);
|
|
2899
|
+
if ("error_code" in raw || "error_message" in raw) throw mapErrorResponse(raw);
|
|
2900
|
+
return {
|
|
2901
|
+
tokens: (raw.tokens ?? []).map((t) => ({
|
|
2902
|
+
text: typeof t.text === "string" ? t.text : "",
|
|
2903
|
+
start_ms: typeof t.start_ms === "number" ? t.start_ms : void 0,
|
|
2904
|
+
end_ms: typeof t.end_ms === "number" ? t.end_ms : void 0,
|
|
2905
|
+
confidence: typeof t.confidence === "number" ? t.confidence : 0,
|
|
2906
|
+
is_final: Boolean(t.is_final),
|
|
2907
|
+
speaker: typeof t.speaker === "string" ? t.speaker : void 0,
|
|
2908
|
+
language: typeof t.language === "string" ? t.language : void 0,
|
|
2909
|
+
translation_status: t.translation_status === "none" || t.translation_status === "original" || t.translation_status === "translation" ? t.translation_status : void 0,
|
|
2910
|
+
source_language: typeof t.source_language === "string" ? t.source_language : void 0
|
|
2911
|
+
})),
|
|
2912
|
+
final_audio_proc_ms: typeof raw.final_audio_proc_ms === "number" ? raw.final_audio_proc_ms : 0,
|
|
2913
|
+
total_audio_proc_ms: typeof raw.total_audio_proc_ms === "number" ? raw.total_audio_proc_ms : 0,
|
|
2914
|
+
finished: raw.finished === true,
|
|
2915
|
+
raw
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Check if a token is a special control token
|
|
2920
|
+
*/
|
|
2921
|
+
function isSpecialToken(text) {
|
|
2922
|
+
return text === "<end>" || text === "<fin>";
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Filter out special control tokens from tokens array
|
|
2926
|
+
*/
|
|
2927
|
+
function filterSpecialTokens(tokens) {
|
|
2928
|
+
return tokens.filter((t) => !isSpecialToken(t.text));
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* Real-time Speech-to-Text session
|
|
2932
|
+
*
|
|
2933
|
+
* Provides WebSocket-based streaming transcription with support for:
|
|
2934
|
+
* - Event-based and async iterator consumption
|
|
2935
|
+
* - Pause/resume with automatic keepalive
|
|
2936
|
+
* - AbortSignal cancellation
|
|
2937
|
+
*
|
|
2938
|
+
* @example
|
|
2939
|
+
* ```typescript
|
|
2940
|
+
* const session = client.realtime.stt({ model: 'stt-rt-preview' });
|
|
2941
|
+
*
|
|
2942
|
+
* session.on('result', (result) => {
|
|
2943
|
+
* console.log(result.tokens.map(t => t.text).join(''));
|
|
2944
|
+
* });
|
|
2945
|
+
*
|
|
2946
|
+
* await session.connect();
|
|
2947
|
+
* await session.sendAudio(audioChunk);
|
|
2948
|
+
* await session.finish();
|
|
2949
|
+
* ```
|
|
2950
|
+
*/
|
|
2951
|
+
var RealtimeSttSession = class {
|
|
2952
|
+
emitter = new TypedEmitter();
|
|
2953
|
+
eventQueue = new AsyncEventQueue();
|
|
2954
|
+
apiKey;
|
|
2955
|
+
wsBaseUrl;
|
|
2956
|
+
config;
|
|
2957
|
+
keepaliveIntervalMs;
|
|
2958
|
+
keepaliveEnabled;
|
|
2959
|
+
signal;
|
|
2960
|
+
ws = null;
|
|
2961
|
+
_state = "idle";
|
|
2962
|
+
_paused = false;
|
|
2963
|
+
keepaliveInterval = null;
|
|
2964
|
+
finishResolver = null;
|
|
2965
|
+
finishRejecter = null;
|
|
2966
|
+
abortHandler = null;
|
|
2967
|
+
constructor(apiKey, wsBaseUrl, config, options) {
|
|
2968
|
+
this.apiKey = apiKey;
|
|
2969
|
+
this.wsBaseUrl = wsBaseUrl;
|
|
2970
|
+
this.config = config;
|
|
2971
|
+
this.keepaliveIntervalMs = options?.keepalive_interval_ms ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
|
|
2972
|
+
this.keepaliveEnabled = options?.keepalive ?? false;
|
|
2973
|
+
this.signal = options?.signal;
|
|
2974
|
+
if (this.signal) {
|
|
2975
|
+
this.abortHandler = () => this.handleAbort();
|
|
2976
|
+
this.signal.addEventListener("abort", this.abortHandler);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Current session state.
|
|
2981
|
+
*/
|
|
2982
|
+
get state() {
|
|
2983
|
+
return this._state;
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Whether the session is currently paused.
|
|
2987
|
+
*/
|
|
2988
|
+
get paused() {
|
|
2989
|
+
return this._paused;
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Connect to the Soniox WebSocket API.
|
|
2993
|
+
*
|
|
2994
|
+
* @throws {AbortError} If aborted
|
|
2995
|
+
* @throws {NetworkError} If connection fails
|
|
2996
|
+
* @throws {StateError} If already connected
|
|
2997
|
+
*/
|
|
2998
|
+
async connect() {
|
|
2999
|
+
if (this._state !== "idle") throw new StateError(`Cannot connect: session is in "${this._state}" state`);
|
|
3000
|
+
this.checkAborted();
|
|
3001
|
+
this.setState("connecting");
|
|
3002
|
+
try {
|
|
3003
|
+
await this.createWebSocket();
|
|
3004
|
+
this.setState("connected");
|
|
3005
|
+
this.emitter.emit("connected");
|
|
3006
|
+
this.updateKeepalive();
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
this.setState("error");
|
|
3009
|
+
throw error;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Send audio data to the server
|
|
3014
|
+
*
|
|
3015
|
+
* @param data - Audio data as Buffer, Uint8Array, or ArrayBuffer
|
|
3016
|
+
* @throws {AbortError} If aborted
|
|
3017
|
+
* @throws {StateError} If not connected
|
|
3018
|
+
*/
|
|
3019
|
+
sendAudio(data) {
|
|
3020
|
+
this.checkAborted();
|
|
3021
|
+
if (this._state !== "connected") throw new StateError(`Cannot send audio: session is in "${this._state}" state`);
|
|
3022
|
+
if (this._paused) return;
|
|
3023
|
+
const chunk = toUint8Array(data);
|
|
3024
|
+
this.sendMessage(chunk, true);
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Stream audio data from an async iterable source.
|
|
3028
|
+
*
|
|
3029
|
+
* Reads chunks from the iterable and sends each via {@link sendAudio}.
|
|
3030
|
+
* Works with Node.js ReadableStreams, Web ReadableStreams, async generators,
|
|
3031
|
+
* and any other `AsyncIterable<AudioData>`.
|
|
3032
|
+
*
|
|
3033
|
+
* @param stream - Async iterable yielding audio chunks
|
|
3034
|
+
* @param options - Optional pacing and auto-finish settings
|
|
3035
|
+
* @throws {AbortError} If aborted during streaming
|
|
3036
|
+
* @throws {StateError} If not connected
|
|
3037
|
+
*
|
|
3038
|
+
* @example
|
|
3039
|
+
* ```typescript
|
|
3040
|
+
* // Stream from a Node.js file
|
|
3041
|
+
* import fs from 'fs';
|
|
3042
|
+
* await session.sendStream(fs.createReadStream('audio.mp3'), { finish: true });
|
|
3043
|
+
*
|
|
3044
|
+
* // Stream with simulated real-time pacing
|
|
3045
|
+
* await session.sendStream(
|
|
3046
|
+
* fs.createReadStream('audio.pcm_s16le', { highWaterMark: 3840 }),
|
|
3047
|
+
* { pace_ms: 120, finish: true }
|
|
3048
|
+
* );
|
|
3049
|
+
*
|
|
3050
|
+
* // Stream from a Web fetch response
|
|
3051
|
+
* const response = await fetch('https://soniox.com/media/examples/coffee_shop.mp3');
|
|
3052
|
+
* await session.sendStream(response.body, { finish: true });
|
|
3053
|
+
* ```
|
|
3054
|
+
*/
|
|
3055
|
+
async sendStream(stream, options) {
|
|
3056
|
+
for await (const chunk of stream) {
|
|
3057
|
+
this.sendAudio(chunk);
|
|
3058
|
+
if (options?.pace_ms) await new Promise((resolve) => setTimeout(resolve, options.pace_ms));
|
|
3059
|
+
}
|
|
3060
|
+
if (options?.finish) await this.finish();
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Pause audio transmission and starts automatic keepalive messages
|
|
3064
|
+
*/
|
|
3065
|
+
pause() {
|
|
3066
|
+
if (this._paused) return;
|
|
3067
|
+
this._paused = true;
|
|
3068
|
+
this.updateKeepalive();
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Resume audio transmission
|
|
3072
|
+
*/
|
|
3073
|
+
resume() {
|
|
3074
|
+
if (!this._paused) return;
|
|
3075
|
+
this._paused = false;
|
|
3076
|
+
this.updateKeepalive();
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Requests the server to finalize current transcription
|
|
3080
|
+
*/
|
|
3081
|
+
finalize(options) {
|
|
3082
|
+
if (this._state !== "connected" && this._state !== "finishing") return;
|
|
3083
|
+
const message = { type: "finalize" };
|
|
3084
|
+
if (options?.trailing_silence_ms !== void 0) message.trailing_silence_ms = options.trailing_silence_ms;
|
|
3085
|
+
this.sendMessage(JSON.stringify(message), false);
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Send a keepalive message
|
|
3089
|
+
*/
|
|
3090
|
+
keepAlive() {
|
|
3091
|
+
if (this._state !== "connected" && this._state !== "finishing") return;
|
|
3092
|
+
this.sendMessage(JSON.stringify({ type: "keepalive" }), false);
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* Gracefully finish the session
|
|
3096
|
+
*/
|
|
3097
|
+
async finish() {
|
|
3098
|
+
this.checkAborted();
|
|
3099
|
+
if (this._state !== "connected") throw new StateError(`Cannot finish: session is in "${this._state}" state`);
|
|
3100
|
+
if (this._paused) this.resume();
|
|
3101
|
+
this.setState("finishing");
|
|
3102
|
+
this.updateKeepalive();
|
|
3103
|
+
const finishPromise = new Promise((resolve, reject) => {
|
|
3104
|
+
this.finishResolver = resolve;
|
|
3105
|
+
this.finishRejecter = reject;
|
|
3106
|
+
});
|
|
3107
|
+
this.sendMessage("", false);
|
|
3108
|
+
return finishPromise;
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Close (cancel) the session immediately without waiting
|
|
3112
|
+
*/
|
|
3113
|
+
close() {
|
|
3114
|
+
this.emitter.emit("disconnected", "client_closed");
|
|
3115
|
+
this.settleFinish(new StateError("Session canceled"));
|
|
3116
|
+
this.cleanup("canceled");
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Register an event handler
|
|
3120
|
+
*/
|
|
3121
|
+
on(event, handler) {
|
|
3122
|
+
this.emitter.on(event, handler);
|
|
3123
|
+
return this;
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Register a one-time event handler
|
|
3127
|
+
*/
|
|
3128
|
+
once(event, handler) {
|
|
3129
|
+
this.emitter.once(event, handler);
|
|
3130
|
+
return this;
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Remove an event handler
|
|
3134
|
+
*/
|
|
3135
|
+
off(event, handler) {
|
|
3136
|
+
this.emitter.off(event, handler);
|
|
3137
|
+
return this;
|
|
3138
|
+
}
|
|
3139
|
+
/**
|
|
3140
|
+
* Async iterator for consuming events.
|
|
3141
|
+
*/
|
|
3142
|
+
[Symbol.asyncIterator]() {
|
|
3143
|
+
return this.eventQueue[Symbol.asyncIterator]();
|
|
3144
|
+
}
|
|
3145
|
+
async createWebSocket() {
|
|
3146
|
+
return new Promise((resolve, reject) => {
|
|
3147
|
+
try {
|
|
3148
|
+
const ws = new WebSocket(this.wsBaseUrl);
|
|
3149
|
+
this.ws = ws;
|
|
3150
|
+
ws.binaryType = "arraybuffer";
|
|
3151
|
+
const cleanup = () => {
|
|
3152
|
+
ws.removeEventListener("open", onOpen);
|
|
3153
|
+
ws.removeEventListener("error", onError);
|
|
3154
|
+
};
|
|
3155
|
+
const onOpen = () => {
|
|
3156
|
+
cleanup();
|
|
3157
|
+
const configMessage = buildConfigMessage(this.config, this.apiKey);
|
|
3158
|
+
ws.send(JSON.stringify(configMessage));
|
|
3159
|
+
ws.addEventListener("message", this.handleMessage.bind(this));
|
|
3160
|
+
ws.addEventListener("close", this.handleClose.bind(this));
|
|
3161
|
+
ws.addEventListener("error", this.handleError.bind(this));
|
|
3162
|
+
resolve();
|
|
3163
|
+
};
|
|
3164
|
+
const onError = (event) => {
|
|
3165
|
+
cleanup();
|
|
3166
|
+
reject(new ConnectionError("WebSocket connection failed", event));
|
|
3167
|
+
};
|
|
3168
|
+
ws.addEventListener("open", onOpen);
|
|
3169
|
+
ws.addEventListener("error", onError);
|
|
3170
|
+
if (this.signal) this.signal.addEventListener("abort", () => {
|
|
3171
|
+
cleanup();
|
|
3172
|
+
ws.close();
|
|
3173
|
+
reject(new AbortError());
|
|
3174
|
+
}, { once: true });
|
|
3175
|
+
} catch (error) {
|
|
3176
|
+
reject(new ConnectionError("Failed to create WebSocket", error));
|
|
3177
|
+
}
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
handleMessage(event) {
|
|
3181
|
+
if (typeof event.data !== "string") return;
|
|
3182
|
+
const data = event.data;
|
|
3183
|
+
try {
|
|
3184
|
+
const result = parseResultMessage(data);
|
|
3185
|
+
const hasEndpoint = result.tokens.some((t) => t.text === "<end>");
|
|
3186
|
+
const hasFinalized = result.tokens.some((t) => t.text === "<fin>");
|
|
3187
|
+
const userTokens = filterSpecialTokens(result.tokens);
|
|
3188
|
+
for (const token of userTokens) this.emitter.emit("token", token);
|
|
3189
|
+
const filteredResult = {
|
|
3190
|
+
...result,
|
|
3191
|
+
tokens: userTokens
|
|
3192
|
+
};
|
|
3193
|
+
this.emitter.emit("result", filteredResult);
|
|
3194
|
+
this.eventQueue.push({
|
|
3195
|
+
kind: "result",
|
|
3196
|
+
data: filteredResult
|
|
3197
|
+
});
|
|
3198
|
+
if (hasEndpoint) {
|
|
3199
|
+
this.emitter.emit("endpoint");
|
|
3200
|
+
this.eventQueue.push({ kind: "endpoint" });
|
|
3201
|
+
}
|
|
3202
|
+
if (hasFinalized) {
|
|
3203
|
+
this.emitter.emit("finalized");
|
|
3204
|
+
this.eventQueue.push({ kind: "finalized" });
|
|
3205
|
+
}
|
|
3206
|
+
if (result.finished) {
|
|
3207
|
+
this.emitter.emit("finished");
|
|
3208
|
+
this.eventQueue.push({ kind: "finished" });
|
|
3209
|
+
this.settleFinish();
|
|
3210
|
+
this.cleanup("finished");
|
|
3211
|
+
}
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
const err = error;
|
|
3214
|
+
this.emitter.emit("error", err);
|
|
3215
|
+
this.settleFinish(err);
|
|
3216
|
+
this.cleanup("error", err);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
handleClose(event) {
|
|
3220
|
+
if (this.isTerminalState(this._state)) return;
|
|
3221
|
+
this.emitter.emit("disconnected", event.reason || void 0);
|
|
3222
|
+
if (this._state === "finishing") {
|
|
3223
|
+
const error = new ConnectionError("WebSocket closed before finished response", event);
|
|
3224
|
+
this.emitter.emit("error", error);
|
|
3225
|
+
this.settleFinish(error);
|
|
3226
|
+
this.cleanup("error", error);
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
this.cleanup("closed");
|
|
3230
|
+
}
|
|
3231
|
+
handleError(event) {
|
|
3232
|
+
const error = new ConnectionError("WebSocket error", event);
|
|
3233
|
+
this.emitter.emit("error", error);
|
|
3234
|
+
this.settleFinish(error);
|
|
3235
|
+
this.cleanup("error", error);
|
|
3236
|
+
}
|
|
3237
|
+
handleAbort() {
|
|
3238
|
+
const error = new AbortError();
|
|
3239
|
+
this.emitter.emit("error", error);
|
|
3240
|
+
this.settleFinish(error);
|
|
3241
|
+
this.cleanup("canceled", error);
|
|
3242
|
+
}
|
|
3243
|
+
setState(newState) {
|
|
3244
|
+
if (this._state === newState) return;
|
|
3245
|
+
const oldState = this._state;
|
|
3246
|
+
this._state = newState;
|
|
3247
|
+
this.emitter.emit("state_change", {
|
|
3248
|
+
old_state: oldState,
|
|
3249
|
+
new_state: newState
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
cleanup(finalState, error) {
|
|
3253
|
+
this.setState(finalState);
|
|
3254
|
+
this.stopKeepalive();
|
|
3255
|
+
if (this.signal && this.abortHandler) {
|
|
3256
|
+
this.signal.removeEventListener("abort", this.abortHandler);
|
|
3257
|
+
this.abortHandler = null;
|
|
3258
|
+
}
|
|
3259
|
+
if (this.ws) {
|
|
3260
|
+
this.ws.close();
|
|
3261
|
+
this.ws = null;
|
|
3262
|
+
}
|
|
3263
|
+
if (error) this.eventQueue.abort(error);
|
|
3264
|
+
else this.eventQueue.end();
|
|
3265
|
+
this.emitter.removeAllListeners();
|
|
3266
|
+
}
|
|
3267
|
+
isTerminalState(state) {
|
|
3268
|
+
return state === "closed" || state === "error" || state === "finished" || state === "canceled";
|
|
3269
|
+
}
|
|
3270
|
+
checkAborted() {
|
|
3271
|
+
if (this.signal?.aborted) throw new AbortError();
|
|
3272
|
+
}
|
|
3273
|
+
settleFinish(error) {
|
|
3274
|
+
if (!this.finishResolver && !this.finishRejecter) return;
|
|
3275
|
+
const resolve = this.finishResolver;
|
|
3276
|
+
const reject = this.finishRejecter;
|
|
3277
|
+
this.finishResolver = null;
|
|
3278
|
+
this.finishRejecter = null;
|
|
3279
|
+
if (error) reject?.(error);
|
|
3280
|
+
else resolve?.();
|
|
3281
|
+
}
|
|
3282
|
+
sendMessage(data, shouldThrow) {
|
|
3283
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
3284
|
+
const error = new ConnectionError("WebSocket is not open");
|
|
3285
|
+
this.emitter.emit("error", error);
|
|
3286
|
+
this.settleFinish(error);
|
|
3287
|
+
this.cleanup("error", error);
|
|
3288
|
+
if (shouldThrow) throw error;
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
try {
|
|
3292
|
+
this.ws.send(data);
|
|
3293
|
+
} catch (err) {
|
|
3294
|
+
const error = new ConnectionError("WebSocket send failed", err);
|
|
3295
|
+
this.emitter.emit("error", error);
|
|
3296
|
+
this.settleFinish(error);
|
|
3297
|
+
this.cleanup("error", error);
|
|
3298
|
+
if (shouldThrow) throw error;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
startKeepalive() {
|
|
3302
|
+
if (this.keepaliveInterval) return;
|
|
3303
|
+
this.keepaliveInterval = setInterval(() => {
|
|
3304
|
+
this.keepAlive();
|
|
3305
|
+
}, this.keepaliveIntervalMs);
|
|
3306
|
+
}
|
|
3307
|
+
stopKeepalive() {
|
|
3308
|
+
if (this.keepaliveInterval) {
|
|
3309
|
+
clearInterval(this.keepaliveInterval);
|
|
3310
|
+
this.keepaliveInterval = null;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
updateKeepalive() {
|
|
3314
|
+
if ((this._state === "connected" || this._state === "finishing") && (this._paused || this.keepaliveEnabled)) this.startKeepalive();
|
|
3315
|
+
else this.stopKeepalive();
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
|
|
3319
|
+
//#endregion
|
|
3320
|
+
//#region src/realtime/segments.ts
|
|
3321
|
+
/**
|
|
3322
|
+
* Groups real-time tokens into segments based on specified grouping keys.
|
|
3323
|
+
*
|
|
3324
|
+
* A new segment starts when any of the `group_by` fields changes.
|
|
3325
|
+
* Tokens are concatenated as-is.
|
|
3326
|
+
*
|
|
3327
|
+
* @param tokens - Array of real-time tokens to segment
|
|
3328
|
+
* @param options - Segmentation options
|
|
3329
|
+
* @param options.group_by - Fields to group by (default: ['speaker', 'language'])
|
|
3330
|
+
* @param options.final_only - When true, only finalized tokens are included
|
|
3331
|
+
* @returns Array of segments with combined text and timing (if available)
|
|
3332
|
+
*/
|
|
3333
|
+
function segmentRealtimeTokens(tokens, options = {}) {
|
|
3334
|
+
return segmentTokens(options.final_only ? tokens.filter((token) => token.is_final) : tokens, options, buildSegment);
|
|
3335
|
+
}
|
|
3336
|
+
function buildSegment(tokens, speaker, language) {
|
|
3337
|
+
const firstToken = tokens[0];
|
|
3338
|
+
const lastToken = tokens[tokens.length - 1];
|
|
3339
|
+
if (!firstToken || !lastToken) throw new Error("Cannot build segment from an empty token array");
|
|
3340
|
+
return {
|
|
3341
|
+
text: tokens.map((t) => t.text).join(""),
|
|
3342
|
+
start_ms: firstToken.start_ms,
|
|
3343
|
+
end_ms: lastToken.end_ms,
|
|
3344
|
+
...!!speaker && { speaker },
|
|
3345
|
+
...!!language && { language },
|
|
3346
|
+
tokens
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
//#endregion
|
|
3351
|
+
//#region src/realtime/segment-buffer.ts
|
|
3352
|
+
const DEFAULT_MAX_TOKENS = 2e3;
|
|
3353
|
+
/**
|
|
3354
|
+
* Rolling buffer for turning real-time results into stable segments.
|
|
3355
|
+
*/
|
|
3356
|
+
var RealtimeSegmentBuffer = class {
|
|
3357
|
+
tokens = [];
|
|
3358
|
+
groupBy;
|
|
3359
|
+
finalOnly;
|
|
3360
|
+
maxTokens;
|
|
3361
|
+
maxMs;
|
|
3362
|
+
constructor(options = {}) {
|
|
3363
|
+
validatePositive("max_tokens", options.max_tokens);
|
|
3364
|
+
validatePositive("max_ms", options.max_ms);
|
|
3365
|
+
this.groupBy = options.group_by;
|
|
3366
|
+
this.finalOnly = options.final_only ?? true;
|
|
3367
|
+
this.maxTokens = options.max_tokens ?? DEFAULT_MAX_TOKENS;
|
|
3368
|
+
this.maxMs = options.max_ms;
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Number of tokens currently buffered.
|
|
3372
|
+
*/
|
|
3373
|
+
get size() {
|
|
3374
|
+
return this.tokens.length;
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Add a real-time result and return stable segments.
|
|
3378
|
+
*/
|
|
3379
|
+
add(result) {
|
|
3380
|
+
const incoming = this.finalOnly ? result.tokens.filter((token) => token.is_final) : result.tokens;
|
|
3381
|
+
if (incoming.length > 0) this.tokens.push(...incoming);
|
|
3382
|
+
const stableSegments = this.flushStable(result.final_audio_proc_ms);
|
|
3383
|
+
this.trim();
|
|
3384
|
+
return stableSegments;
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Clear all buffered tokens.
|
|
3388
|
+
*/
|
|
3389
|
+
reset() {
|
|
3390
|
+
this.tokens = [];
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Flush all buffered tokens into segments and clear the buffer.
|
|
3394
|
+
*
|
|
3395
|
+
* Includes tokens that are not yet stable by final_audio_proc_ms.
|
|
3396
|
+
*/
|
|
3397
|
+
flushAll() {
|
|
3398
|
+
if (this.tokens.length === 0) return [];
|
|
3399
|
+
const segments = segmentRealtimeTokens(this.tokens, { group_by: this.groupBy });
|
|
3400
|
+
this.tokens = [];
|
|
3401
|
+
return segments;
|
|
3402
|
+
}
|
|
3403
|
+
flushStable(finalAudioProcMs) {
|
|
3404
|
+
if (!Number.isFinite(finalAudioProcMs) || finalAudioProcMs <= 0) return [];
|
|
3405
|
+
const segments = segmentRealtimeTokens(this.tokens, { group_by: this.groupBy });
|
|
3406
|
+
const stableSegments = [];
|
|
3407
|
+
let dropCount = 0;
|
|
3408
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
3409
|
+
const segment = segments[i];
|
|
3410
|
+
const endMs = segment.tokens[segment.tokens.length - 1]?.end_ms;
|
|
3411
|
+
if (endMs === void 0 || endMs > finalAudioProcMs) break;
|
|
3412
|
+
stableSegments.push(segment);
|
|
3413
|
+
dropCount += segment.tokens.length;
|
|
3414
|
+
}
|
|
3415
|
+
if (dropCount > 0) this.tokens = this.tokens.slice(dropCount);
|
|
3416
|
+
return stableSegments;
|
|
3417
|
+
}
|
|
3418
|
+
trim() {
|
|
3419
|
+
if (this.maxTokens !== void 0 && this.tokens.length > this.maxTokens) this.tokens = this.tokens.slice(this.tokens.length - this.maxTokens);
|
|
3420
|
+
if (this.maxMs === void 0) return;
|
|
3421
|
+
const latestEndMs = findLatestEndMs(this.tokens);
|
|
3422
|
+
if (latestEndMs === void 0) return;
|
|
3423
|
+
const cutoff = latestEndMs - this.maxMs;
|
|
3424
|
+
if (cutoff <= 0) return;
|
|
3425
|
+
let dropIndex = 0;
|
|
3426
|
+
while (dropIndex < this.tokens.length) {
|
|
3427
|
+
const token = this.tokens[dropIndex];
|
|
3428
|
+
if (token?.end_ms === void 0 || token.end_ms >= cutoff) break;
|
|
3429
|
+
dropIndex += 1;
|
|
3430
|
+
}
|
|
3431
|
+
if (dropIndex > 0) this.tokens = this.tokens.slice(dropIndex);
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
function findLatestEndMs(tokens) {
|
|
3435
|
+
for (let i = tokens.length - 1; i >= 0; i -= 1) {
|
|
3436
|
+
const endMs = tokens[i]?.end_ms;
|
|
3437
|
+
if (typeof endMs === "number") return endMs;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
function validatePositive(name, value) {
|
|
3441
|
+
if (value === void 0) return;
|
|
3442
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a finite positive number`);
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
//#endregion
|
|
3446
|
+
//#region src/realtime/utterance-buffer.ts
|
|
3447
|
+
/**
|
|
3448
|
+
* Collects real-time results into utterances for endpoint-driven workflows.
|
|
3449
|
+
*/
|
|
3450
|
+
var RealtimeUtteranceBuffer = class {
|
|
3451
|
+
segmentBuffer;
|
|
3452
|
+
pendingSegments = [];
|
|
3453
|
+
lastFinalAudioProcMs;
|
|
3454
|
+
lastTotalAudioProcMs;
|
|
3455
|
+
constructor(options = {}) {
|
|
3456
|
+
this.segmentBuffer = new RealtimeSegmentBuffer(options);
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Add a real-time result and collect stable segments.
|
|
3460
|
+
*/
|
|
3461
|
+
addResult(result) {
|
|
3462
|
+
this.lastFinalAudioProcMs = result.final_audio_proc_ms;
|
|
3463
|
+
this.lastTotalAudioProcMs = result.total_audio_proc_ms;
|
|
3464
|
+
const stableSegments = this.segmentBuffer.add(result);
|
|
3465
|
+
if (stableSegments.length > 0) this.pendingSegments.push(...stableSegments);
|
|
3466
|
+
return stableSegments;
|
|
3467
|
+
}
|
|
3468
|
+
/**
|
|
3469
|
+
* Mark an endpoint and flush the current utterance.
|
|
3470
|
+
*/
|
|
3471
|
+
markEndpoint() {
|
|
3472
|
+
const trailingSegments = this.segmentBuffer.flushAll();
|
|
3473
|
+
const segments = [...this.pendingSegments, ...trailingSegments];
|
|
3474
|
+
this.pendingSegments = [];
|
|
3475
|
+
if (segments.length === 0) return;
|
|
3476
|
+
return buildUtterance(segments, this.lastFinalAudioProcMs, this.lastTotalAudioProcMs);
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Clear buffered segments and tokens.
|
|
3480
|
+
*/
|
|
3481
|
+
reset() {
|
|
3482
|
+
this.pendingSegments = [];
|
|
3483
|
+
this.segmentBuffer.reset();
|
|
3484
|
+
}
|
|
3485
|
+
};
|
|
3486
|
+
function buildUtterance(segments, finalAudioProcMs, totalAudioProcMs) {
|
|
3487
|
+
const tokens = segments.flatMap((segment) => segment.tokens);
|
|
3488
|
+
return {
|
|
3489
|
+
text: segments.map((segment) => segment.text).join(""),
|
|
3490
|
+
segments,
|
|
3491
|
+
tokens,
|
|
3492
|
+
start_ms: segments[0]?.start_ms,
|
|
3493
|
+
end_ms: segments[segments.length - 1]?.end_ms,
|
|
3494
|
+
speaker: getCommonValue(segments.map((segment) => segment.speaker)),
|
|
3495
|
+
language: getCommonValue(segments.map((segment) => segment.language)),
|
|
3496
|
+
final_audio_proc_ms: finalAudioProcMs,
|
|
3497
|
+
total_audio_proc_ms: totalAudioProcMs
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
function getCommonValue(values) {
|
|
3501
|
+
let common;
|
|
3502
|
+
for (const value of values) {
|
|
3503
|
+
if (value === void 0) return;
|
|
3504
|
+
if (common === void 0) {
|
|
3505
|
+
common = value;
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
if (value !== common) return;
|
|
3509
|
+
}
|
|
3510
|
+
return common;
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
//#endregion
|
|
3514
|
+
//#region src/realtime/index.ts
|
|
3515
|
+
/**
|
|
3516
|
+
* Real-time API factory for creating STT sessions.
|
|
3517
|
+
*
|
|
3518
|
+
* @example
|
|
3519
|
+
* ```typescript
|
|
3520
|
+
* const session = client.realtime.stt({
|
|
3521
|
+
* model: 'stt-rt-preview',
|
|
3522
|
+
* enable_endpoint_detection: true,
|
|
3523
|
+
* });
|
|
3524
|
+
*
|
|
3525
|
+
* await session.connect();
|
|
3526
|
+
* ```
|
|
3527
|
+
*/
|
|
3528
|
+
var SonioxRealtimeApi = class {
|
|
3529
|
+
options;
|
|
3530
|
+
constructor(options) {
|
|
3531
|
+
this.options = options;
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Create a new Speech-to-Text session.
|
|
3535
|
+
*
|
|
3536
|
+
* @param config - Session configuration (sent to server)
|
|
3537
|
+
* @param options - Session options (SDK-level settings)
|
|
3538
|
+
* @returns New STT session instance
|
|
3539
|
+
*/
|
|
3540
|
+
stt(config, options) {
|
|
3541
|
+
const mergedOptions = {
|
|
3542
|
+
...this.options.default_session_options,
|
|
3543
|
+
...options
|
|
3544
|
+
};
|
|
3545
|
+
return new RealtimeSttSession(this.options.api_key, this.options.ws_base_url, config, mergedOptions);
|
|
3546
|
+
}
|
|
3547
|
+
};
|
|
3548
|
+
|
|
3549
|
+
//#endregion
|
|
3550
|
+
//#region src/client.ts
|
|
3551
|
+
/**
|
|
3552
|
+
* Soniox Node Client
|
|
3553
|
+
* @returns {SonioxNodeClient}
|
|
3554
|
+
*
|
|
3555
|
+
* @example
|
|
3556
|
+
* ```typescript
|
|
3557
|
+
* import { SonioxNodeClient } from '@soniox/node';
|
|
3558
|
+
*
|
|
3559
|
+
* const client = new SonioxNodeClient({
|
|
3560
|
+
* api_key: 'your-api-key',
|
|
3561
|
+
* });
|
|
3562
|
+
* ```
|
|
3563
|
+
*/
|
|
3564
|
+
var SonioxNodeClient = class {
|
|
3565
|
+
files;
|
|
3566
|
+
stt;
|
|
3567
|
+
models;
|
|
3568
|
+
webhooks;
|
|
3569
|
+
auth;
|
|
3570
|
+
realtime;
|
|
3571
|
+
constructor(options = {}) {
|
|
3572
|
+
const apiKey = options.api_key ?? process.env["SONIOX_API_KEY"];
|
|
3573
|
+
if (!apiKey) throw new Error("Missing API key. Provide it via options.api_key or set the SONIOX_API_KEY environment variable.");
|
|
3574
|
+
const baseURL = options.base_url ?? process.env["SONIOX_API_BASE_URL"] ?? SONIOX_API_BASE_URL;
|
|
3575
|
+
const http = options.http_client ?? new FetchHttpClient({
|
|
3576
|
+
base_url: baseURL,
|
|
3577
|
+
default_headers: {
|
|
3578
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3579
|
+
"Content-Type": "application/json"
|
|
3580
|
+
}
|
|
3581
|
+
});
|
|
3582
|
+
this.files = new SonioxFilesAPI(http);
|
|
3583
|
+
this.stt = new SonioxSttApi(http, this.files);
|
|
3584
|
+
this.models = new SonioxModelsAPI(http);
|
|
3585
|
+
this.webhooks = new SonioxWebhooksAPI(this.stt);
|
|
3586
|
+
this.auth = new SonioxAuthAPI(http);
|
|
3587
|
+
this.realtime = new SonioxRealtimeApi({
|
|
3588
|
+
api_key: apiKey,
|
|
3589
|
+
ws_base_url: options.realtime?.ws_base_url ?? process.env["SONIOX_WS_URL"] ?? SONIOX_API_WS_URL,
|
|
3590
|
+
default_session_options: options.realtime?.default_session_options
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
};
|
|
3594
|
+
|
|
3595
|
+
//#endregion
|
|
3596
|
+
export { AbortError, AuthError, BadRequestError, ConnectionError, FetchHttpClient, FileListResult, NetworkError, QuotaError, RealtimeError, RealtimeSegmentBuffer, RealtimeSttSession, RealtimeUtteranceBuffer, SONIOX_API_BASE_URL, SONIOX_API_WEBHOOK_HEADER_ENV, SONIOX_API_WEBHOOK_SECRET_ENV, SONIOX_API_WS_URL, SONIOX_TMP_API_KEY_DURATION_MAX, SONIOX_TMP_API_KEY_DURATION_MIN, SONIOX_TMP_API_KEY_USAGE_TYPE, SonioxError, SonioxFile, SonioxHttpError, SonioxNodeClient, SonioxRealtimeApi, SonioxTranscript, SonioxTranscription, StateError, TranscriptionListResult, buildUrl, createAbortError, createHttpError, createNetworkError, createParseError, createTimeoutError, isAbortError, isSonioxError, isSonioxHttpError, mergeHeaders, normalizeHeaders, segmentRealtimeTokens, segmentTranscript };
|
|
3597
|
+
//# sourceMappingURL=index.mjs.map
|