assemblyai 4.35.4 → 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.
package/dist/node.mjs CHANGED
@@ -1,7 +1,12 @@
1
- import { WritableStream } from 'stream/web';
2
- import ws from 'ws';
3
1
  import { createReadStream } from 'fs';
4
2
  import { Readable } from 'stream';
3
+ import { WritableStream } from 'stream/web';
4
+ import ws from 'ws';
5
+
6
+ /**
7
+ * The default speech model for synchronous transcription.
8
+ */
9
+ const defaultSyncSpeechModel = "universal-3-5-pro";
5
10
 
6
11
  /**
7
12
  * Thrown when `DualChannelCapture` is constructed in a non-browser environment
@@ -16,6 +21,8 @@ class BrowserOnlyError extends Error {
16
21
  }
17
22
  }
18
23
 
24
+ const readFile = async (path) => Readable.toWeb(createReadStream(path));
25
+
19
26
  const DEFAULT_FETCH_INIT = {
20
27
  cache: "no-store",
21
28
  };
@@ -33,7 +40,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
33
40
  defaultUserAgentString += navigator.userAgent;
34
41
  }
35
42
  const defaultUserAgent = {
36
- sdk: { name: "JavaScript", version: "4.35.4" },
43
+ sdk: { name: "JavaScript", version: "4.36.3" },
37
44
  };
38
45
  if (typeof process !== "undefined") {
39
46
  if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
@@ -72,12 +79,15 @@ class BaseService {
72
79
  this.userAgent = buildUserAgent(params.userAgent || {});
73
80
  }
74
81
  }
75
- async fetch(input, init) {
82
+ async fetchResponse(input, init) {
76
83
  init = { ...DEFAULT_FETCH_INIT, ...init };
77
84
  let headers = {
78
85
  Authorization: this.params.apiKey,
79
- "Content-Type": "application/json",
80
86
  };
87
+ // FormData bodies must let fetch set the multipart boundary itself.
88
+ if (!(init.body instanceof FormData)) {
89
+ headers["Content-Type"] = "application/json";
90
+ }
81
91
  if (DEFAULT_FETCH_INIT?.headers)
82
92
  headers = { ...headers, ...DEFAULT_FETCH_INIT.headers };
83
93
  if (init?.headers)
@@ -88,7 +98,10 @@ class BaseService {
88
98
  init.headers = headers;
89
99
  if (!input.startsWith("http"))
90
100
  input = this.params.baseUrl + input;
91
- const response = await fetch(input, init);
101
+ return await fetch(input, init);
102
+ }
103
+ async fetch(input, init) {
104
+ const response = await this.fetchResponse(input, init);
92
105
  if (response.status >= 400) {
93
106
  let json;
94
107
  const text = await response.text();
@@ -113,52 +126,342 @@ class BaseService {
113
126
  }
114
127
  }
115
128
 
116
- class LemurService extends BaseService {
117
- summary(params, signal) {
118
- return this.fetchJson("/lemur/v3/generate/summary", {
119
- method: "POST",
120
- body: JSON.stringify(params),
121
- signal,
122
- });
123
- }
124
- questionAnswer(params, signal) {
125
- return this.fetchJson("/lemur/v3/generate/question-answer", {
126
- method: "POST",
127
- body: JSON.stringify(params),
128
- signal,
129
- });
129
+ /**
130
+ * Error thrown when a synchronous transcription request fails.
131
+ */
132
+ class SyncTranscriptError extends Error {
133
+ /**
134
+ * Create a new SyncTranscriptError.
135
+ * @param message - The human-readable error message.
136
+ * @param status - The HTTP status code of the failed request.
137
+ * @param errorCode - Machine-readable code — the snake_cased
138
+ * problem-details `title` from the server (e.g. `bad_audio`,
139
+ * `audio_too_large`, `capacity_exceeded`, `inference_timeout`).
140
+ * @param retryAfter - Seconds to wait before retrying, from the
141
+ * `Retry-After` header on 429/503 responses.
142
+ */
143
+ constructor(message, status, errorCode, retryAfter) {
144
+ super(message);
145
+ this.status = status;
146
+ this.errorCode = errorCode;
147
+ this.retryAfter = retryAfter;
148
+ this.name = "SyncTranscriptError";
130
149
  }
131
- actionItems(params, signal) {
132
- return this.fetchJson("/lemur/v3/generate/action-items", {
133
- method: "POST",
134
- body: JSON.stringify(params),
135
- signal,
136
- });
150
+ }
151
+
152
+ function getPath(path) {
153
+ if (path.startsWith("http"))
154
+ return null;
155
+ if (path.startsWith("https"))
156
+ return null;
157
+ if (path.startsWith("data:"))
158
+ return null;
159
+ if (path.startsWith("file://"))
160
+ return path.substring(7);
161
+ if (path.startsWith("file:"))
162
+ return path.substring(5);
163
+ return path;
164
+ }
165
+
166
+ // Canonical paths since the sync API gained a /v1 prefix (#18103); the
167
+ // unprefixed routes remain served for SDK versions that predate it.
168
+ const transcribeEndpoint = "/v1/transcribe";
169
+ const warmEndpoint = "/v1/warm";
170
+ const modelHeader = "X-AAI-Model";
171
+ // Kept above the server's 30 s deadline so the client doesn't race it.
172
+ const defaultTimeoutMs = 60_000;
173
+ const warmTimeoutMs = 10_000;
174
+ const maxPromptLength = 4096;
175
+ const maxKeytermsPromptLength = 2048;
176
+ const maxContextTurns = 100;
177
+ const maxContextLength = 4096;
178
+ // Extensions that signal raw S16LE PCM rather than a WAV container.
179
+ const pcmSuffixes = [".pcm", ".raw"];
180
+ /**
181
+ * The synchronous transcription service: audio in, transcript out,
182
+ * one request.
183
+ *
184
+ * Unlike `client.transcripts` (which submits a job to the async API and
185
+ * polls for completion), `SyncTranscriber` posts the audio to the sync
186
+ * API and returns the finished transcript in the HTTP response. There is no
187
+ * job id or status to poll. Accepts a local file path, raw audio bytes, a
188
+ * Blob, or a readable stream — but not a URL.
189
+ */
190
+ class SyncTranscriber extends BaseService {
191
+ /**
192
+ * Create a new synchronous transcription service.
193
+ * @param params - The parameters to use for the service.
194
+ */
195
+ constructor(params) {
196
+ super(params);
137
197
  }
138
- task(params, signal) {
139
- return this.fetchJson("/lemur/v3/generate/task", {
198
+ /**
199
+ * Transcribe audio and return the finished transcript in one request.
200
+ * @param audio - A local file path, raw audio bytes, a Blob, or a readable
201
+ * stream. Raw PCM also requires `sample_rate` and `channels` on the config.
202
+ * @param config - Options for this transcription request.
203
+ * @param options - Client-side options, such as the request timeout.
204
+ * @returns A promise that resolves to the finished transcript.
205
+ * @throws SyncTranscriptError when the request fails.
206
+ */
207
+ async transcribe(audio, config = {}, options = {}) {
208
+ const { bytes, filename, contentType } = await resolveAudio(audio, config);
209
+ const body = new FormData();
210
+ body.append("audio", new Blob([bytes], { type: contentType }), filename);
211
+ const configJson = buildConfigJson(config);
212
+ if (configJson) {
213
+ body.append("config", new Blob([JSON.stringify(configJson)], { type: "application/json" }));
214
+ }
215
+ const response = await this.fetchResponse(transcribeEndpoint, {
140
216
  method: "POST",
141
- body: JSON.stringify(params),
142
- signal,
217
+ body,
218
+ headers: { [modelHeader]: config.model ?? defaultSyncSpeechModel },
219
+ signal: AbortSignal.timeout(options.timeout ?? defaultTimeoutMs),
143
220
  });
144
- }
145
- getResponse(id, signal) {
146
- return this.fetchJson(`/lemur/v3/${id}`, { signal });
221
+ if (response.status !== 200)
222
+ throw await errorFromResponse(response);
223
+ return (await response.json());
147
224
  }
148
225
  /**
149
- * Delete the data for a previously submitted LeMUR request.
150
- * @param id - ID of the LeMUR request
151
- * @param signal - Optional AbortSignal to cancel the request
226
+ * Open the connection to the sync API ahead of time.
227
+ *
228
+ * The sync API is a single request/response, so a `transcribe()` that
229
+ * opens its connection on demand pays the full DNS + TCP + TLS handshake
230
+ * on the critical path. Call `warm()` as soon as you know audio is coming —
231
+ * typically while the clip is still being recorded — so the next
232
+ * `transcribe()` reuses the already-open connection. `warm()` is idempotent
233
+ * and cheap; call it shortly before `transcribe()` so the pooled connection
234
+ * hasn't idled out.
235
+ * @param params - Optionally the model to route the probe to, so the warmed
236
+ * connection lands on the same backend as the eventual transcription.
237
+ * @returns A promise that resolves to `true` once the connection is open
238
+ * (any HTTP response — even a non-200 — means the socket is
239
+ * established), or `false` if the connection could not be opened.
152
240
  */
153
- purgeRequestData(id, signal) {
154
- return this.fetchJson(`/lemur/v3/${id}`, {
155
- method: "DELETE",
156
- signal,
157
- });
241
+ async warm(params) {
242
+ try {
243
+ await this.fetchResponse(warmEndpoint, {
244
+ method: "GET",
245
+ headers: { [modelHeader]: params?.model ?? defaultSyncSpeechModel },
246
+ signal: AbortSignal.timeout(warmTimeoutMs),
247
+ });
248
+ return true;
249
+ }
250
+ catch {
251
+ return false;
252
+ }
158
253
  }
159
254
  }
160
-
161
- const factory = (url, params) => new ws(url, params);
255
+ /**
256
+ * Read the audio input into bytes and decide its multipart content type.
257
+ *
258
+ * PCM is selected when the source has a `.pcm`/`.raw` extension or when
259
+ * `sample_rate`/`channels` are set on the config (the fields the sync API
260
+ * requires only for raw PCM) — and both must then be present. Everything
261
+ * else is treated as a WAV container. URLs are rejected — the sync API has
262
+ * no URL ingestion.
263
+ */
264
+ async function resolveAudio(input, config) {
265
+ let bytes;
266
+ let filename;
267
+ let suffix = "";
268
+ if (typeof input === "string") {
269
+ if (/^https?:\/\//i.test(input)) {
270
+ throw new Error("SyncTranscriber does not accept URLs. Pass a local file path or " +
271
+ "audio bytes, or use client.transcripts for URL/async transcription.");
272
+ }
273
+ if (input.startsWith("data:")) {
274
+ bytes = dataUrlToBytes(input);
275
+ }
276
+ else {
277
+ const path = getPath(input) ?? input;
278
+ bytes = await readStream(await readFile(path));
279
+ filename = basename(path);
280
+ suffix = extname(filename);
281
+ }
282
+ }
283
+ else if (input instanceof Uint8Array) {
284
+ bytes = input;
285
+ }
286
+ else if (input instanceof ArrayBuffer) {
287
+ bytes = new Uint8Array(input);
288
+ }
289
+ else if (input instanceof Blob) {
290
+ bytes = new Uint8Array(await input.arrayBuffer());
291
+ // File instances carry a name; the File global itself needs Node >= 20.
292
+ const name = input.name;
293
+ if (name) {
294
+ filename = basename(name);
295
+ suffix = extname(filename);
296
+ }
297
+ }
298
+ else if (isWebReadableStream(input)) {
299
+ bytes = await readStream(input);
300
+ }
301
+ else if (isAsyncIterable(input)) {
302
+ bytes = await readAsyncIterable(input);
303
+ // fs.ReadStream carries the path it was opened from.
304
+ const path = input.path;
305
+ if (typeof path === "string") {
306
+ filename = basename(path);
307
+ suffix = extname(filename);
308
+ }
309
+ }
310
+ else {
311
+ throw new TypeError("unsupported audio input type");
312
+ }
313
+ const wantsPcm = config.sample_rate !== undefined || config.channels !== undefined;
314
+ const isPcm = pcmSuffixes.includes(suffix) || wantsPcm;
315
+ if (isPcm &&
316
+ (config.sample_rate === undefined || config.channels === undefined)) {
317
+ throw new Error("raw PCM audio requires both sample_rate and channels in the config");
318
+ }
319
+ const contentType = isPcm ? "audio/pcm" : "audio/wav";
320
+ if (!filename)
321
+ filename = isPcm ? "audio.pcm" : "audio.wav";
322
+ return { bytes, filename, contentType };
323
+ }
324
+ /**
325
+ * Serialize the config to the JSON `config` part, validating and normalizing
326
+ * field values to match the server's caps. The routing `model` is never
327
+ * included — it travels in the `X-AAI-Model` header. Returns `undefined`
328
+ * when there is nothing to send, so the part can be omitted entirely.
329
+ */
330
+ function buildConfigJson(config) {
331
+ if (config.prompt !== undefined && config.prompt.length > maxPromptLength) {
332
+ throw new Error(`prompt exceeds ${maxPromptLength} characters (got ${config.prompt.length})`);
333
+ }
334
+ const json = {};
335
+ if (config.prompt !== undefined)
336
+ json["prompt"] = config.prompt;
337
+ const keytermsPrompt = normalizeKeytermsPrompt(config.keyterms_prompt);
338
+ if (keytermsPrompt)
339
+ json["keyterms_prompt"] = keytermsPrompt;
340
+ const context = normalizeConversationContext(config.conversation_context);
341
+ if (context)
342
+ json["conversation_context"] = context;
343
+ if (config.language_codes !== undefined)
344
+ json["language_codes"] = config.language_codes;
345
+ if (config.sample_rate !== undefined)
346
+ json["sample_rate"] = config.sample_rate;
347
+ if (config.channels !== undefined)
348
+ json["channels"] = config.channels;
349
+ if (config.timestamps !== undefined)
350
+ json["timestamps"] = config.timestamps;
351
+ return Object.keys(json).length > 0 ? json : undefined;
352
+ }
353
+ function normalizeKeytermsPrompt(keytermsPrompt) {
354
+ if (!keytermsPrompt)
355
+ return undefined;
356
+ const terms = keytermsPrompt
357
+ .map((term) => term.trim())
358
+ .filter((term) => term.length > 0);
359
+ const total = terms.reduce((sum, term) => sum + term.length, 0);
360
+ if (total > maxKeytermsPromptLength) {
361
+ throw new Error(`keyterms_prompt exceeds ${maxKeytermsPromptLength} characters (got ${total})`);
362
+ }
363
+ return terms.length > 0 ? terms : undefined;
364
+ }
365
+ function normalizeConversationContext(context) {
366
+ if (context === undefined)
367
+ return undefined;
368
+ let turns = (typeof context === "string" ? [context] : context)
369
+ .map((turn) => turn.trim())
370
+ .filter((turn) => turn.length > 0);
371
+ let total = turns.reduce((sum, turn) => sum + turn.length, 0);
372
+ // Over-cap context is trimmed oldest-first, never rejected.
373
+ while (turns.length > 0 &&
374
+ (turns.length > maxContextTurns || total > maxContextLength)) {
375
+ total -= turns[0].length;
376
+ turns = turns.slice(1);
377
+ }
378
+ return turns.length > 0 ? turns : undefined;
379
+ }
380
+ /**
381
+ * Build a SyncTranscriptError from a non-200 response. The primary format
382
+ * is an RFC 9457 problem-details body (`status`/`title`/`detail`); legacy
383
+ * `{error_code, message}` and `{detail}`-only bodies are also accepted.
384
+ */
385
+ async function errorFromResponse(response) {
386
+ let errorCode;
387
+ let message;
388
+ const text = await response.text();
389
+ try {
390
+ const body = JSON.parse(text);
391
+ if (body && typeof body === "object" && !Array.isArray(body)) {
392
+ if (typeof body.error_code === "string")
393
+ errorCode = body.error_code;
394
+ if (errorCode === undefined && typeof body.title === "string") {
395
+ errorCode = body.title.toLowerCase().replace(/ /g, "_");
396
+ }
397
+ if (typeof body.detail === "string")
398
+ message = body.detail;
399
+ else if (typeof body.message === "string")
400
+ message = body.message;
401
+ }
402
+ }
403
+ catch {
404
+ if (text)
405
+ message = text;
406
+ }
407
+ if (!message) {
408
+ message = `sync transcription failed with status ${response.status}`;
409
+ }
410
+ const retryHeader = response.headers.get("retry-after");
411
+ const retryAfter = retryHeader && /^\d+$/.test(retryHeader)
412
+ ? parseInt(retryHeader, 10)
413
+ : undefined;
414
+ return new SyncTranscriptError(message, response.status, errorCode, retryAfter);
415
+ }
416
+ function basename(path) {
417
+ return path.split(/[\\/]/).pop() ?? path;
418
+ }
419
+ function extname(filename) {
420
+ const dotIndex = filename.lastIndexOf(".");
421
+ return dotIndex > 0 ? filename.slice(dotIndex).toLowerCase() : "";
422
+ }
423
+ function dataUrlToBytes(dataUrl) {
424
+ const base64 = dataUrl.split(",")[1];
425
+ const binary = atob(base64);
426
+ const bytes = new Uint8Array(binary.length);
427
+ for (let i = 0; i < binary.length; i++)
428
+ bytes[i] = binary.charCodeAt(i);
429
+ return bytes;
430
+ }
431
+ function isWebReadableStream(input) {
432
+ return typeof input?.getReader === "function";
433
+ }
434
+ function isAsyncIterable(input) {
435
+ return (typeof input?.[Symbol.asyncIterator] ===
436
+ "function");
437
+ }
438
+ async function readStream(stream) {
439
+ const chunks = [];
440
+ const reader = stream.getReader();
441
+ for (;;) {
442
+ const { done, value } = await reader.read();
443
+ if (done)
444
+ break;
445
+ chunks.push(value);
446
+ }
447
+ return concatChunks(chunks);
448
+ }
449
+ async function readAsyncIterable(iterable) {
450
+ const chunks = [];
451
+ for await (const chunk of iterable)
452
+ chunks.push(chunk);
453
+ return concatChunks(chunks);
454
+ }
455
+ function concatChunks(chunks) {
456
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
457
+ const bytes = new Uint8Array(total);
458
+ let offset = 0;
459
+ for (const chunk of chunks) {
460
+ bytes.set(chunk, offset);
461
+ offset += chunk.length;
462
+ }
463
+ return bytes;
464
+ }
162
465
 
163
466
  const RealtimeErrorType = {
164
467
  BadSampleRate: 4000,
@@ -264,6 +567,53 @@ const StreamingErrorMessages = {
264
567
  class StreamingError extends Error {
265
568
  }
266
569
 
570
+ class LemurService extends BaseService {
571
+ summary(params, signal) {
572
+ return this.fetchJson("/lemur/v3/generate/summary", {
573
+ method: "POST",
574
+ body: JSON.stringify(params),
575
+ signal,
576
+ });
577
+ }
578
+ questionAnswer(params, signal) {
579
+ return this.fetchJson("/lemur/v3/generate/question-answer", {
580
+ method: "POST",
581
+ body: JSON.stringify(params),
582
+ signal,
583
+ });
584
+ }
585
+ actionItems(params, signal) {
586
+ return this.fetchJson("/lemur/v3/generate/action-items", {
587
+ method: "POST",
588
+ body: JSON.stringify(params),
589
+ signal,
590
+ });
591
+ }
592
+ task(params, signal) {
593
+ return this.fetchJson("/lemur/v3/generate/task", {
594
+ method: "POST",
595
+ body: JSON.stringify(params),
596
+ signal,
597
+ });
598
+ }
599
+ getResponse(id, signal) {
600
+ return this.fetchJson(`/lemur/v3/${id}`, { signal });
601
+ }
602
+ /**
603
+ * Delete the data for a previously submitted LeMUR request.
604
+ * @param id - ID of the LeMUR request
605
+ * @param signal - Optional AbortSignal to cancel the request
606
+ */
607
+ purgeRequestData(id, signal) {
608
+ return this.fetchJson(`/lemur/v3/${id}`, {
609
+ method: "DELETE",
610
+ signal,
611
+ });
612
+ }
613
+ }
614
+
615
+ const factory = (url, params) => new ws(url, params);
616
+
267
617
  const defaultRealtimeUrl = "wss://api.assemblyai.com/v2/realtime/ws";
268
618
  const forceEndOfUtteranceMessage = `{"force_end_utterance":true}`;
269
619
  const terminateSessionMessage$1 = `{"terminate_session":true}`;
@@ -508,20 +858,6 @@ class RealtimeTranscriberFactory extends BaseService {
508
858
  class RealtimeServiceFactory extends RealtimeTranscriberFactory {
509
859
  }
510
860
 
511
- function getPath(path) {
512
- if (path.startsWith("http"))
513
- return null;
514
- if (path.startsWith("https"))
515
- return null;
516
- if (path.startsWith("data:"))
517
- return null;
518
- if (path.startsWith("file://"))
519
- return path.substring(7);
520
- if (path.startsWith("file:"))
521
- return path.substring(5);
522
- return path;
523
- }
524
-
525
861
  class TranscriptService extends BaseService {
526
862
  constructor(params, files) {
527
863
  super(params);
@@ -749,8 +1085,6 @@ class TranscriptService extends BaseService {
749
1085
  }
750
1086
  }
751
1087
 
752
- const readFile = async (path) => Readable.toWeb(createReadStream(path));
753
-
754
1088
  class FileService extends BaseService {
755
1089
  /**
756
1090
  * Upload a local file to AssemblyAI.
@@ -2012,6 +2346,7 @@ function float32ToPcm16(input) {
2012
2346
 
2013
2347
  const defaultBaseUrl = "https://api.assemblyai.com";
2014
2348
  const defaultStreamingUrl = "https://streaming.assemblyai.com";
2349
+ const defaultSyncUrl = "https://sync.assemblyai.com";
2015
2350
  class AssemblyAI {
2016
2351
  /**
2017
2352
  * Create a new AssemblyAI client.
@@ -2030,7 +2365,15 @@ class AssemblyAI {
2030
2365
  ...params,
2031
2366
  baseUrl: params.streamingBaseUrl || defaultStreamingUrl,
2032
2367
  });
2368
+ let syncBaseUrl = params.syncBaseUrl || defaultSyncUrl;
2369
+ if (syncBaseUrl.endsWith("/")) {
2370
+ syncBaseUrl = syncBaseUrl.slice(0, -1);
2371
+ }
2372
+ this.sync = new SyncTranscriber({
2373
+ ...params,
2374
+ baseUrl: syncBaseUrl,
2375
+ });
2033
2376
  }
2034
2377
  }
2035
2378
 
2036
- export { AssemblyAI, BrowserOnlyError, DualChannelCapture, EnergyVad, FileService, LemurService, LinearResampler, RealtimeService, RealtimeServiceFactory, RealtimeTranscriber, RealtimeTranscriberFactory, StreamingTranscriber, TranscriptService, VadTimeline, attributeTurn, attributeWord, float32ToPcm16, rollUpTurnChannel };
2379
+ export { AssemblyAI, BrowserOnlyError, DualChannelCapture, EnergyVad, FileService, LemurService, LinearResampler, RealtimeService, RealtimeServiceFactory, RealtimeTranscriber, RealtimeTranscriberFactory, StreamingTranscriber, SyncTranscriber, SyncTranscriptError, TranscriptService, VadTimeline, attributeTurn, attributeWord, defaultSyncSpeechModel, float32ToPcm16, rollUpTurnChannel };
@@ -10,6 +10,7 @@ export declare abstract class BaseService {
10
10
  * @param params - The parameters to use for the service.
11
11
  */
12
12
  constructor(params: BaseServiceParams);
13
+ protected fetchResponse(input: string, init?: RequestInit | undefined): Promise<Response>;
13
14
  protected fetch(input: string, init?: RequestInit | undefined): Promise<Response>;
14
15
  protected fetchJson<T>(input: string, init?: RequestInit | undefined): Promise<T>;
15
16
  }
@@ -1,4 +1,6 @@
1
1
  import { BaseServiceParams } from "..";
2
+ import { SyncTranscriber } from "./sync";
3
+ import { SyncTranscriptError } from "../utils/errors";
2
4
  import { LemurService } from "./lemur";
3
5
  import { RealtimeTranscriber, RealtimeTranscriberFactory, RealtimeService, RealtimeServiceFactory } from "./realtime";
4
6
  import { TranscriptService } from "./transcripts";
@@ -25,10 +27,14 @@ declare class AssemblyAI {
25
27
  * The streaming service.
26
28
  */
27
29
  streaming: StreamingTranscriberFactory;
30
+ /**
31
+ * The synchronous transcription service.
32
+ */
33
+ sync: SyncTranscriber;
28
34
  /**
29
35
  * Create a new AssemblyAI client.
30
36
  * @param params - The parameters for the service, including the API key and base URL, if any.
31
37
  */
32
38
  constructor(params: BaseServiceParams);
33
39
  }
34
- export { AssemblyAI, LemurService, RealtimeTranscriberFactory, RealtimeTranscriber, RealtimeServiceFactory, RealtimeService, TranscriptService, FileService, StreamingTranscriber, DualChannelCapture, EnergyVad, LinearResampler, VadTimeline, attributeTurn, attributeWord, rollUpTurnChannel, float32ToPcm16, };
40
+ export { AssemblyAI, SyncTranscriber, SyncTranscriptError, LemurService, RealtimeTranscriberFactory, RealtimeTranscriber, RealtimeServiceFactory, RealtimeService, TranscriptService, FileService, StreamingTranscriber, DualChannelCapture, EnergyVad, LinearResampler, VadTimeline, attributeTurn, attributeWord, rollUpTurnChannel, float32ToPcm16, };
@@ -0,0 +1 @@
1
+ export * from "./service";
@@ -0,0 +1,48 @@
1
+ import { BaseService } from "../base";
2
+ import { BaseServiceParams, SyncAudioInput, SyncTranscribeOptions, SyncTranscriptResponse, SyncTranscriptionConfig } from "../..";
3
+ /**
4
+ * The synchronous transcription service: audio in, transcript out,
5
+ * one request.
6
+ *
7
+ * Unlike `client.transcripts` (which submits a job to the async API and
8
+ * polls for completion), `SyncTranscriber` posts the audio to the sync
9
+ * API and returns the finished transcript in the HTTP response. There is no
10
+ * job id or status to poll. Accepts a local file path, raw audio bytes, a
11
+ * Blob, or a readable stream — but not a URL.
12
+ */
13
+ export declare class SyncTranscriber extends BaseService {
14
+ /**
15
+ * Create a new synchronous transcription service.
16
+ * @param params - The parameters to use for the service.
17
+ */
18
+ constructor(params: BaseServiceParams);
19
+ /**
20
+ * Transcribe audio and return the finished transcript in one request.
21
+ * @param audio - A local file path, raw audio bytes, a Blob, or a readable
22
+ * stream. Raw PCM also requires `sample_rate` and `channels` on the config.
23
+ * @param config - Options for this transcription request.
24
+ * @param options - Client-side options, such as the request timeout.
25
+ * @returns A promise that resolves to the finished transcript.
26
+ * @throws SyncTranscriptError when the request fails.
27
+ */
28
+ transcribe(audio: SyncAudioInput, config?: SyncTranscriptionConfig, options?: SyncTranscribeOptions): Promise<SyncTranscriptResponse>;
29
+ /**
30
+ * Open the connection to the sync API ahead of time.
31
+ *
32
+ * The sync API is a single request/response, so a `transcribe()` that
33
+ * opens its connection on demand pays the full DNS + TCP + TLS handshake
34
+ * on the critical path. Call `warm()` as soon as you know audio is coming —
35
+ * typically while the clip is still being recorded — so the next
36
+ * `transcribe()` reuses the already-open connection. `warm()` is idempotent
37
+ * and cheap; call it shortly before `transcribe()` so the pooled connection
38
+ * hasn't idled out.
39
+ * @param params - Optionally the model to route the probe to, so the warmed
40
+ * connection lands on the same backend as the eventual transcription.
41
+ * @returns A promise that resolves to `true` once the connection is open
42
+ * (any HTTP response — even a non-200 — means the socket is
43
+ * established), or `false` if the connection could not be opened.
44
+ */
45
+ warm(params?: {
46
+ model?: string;
47
+ }): Promise<boolean>;
48
+ }
@@ -1312,7 +1312,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
1312
1312
  defaultUserAgentString += navigator.userAgent;
1313
1313
  }
1314
1314
  const defaultUserAgent = {
1315
- sdk: { name: "JavaScript", version: "4.35.4" },
1315
+ sdk: { name: "JavaScript", version: "4.36.3" },
1316
1316
  };
1317
1317
  if (typeof process !== "undefined") {
1318
1318
  if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
@@ -1351,12 +1351,15 @@ class BaseService {
1351
1351
  this.userAgent = buildUserAgent(params.userAgent || {});
1352
1352
  }
1353
1353
  }
1354
- async fetch(input, init) {
1354
+ async fetchResponse(input, init) {
1355
1355
  init = { ...DEFAULT_FETCH_INIT, ...init };
1356
1356
  let headers = {
1357
1357
  Authorization: this.params.apiKey,
1358
- "Content-Type": "application/json",
1359
1358
  };
1359
+ // FormData bodies must let fetch set the multipart boundary itself.
1360
+ if (!(init.body instanceof FormData)) {
1361
+ headers["Content-Type"] = "application/json";
1362
+ }
1360
1363
  if (DEFAULT_FETCH_INIT?.headers)
1361
1364
  headers = { ...headers, ...DEFAULT_FETCH_INIT.headers };
1362
1365
  if (init?.headers)
@@ -1366,15 +1369,17 @@ class BaseService {
1366
1369
  {
1367
1370
  // chromium browsers have a bug where the user agent can't be modified
1368
1371
  if (typeof window !== "undefined" && "chrome" in window) {
1369
- headers["AssemblyAI-Agent"] =
1370
- this.userAgent;
1372
+ headers["AssemblyAI-Agent"] = this.userAgent;
1371
1373
  }
1372
1374
  }
1373
1375
  }
1374
1376
  init.headers = headers;
1375
1377
  if (!input.startsWith("http"))
1376
1378
  input = this.params.baseUrl + input;
1377
- const response = await fetch(input, init);
1379
+ return await fetch(input, init);
1380
+ }
1381
+ async fetch(input, init) {
1382
+ const response = await this.fetchResponse(input, init);
1378
1383
  if (response.status >= 400) {
1379
1384
  let json;
1380
1385
  const text = await response.text();