assemblyai 4.35.3 → 4.36.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +98 -0
  3. package/dist/assemblyai.streaming.umd.js +26 -11
  4. package/dist/assemblyai.streaming.umd.min.js +1 -1
  5. package/dist/assemblyai.umd.js +486 -83
  6. package/dist/assemblyai.umd.min.js +1 -1
  7. package/dist/browser.mjs +430 -80
  8. package/dist/bun.mjs +420 -69
  9. package/dist/deno.mjs +420 -69
  10. package/dist/index.cjs +479 -76
  11. package/dist/index.mjs +477 -77
  12. package/dist/node.cjs +418 -64
  13. package/dist/node.mjs +416 -65
  14. package/dist/services/base.d.ts +1 -0
  15. package/dist/services/index.d.ts +7 -1
  16. package/dist/services/sync/index.d.ts +1 -0
  17. package/dist/services/sync/service.d.ts +48 -0
  18. package/dist/streaming.browser.mjs +24 -11
  19. package/dist/streaming.cjs +25 -10
  20. package/dist/streaming.mjs +25 -10
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/services/index.d.ts +1 -0
  23. package/dist/types/streaming/index.d.ts +6 -1
  24. package/dist/types/sync/index.d.ts +133 -0
  25. package/dist/utils/errors/index.d.ts +1 -0
  26. package/dist/utils/errors/sync.d.ts +20 -0
  27. package/dist/workerd.mjs +424 -73
  28. package/package.json +1 -1
  29. package/src/services/base.ts +18 -8
  30. package/src/services/index.ts +19 -0
  31. package/src/services/streaming/service.ts +17 -6
  32. package/src/services/sync/index.ts +1 -0
  33. package/src/services/sync/service.ts +369 -0
  34. package/src/types/index.ts +1 -0
  35. package/src/types/services/index.ts +1 -0
  36. package/src/types/streaming/index.ts +6 -1
  37. package/src/types/sync/index.ts +145 -0
  38. package/src/utils/errors/index.ts +2 -0
  39. package/src/utils/errors/sync.ts +25 -0
package/dist/workerd.mjs CHANGED
@@ -1,5 +1,10 @@
1
1
  import ws from 'ws';
2
2
 
3
+ /**
4
+ * The default speech model for synchronous transcription.
5
+ */
6
+ const defaultSyncSpeechModel = "universal-3-5-pro";
7
+
3
8
  /**
4
9
  * Thrown when `DualChannelCapture` is constructed in a non-browser environment
5
10
  * (no `globalThis.AudioContext`). The helper is intentionally surfaced from the
@@ -13,6 +18,12 @@ class BrowserOnlyError extends Error {
13
18
  }
14
19
  }
15
20
 
21
+ const readFile = async function (
22
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
23
+ path) {
24
+ throw new Error("Interacting with the file system is not supported in this environment.");
25
+ };
26
+
16
27
  const DEFAULT_FETCH_INIT = {};
17
28
 
18
29
  const buildUserAgent = (userAgent) => defaultUserAgentString +
@@ -28,7 +39,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
28
39
  defaultUserAgentString += navigator.userAgent;
29
40
  }
30
41
  const defaultUserAgent = {
31
- sdk: { name: "JavaScript", version: "4.35.3" },
42
+ sdk: { name: "JavaScript", version: "4.36.3" },
32
43
  };
33
44
  if (typeof process !== "undefined") {
34
45
  if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
@@ -67,12 +78,15 @@ class BaseService {
67
78
  this.userAgent = buildUserAgent(params.userAgent || {});
68
79
  }
69
80
  }
70
- async fetch(input, init) {
81
+ async fetchResponse(input, init) {
71
82
  init = { ...DEFAULT_FETCH_INIT, ...init };
72
83
  let headers = {
73
84
  Authorization: this.params.apiKey,
74
- "Content-Type": "application/json",
75
85
  };
86
+ // FormData bodies must let fetch set the multipart boundary itself.
87
+ if (!(init.body instanceof FormData)) {
88
+ headers["Content-Type"] = "application/json";
89
+ }
76
90
  if (DEFAULT_FETCH_INIT?.headers)
77
91
  headers = { ...headers, ...DEFAULT_FETCH_INIT.headers };
78
92
  if (init?.headers)
@@ -83,7 +97,10 @@ class BaseService {
83
97
  init.headers = headers;
84
98
  if (!input.startsWith("http"))
85
99
  input = this.params.baseUrl + input;
86
- const response = await fetch(input, init);
100
+ return await fetch(input, init);
101
+ }
102
+ async fetch(input, init) {
103
+ const response = await this.fetchResponse(input, init);
87
104
  if (response.status >= 400) {
88
105
  let json;
89
106
  const text = await response.text();
@@ -108,58 +125,342 @@ class BaseService {
108
125
  }
109
126
  }
110
127
 
111
- class LemurService extends BaseService {
112
- summary(params, signal) {
113
- return this.fetchJson("/lemur/v3/generate/summary", {
114
- method: "POST",
115
- body: JSON.stringify(params),
116
- signal,
117
- });
118
- }
119
- questionAnswer(params, signal) {
120
- return this.fetchJson("/lemur/v3/generate/question-answer", {
121
- method: "POST",
122
- body: JSON.stringify(params),
123
- signal,
124
- });
128
+ /**
129
+ * Error thrown when a synchronous transcription request fails.
130
+ */
131
+ class SyncTranscriptError extends Error {
132
+ /**
133
+ * Create a new SyncTranscriptError.
134
+ * @param message - The human-readable error message.
135
+ * @param status - The HTTP status code of the failed request.
136
+ * @param errorCode - Machine-readable code — the snake_cased
137
+ * problem-details `title` from the server (e.g. `bad_audio`,
138
+ * `audio_too_large`, `capacity_exceeded`, `inference_timeout`).
139
+ * @param retryAfter - Seconds to wait before retrying, from the
140
+ * `Retry-After` header on 429/503 responses.
141
+ */
142
+ constructor(message, status, errorCode, retryAfter) {
143
+ super(message);
144
+ this.status = status;
145
+ this.errorCode = errorCode;
146
+ this.retryAfter = retryAfter;
147
+ this.name = "SyncTranscriptError";
125
148
  }
126
- actionItems(params, signal) {
127
- return this.fetchJson("/lemur/v3/generate/action-items", {
128
- method: "POST",
129
- body: JSON.stringify(params),
130
- signal,
131
- });
149
+ }
150
+
151
+ function getPath(path) {
152
+ if (path.startsWith("http"))
153
+ return null;
154
+ if (path.startsWith("https"))
155
+ return null;
156
+ if (path.startsWith("data:"))
157
+ return null;
158
+ if (path.startsWith("file://"))
159
+ return path.substring(7);
160
+ if (path.startsWith("file:"))
161
+ return path.substring(5);
162
+ return path;
163
+ }
164
+
165
+ // Canonical paths since the sync API gained a /v1 prefix (#18103); the
166
+ // unprefixed routes remain served for SDK versions that predate it.
167
+ const transcribeEndpoint = "/v1/transcribe";
168
+ const warmEndpoint = "/v1/warm";
169
+ const modelHeader = "X-AAI-Model";
170
+ // Kept above the server's 30 s deadline so the client doesn't race it.
171
+ const defaultTimeoutMs = 60_000;
172
+ const warmTimeoutMs = 10_000;
173
+ const maxPromptLength = 4096;
174
+ const maxKeytermsPromptLength = 2048;
175
+ const maxContextTurns = 100;
176
+ const maxContextLength = 4096;
177
+ // Extensions that signal raw S16LE PCM rather than a WAV container.
178
+ const pcmSuffixes = [".pcm", ".raw"];
179
+ /**
180
+ * The synchronous transcription service: audio in, transcript out,
181
+ * one request.
182
+ *
183
+ * Unlike `client.transcripts` (which submits a job to the async API and
184
+ * polls for completion), `SyncTranscriber` posts the audio to the sync
185
+ * API and returns the finished transcript in the HTTP response. There is no
186
+ * job id or status to poll. Accepts a local file path, raw audio bytes, a
187
+ * Blob, or a readable stream — but not a URL.
188
+ */
189
+ class SyncTranscriber extends BaseService {
190
+ /**
191
+ * Create a new synchronous transcription service.
192
+ * @param params - The parameters to use for the service.
193
+ */
194
+ constructor(params) {
195
+ super(params);
132
196
  }
133
- task(params, signal) {
134
- return this.fetchJson("/lemur/v3/generate/task", {
197
+ /**
198
+ * Transcribe audio and return the finished transcript in one request.
199
+ * @param audio - A local file path, raw audio bytes, a Blob, or a readable
200
+ * stream. Raw PCM also requires `sample_rate` and `channels` on the config.
201
+ * @param config - Options for this transcription request.
202
+ * @param options - Client-side options, such as the request timeout.
203
+ * @returns A promise that resolves to the finished transcript.
204
+ * @throws SyncTranscriptError when the request fails.
205
+ */
206
+ async transcribe(audio, config = {}, options = {}) {
207
+ const { bytes, filename, contentType } = await resolveAudio(audio, config);
208
+ const body = new FormData();
209
+ body.append("audio", new Blob([bytes], { type: contentType }), filename);
210
+ const configJson = buildConfigJson(config);
211
+ if (configJson) {
212
+ body.append("config", new Blob([JSON.stringify(configJson)], { type: "application/json" }));
213
+ }
214
+ const response = await this.fetchResponse(transcribeEndpoint, {
135
215
  method: "POST",
136
- body: JSON.stringify(params),
137
- signal,
216
+ body,
217
+ headers: { [modelHeader]: config.model ?? defaultSyncSpeechModel },
218
+ signal: AbortSignal.timeout(options.timeout ?? defaultTimeoutMs),
138
219
  });
139
- }
140
- getResponse(id, signal) {
141
- return this.fetchJson(`/lemur/v3/${id}`, { signal });
220
+ if (response.status !== 200)
221
+ throw await errorFromResponse(response);
222
+ return (await response.json());
142
223
  }
143
224
  /**
144
- * Delete the data for a previously submitted LeMUR request.
145
- * @param id - ID of the LeMUR request
146
- * @param signal - Optional AbortSignal to cancel the request
225
+ * Open the connection to the sync API ahead of time.
226
+ *
227
+ * The sync API is a single request/response, so a `transcribe()` that
228
+ * opens its connection on demand pays the full DNS + TCP + TLS handshake
229
+ * on the critical path. Call `warm()` as soon as you know audio is coming —
230
+ * typically while the clip is still being recorded — so the next
231
+ * `transcribe()` reuses the already-open connection. `warm()` is idempotent
232
+ * and cheap; call it shortly before `transcribe()` so the pooled connection
233
+ * hasn't idled out.
234
+ * @param params - Optionally the model to route the probe to, so the warmed
235
+ * connection lands on the same backend as the eventual transcription.
236
+ * @returns A promise that resolves to `true` once the connection is open
237
+ * (any HTTP response — even a non-200 — means the socket is
238
+ * established), or `false` if the connection could not be opened.
147
239
  */
148
- purgeRequestData(id, signal) {
149
- return this.fetchJson(`/lemur/v3/${id}`, {
150
- method: "DELETE",
151
- signal,
152
- });
240
+ async warm(params) {
241
+ try {
242
+ await this.fetchResponse(warmEndpoint, {
243
+ method: "GET",
244
+ headers: { [modelHeader]: params?.model ?? defaultSyncSpeechModel },
245
+ signal: AbortSignal.timeout(warmTimeoutMs),
246
+ });
247
+ return true;
248
+ }
249
+ catch {
250
+ return false;
251
+ }
153
252
  }
154
253
  }
155
-
156
- const { WritableStream } = typeof window !== "undefined"
157
- ? window
158
- : typeof global !== "undefined"
159
- ? global
160
- : globalThis;
161
-
162
- const factory = (url, params) => new ws(url, params);
254
+ /**
255
+ * Read the audio input into bytes and decide its multipart content type.
256
+ *
257
+ * PCM is selected when the source has a `.pcm`/`.raw` extension or when
258
+ * `sample_rate`/`channels` are set on the config (the fields the sync API
259
+ * requires only for raw PCM) — and both must then be present. Everything
260
+ * else is treated as a WAV container. URLs are rejected — the sync API has
261
+ * no URL ingestion.
262
+ */
263
+ async function resolveAudio(input, config) {
264
+ let bytes;
265
+ let filename;
266
+ let suffix = "";
267
+ if (typeof input === "string") {
268
+ if (/^https?:\/\//i.test(input)) {
269
+ throw new Error("SyncTranscriber does not accept URLs. Pass a local file path or " +
270
+ "audio bytes, or use client.transcripts for URL/async transcription.");
271
+ }
272
+ if (input.startsWith("data:")) {
273
+ bytes = dataUrlToBytes(input);
274
+ }
275
+ else {
276
+ const path = getPath(input) ?? input;
277
+ bytes = await readStream(await readFile());
278
+ filename = basename(path);
279
+ suffix = extname(filename);
280
+ }
281
+ }
282
+ else if (input instanceof Uint8Array) {
283
+ bytes = input;
284
+ }
285
+ else if (input instanceof ArrayBuffer) {
286
+ bytes = new Uint8Array(input);
287
+ }
288
+ else if (input instanceof Blob) {
289
+ bytes = new Uint8Array(await input.arrayBuffer());
290
+ // File instances carry a name; the File global itself needs Node >= 20.
291
+ const name = input.name;
292
+ if (name) {
293
+ filename = basename(name);
294
+ suffix = extname(filename);
295
+ }
296
+ }
297
+ else if (isWebReadableStream(input)) {
298
+ bytes = await readStream(input);
299
+ }
300
+ else if (isAsyncIterable(input)) {
301
+ bytes = await readAsyncIterable(input);
302
+ // fs.ReadStream carries the path it was opened from.
303
+ const path = input.path;
304
+ if (typeof path === "string") {
305
+ filename = basename(path);
306
+ suffix = extname(filename);
307
+ }
308
+ }
309
+ else {
310
+ throw new TypeError("unsupported audio input type");
311
+ }
312
+ const wantsPcm = config.sample_rate !== undefined || config.channels !== undefined;
313
+ const isPcm = pcmSuffixes.includes(suffix) || wantsPcm;
314
+ if (isPcm &&
315
+ (config.sample_rate === undefined || config.channels === undefined)) {
316
+ throw new Error("raw PCM audio requires both sample_rate and channels in the config");
317
+ }
318
+ const contentType = isPcm ? "audio/pcm" : "audio/wav";
319
+ if (!filename)
320
+ filename = isPcm ? "audio.pcm" : "audio.wav";
321
+ return { bytes, filename, contentType };
322
+ }
323
+ /**
324
+ * Serialize the config to the JSON `config` part, validating and normalizing
325
+ * field values to match the server's caps. The routing `model` is never
326
+ * included — it travels in the `X-AAI-Model` header. Returns `undefined`
327
+ * when there is nothing to send, so the part can be omitted entirely.
328
+ */
329
+ function buildConfigJson(config) {
330
+ if (config.prompt !== undefined && config.prompt.length > maxPromptLength) {
331
+ throw new Error(`prompt exceeds ${maxPromptLength} characters (got ${config.prompt.length})`);
332
+ }
333
+ const json = {};
334
+ if (config.prompt !== undefined)
335
+ json["prompt"] = config.prompt;
336
+ const keytermsPrompt = normalizeKeytermsPrompt(config.keyterms_prompt);
337
+ if (keytermsPrompt)
338
+ json["keyterms_prompt"] = keytermsPrompt;
339
+ const context = normalizeConversationContext(config.conversation_context);
340
+ if (context)
341
+ json["conversation_context"] = context;
342
+ if (config.language_codes !== undefined)
343
+ json["language_codes"] = config.language_codes;
344
+ if (config.sample_rate !== undefined)
345
+ json["sample_rate"] = config.sample_rate;
346
+ if (config.channels !== undefined)
347
+ json["channels"] = config.channels;
348
+ if (config.timestamps !== undefined)
349
+ json["timestamps"] = config.timestamps;
350
+ return Object.keys(json).length > 0 ? json : undefined;
351
+ }
352
+ function normalizeKeytermsPrompt(keytermsPrompt) {
353
+ if (!keytermsPrompt)
354
+ return undefined;
355
+ const terms = keytermsPrompt
356
+ .map((term) => term.trim())
357
+ .filter((term) => term.length > 0);
358
+ const total = terms.reduce((sum, term) => sum + term.length, 0);
359
+ if (total > maxKeytermsPromptLength) {
360
+ throw new Error(`keyterms_prompt exceeds ${maxKeytermsPromptLength} characters (got ${total})`);
361
+ }
362
+ return terms.length > 0 ? terms : undefined;
363
+ }
364
+ function normalizeConversationContext(context) {
365
+ if (context === undefined)
366
+ return undefined;
367
+ let turns = (typeof context === "string" ? [context] : context)
368
+ .map((turn) => turn.trim())
369
+ .filter((turn) => turn.length > 0);
370
+ let total = turns.reduce((sum, turn) => sum + turn.length, 0);
371
+ // Over-cap context is trimmed oldest-first, never rejected.
372
+ while (turns.length > 0 &&
373
+ (turns.length > maxContextTurns || total > maxContextLength)) {
374
+ total -= turns[0].length;
375
+ turns = turns.slice(1);
376
+ }
377
+ return turns.length > 0 ? turns : undefined;
378
+ }
379
+ /**
380
+ * Build a SyncTranscriptError from a non-200 response. The primary format
381
+ * is an RFC 9457 problem-details body (`status`/`title`/`detail`); legacy
382
+ * `{error_code, message}` and `{detail}`-only bodies are also accepted.
383
+ */
384
+ async function errorFromResponse(response) {
385
+ let errorCode;
386
+ let message;
387
+ const text = await response.text();
388
+ try {
389
+ const body = JSON.parse(text);
390
+ if (body && typeof body === "object" && !Array.isArray(body)) {
391
+ if (typeof body.error_code === "string")
392
+ errorCode = body.error_code;
393
+ if (errorCode === undefined && typeof body.title === "string") {
394
+ errorCode = body.title.toLowerCase().replace(/ /g, "_");
395
+ }
396
+ if (typeof body.detail === "string")
397
+ message = body.detail;
398
+ else if (typeof body.message === "string")
399
+ message = body.message;
400
+ }
401
+ }
402
+ catch {
403
+ if (text)
404
+ message = text;
405
+ }
406
+ if (!message) {
407
+ message = `sync transcription failed with status ${response.status}`;
408
+ }
409
+ const retryHeader = response.headers.get("retry-after");
410
+ const retryAfter = retryHeader && /^\d+$/.test(retryHeader)
411
+ ? parseInt(retryHeader, 10)
412
+ : undefined;
413
+ return new SyncTranscriptError(message, response.status, errorCode, retryAfter);
414
+ }
415
+ function basename(path) {
416
+ return path.split(/[\\/]/).pop() ?? path;
417
+ }
418
+ function extname(filename) {
419
+ const dotIndex = filename.lastIndexOf(".");
420
+ return dotIndex > 0 ? filename.slice(dotIndex).toLowerCase() : "";
421
+ }
422
+ function dataUrlToBytes(dataUrl) {
423
+ const base64 = dataUrl.split(",")[1];
424
+ const binary = atob(base64);
425
+ const bytes = new Uint8Array(binary.length);
426
+ for (let i = 0; i < binary.length; i++)
427
+ bytes[i] = binary.charCodeAt(i);
428
+ return bytes;
429
+ }
430
+ function isWebReadableStream(input) {
431
+ return typeof input?.getReader === "function";
432
+ }
433
+ function isAsyncIterable(input) {
434
+ return (typeof input?.[Symbol.asyncIterator] ===
435
+ "function");
436
+ }
437
+ async function readStream(stream) {
438
+ const chunks = [];
439
+ const reader = stream.getReader();
440
+ for (;;) {
441
+ const { done, value } = await reader.read();
442
+ if (done)
443
+ break;
444
+ chunks.push(value);
445
+ }
446
+ return concatChunks(chunks);
447
+ }
448
+ async function readAsyncIterable(iterable) {
449
+ const chunks = [];
450
+ for await (const chunk of iterable)
451
+ chunks.push(chunk);
452
+ return concatChunks(chunks);
453
+ }
454
+ function concatChunks(chunks) {
455
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
456
+ const bytes = new Uint8Array(total);
457
+ let offset = 0;
458
+ for (const chunk of chunks) {
459
+ bytes.set(chunk, offset);
460
+ offset += chunk.length;
461
+ }
462
+ return bytes;
463
+ }
163
464
 
164
465
  const RealtimeErrorType = {
165
466
  BadSampleRate: 4000,
@@ -265,6 +566,59 @@ const StreamingErrorMessages = {
265
566
  class StreamingError extends Error {
266
567
  }
267
568
 
569
+ class LemurService extends BaseService {
570
+ summary(params, signal) {
571
+ return this.fetchJson("/lemur/v3/generate/summary", {
572
+ method: "POST",
573
+ body: JSON.stringify(params),
574
+ signal,
575
+ });
576
+ }
577
+ questionAnswer(params, signal) {
578
+ return this.fetchJson("/lemur/v3/generate/question-answer", {
579
+ method: "POST",
580
+ body: JSON.stringify(params),
581
+ signal,
582
+ });
583
+ }
584
+ actionItems(params, signal) {
585
+ return this.fetchJson("/lemur/v3/generate/action-items", {
586
+ method: "POST",
587
+ body: JSON.stringify(params),
588
+ signal,
589
+ });
590
+ }
591
+ task(params, signal) {
592
+ return this.fetchJson("/lemur/v3/generate/task", {
593
+ method: "POST",
594
+ body: JSON.stringify(params),
595
+ signal,
596
+ });
597
+ }
598
+ getResponse(id, signal) {
599
+ return this.fetchJson(`/lemur/v3/${id}`, { signal });
600
+ }
601
+ /**
602
+ * Delete the data for a previously submitted LeMUR request.
603
+ * @param id - ID of the LeMUR request
604
+ * @param signal - Optional AbortSignal to cancel the request
605
+ */
606
+ purgeRequestData(id, signal) {
607
+ return this.fetchJson(`/lemur/v3/${id}`, {
608
+ method: "DELETE",
609
+ signal,
610
+ });
611
+ }
612
+ }
613
+
614
+ const { WritableStream } = typeof window !== "undefined"
615
+ ? window
616
+ : typeof global !== "undefined"
617
+ ? global
618
+ : globalThis;
619
+
620
+ const factory = (url, params) => new ws(url, params);
621
+
268
622
  const defaultRealtimeUrl = "wss://api.assemblyai.com/v2/realtime/ws";
269
623
  const forceEndOfUtteranceMessage = `{"force_end_utterance":true}`;
270
624
  const terminateSessionMessage$1 = `{"terminate_session":true}`;
@@ -509,20 +863,6 @@ class RealtimeTranscriberFactory extends BaseService {
509
863
  class RealtimeServiceFactory extends RealtimeTranscriberFactory {
510
864
  }
511
865
 
512
- function getPath(path) {
513
- if (path.startsWith("http"))
514
- return null;
515
- if (path.startsWith("https"))
516
- return null;
517
- if (path.startsWith("data:"))
518
- return null;
519
- if (path.startsWith("file://"))
520
- return path.substring(7);
521
- if (path.startsWith("file:"))
522
- return path.substring(5);
523
- return path;
524
- }
525
-
526
866
  class TranscriptService extends BaseService {
527
867
  constructor(params, files) {
528
868
  super(params);
@@ -750,12 +1090,6 @@ class TranscriptService extends BaseService {
750
1090
  }
751
1091
  }
752
1092
 
753
- const readFile = async function (
754
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
755
- path) {
756
- throw new Error("Interacting with the file system is not supported in this environment.");
757
- };
758
-
759
1093
  class FileService extends BaseService {
760
1094
  /**
761
1095
  * Upload a local file to AssemblyAI.
@@ -1036,6 +1370,10 @@ class StreamingTranscriber {
1036
1370
  if (!(this.token || this.apiKey)) {
1037
1371
  throw new Error("API key or temporary token is required.");
1038
1372
  }
1373
+ const isOpus = params.encoding === "opus" || params.encoding === "ogg_opus";
1374
+ if (params.sampleRate === undefined && (!isOpus || params.channels)) {
1375
+ throw new Error('`sampleRate` is required; it may only be omitted when `encoding` is "opus" or "ogg_opus" (the Opus stream is self-describing) and dual-channel mode is not used.');
1376
+ }
1039
1377
  if (params.channels) {
1040
1378
  if (params.channels.length !== 2) {
1041
1379
  throw new Error("StreamingTranscriber.channels must have exactly 2 entries.");
@@ -1061,10 +1399,12 @@ class StreamingTranscriber {
1061
1399
  "speaker-history") {
1062
1400
  this.speakerHistory = new Map();
1063
1401
  }
1064
- // 20 ms VAD frames at the transcriber's target sample rate.
1065
- this.vadFrameSamples = Math.max(1, Math.round(params.sampleRate * 0.02));
1066
- this.minChunkSamples = Math.max(1, Math.round(params.sampleRate * (MIN_CHUNK_MS / 1000)));
1067
- this.maxChunkSamples = Math.max(this.minChunkSamples, Math.round(params.sampleRate * (MAX_CHUNK_MS / 1000)));
1402
+ // 20 ms VAD frames at the transcriber's target sample rate. The
1403
+ // constructor check above guarantees sampleRate in dual-channel mode.
1404
+ const sampleRate = params.sampleRate;
1405
+ this.vadFrameSamples = Math.max(1, Math.round(sampleRate * 0.02));
1406
+ this.minChunkSamples = Math.max(1, Math.round(sampleRate * (MIN_CHUNK_MS / 1000)));
1407
+ this.maxChunkSamples = Math.max(this.minChunkSamples, Math.round(sampleRate * (MAX_CHUNK_MS / 1000)));
1068
1408
  this.channelBuffers = new Map(names.map((n) => [n, []]));
1069
1409
  this.channelSamplesReceived = new Map(names.map((n) => [n, 0]));
1070
1410
  this.channelVadFloatBuffers = new Map(names.map((n) => [n, new Float32Array(this.vadFrameSamples)]));
@@ -1082,7 +1422,9 @@ class StreamingTranscriber {
1082
1422
  if (this.token) {
1083
1423
  searchParams.set("token", this.token);
1084
1424
  }
1085
- searchParams.set("sample_rate", this.params.sampleRate.toString());
1425
+ if (this.params.sampleRate !== undefined) {
1426
+ searchParams.set("sample_rate", this.params.sampleRate.toString());
1427
+ }
1086
1428
  if (this.params.endOfTurnConfidenceThreshold) {
1087
1429
  searchParams.set("end_of_turn_confidence_threshold", this.params.endOfTurnConfidenceThreshold.toString());
1088
1430
  }
@@ -2009,6 +2351,7 @@ function float32ToPcm16(input) {
2009
2351
 
2010
2352
  const defaultBaseUrl = "https://api.assemblyai.com";
2011
2353
  const defaultStreamingUrl = "https://streaming.assemblyai.com";
2354
+ const defaultSyncUrl = "https://sync.assemblyai.com";
2012
2355
  class AssemblyAI {
2013
2356
  /**
2014
2357
  * Create a new AssemblyAI client.
@@ -2027,7 +2370,15 @@ class AssemblyAI {
2027
2370
  ...params,
2028
2371
  baseUrl: params.streamingBaseUrl || defaultStreamingUrl,
2029
2372
  });
2373
+ let syncBaseUrl = params.syncBaseUrl || defaultSyncUrl;
2374
+ if (syncBaseUrl.endsWith("/")) {
2375
+ syncBaseUrl = syncBaseUrl.slice(0, -1);
2376
+ }
2377
+ this.sync = new SyncTranscriber({
2378
+ ...params,
2379
+ baseUrl: syncBaseUrl,
2380
+ });
2030
2381
  }
2031
2382
  }
2032
2383
 
2033
- export { AssemblyAI, BrowserOnlyError, DualChannelCapture, EnergyVad, FileService, LemurService, LinearResampler, RealtimeService, RealtimeServiceFactory, RealtimeTranscriber, RealtimeTranscriberFactory, StreamingTranscriber, TranscriptService, VadTimeline, attributeTurn, attributeWord, float32ToPcm16, rollUpTurnChannel };
2384
+ export { AssemblyAI, BrowserOnlyError, DualChannelCapture, EnergyVad, FileService, LemurService, LinearResampler, RealtimeService, RealtimeServiceFactory, RealtimeTranscriber, RealtimeTranscriberFactory, StreamingTranscriber, SyncTranscriber, SyncTranscriptError, TranscriptService, VadTimeline, attributeTurn, attributeWord, defaultSyncSpeechModel, float32ToPcm16, rollUpTurnChannel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assemblyai",
3
- "version": "4.35.3",
3
+ "version": "4.36.3",
4
4
  "description": "The AssemblyAI JavaScript SDK provides an easy-to-use interface for interacting with the AssemblyAI API, which supports async and real-time transcription, as well as the latest LeMUR models.",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -19,26 +19,29 @@ export abstract class BaseService {
19
19
  this.userAgent = buildUserAgent(params.userAgent || {});
20
20
  }
21
21
  }
22
- protected async fetch(
22
+ protected async fetchResponse(
23
23
  input: string,
24
24
  init?: RequestInit | undefined,
25
25
  ): Promise<Response> {
26
26
  init = { ...DEFAULT_FETCH_INIT, ...init };
27
- let headers = {
27
+ let headers: Record<string, string> = {
28
28
  Authorization: this.params.apiKey,
29
- "Content-Type": "application/json",
30
29
  };
30
+ // FormData bodies must let fetch set the multipart boundary itself.
31
+ if (!(init.body instanceof FormData)) {
32
+ headers["Content-Type"] = "application/json";
33
+ }
31
34
  if (DEFAULT_FETCH_INIT?.headers)
32
35
  headers = { ...headers, ...DEFAULT_FETCH_INIT.headers };
33
- if (init?.headers) headers = { ...headers, ...init.headers };
36
+ if (init?.headers)
37
+ headers = { ...headers, ...(init.headers as Record<string, string>) };
34
38
 
35
39
  if (this.userAgent) {
36
- (headers as Record<string, string>)["User-Agent"] = this.userAgent;
40
+ headers["User-Agent"] = this.userAgent;
37
41
  if (conditions.browser || conditions.default) {
38
42
  // chromium browsers have a bug where the user agent can't be modified
39
43
  if (typeof window !== "undefined" && "chrome" in window) {
40
- (headers as Record<string, string>)["AssemblyAI-Agent"] =
41
- this.userAgent;
44
+ headers["AssemblyAI-Agent"] = this.userAgent;
42
45
  }
43
46
  }
44
47
  }
@@ -46,7 +49,14 @@ export abstract class BaseService {
46
49
 
47
50
  if (!input.startsWith("http")) input = this.params.baseUrl + input;
48
51
 
49
- const response = await fetch(input, init);
52
+ return await fetch(input, init);
53
+ }
54
+
55
+ protected async fetch(
56
+ input: string,
57
+ init?: RequestInit | undefined,
58
+ ): Promise<Response> {
59
+ const response = await this.fetchResponse(input, init);
50
60
 
51
61
  if (response.status >= 400) {
52
62
  let json: JsonError | undefined;