assemblyai 4.35.4 → 4.36.4

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 (42) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +98 -0
  3. package/dist/assemblyai.streaming.umd.js +28 -11
  4. package/dist/assemblyai.streaming.umd.min.js +1 -1
  5. package/dist/assemblyai.umd.js +488 -83
  6. package/dist/assemblyai.umd.min.js +1 -1
  7. package/dist/browser.mjs +430 -78
  8. package/dist/bun.mjs +420 -67
  9. package/dist/deno.mjs +420 -67
  10. package/dist/index.cjs +481 -76
  11. package/dist/index.mjs +479 -77
  12. package/dist/node.cjs +418 -62
  13. package/dist/node.mjs +416 -63
  14. package/dist/services/base.d.ts +1 -0
  15. package/dist/services/index.d.ts +7 -1
  16. package/dist/services/streaming/service.d.ts +2 -1
  17. package/dist/services/sync/index.d.ts +1 -0
  18. package/dist/services/sync/service.d.ts +48 -0
  19. package/dist/streaming.browser.mjs +24 -9
  20. package/dist/streaming.cjs +27 -10
  21. package/dist/streaming.mjs +27 -10
  22. package/dist/types/asyncapi.generated.d.ts +1 -1
  23. package/dist/types/index.d.ts +1 -0
  24. package/dist/types/services/index.d.ts +1 -0
  25. package/dist/types/streaming/index.d.ts +14 -4
  26. package/dist/types/sync/index.d.ts +133 -0
  27. package/dist/utils/errors/index.d.ts +1 -0
  28. package/dist/utils/errors/sync.d.ts +20 -0
  29. package/dist/workerd.mjs +424 -71
  30. package/package.json +1 -1
  31. package/src/services/base.ts +18 -8
  32. package/src/services/index.ts +19 -0
  33. package/src/services/streaming/service.ts +22 -3
  34. package/src/services/sync/index.ts +1 -0
  35. package/src/services/sync/service.ts +369 -0
  36. package/src/types/asyncapi.generated.ts +6 -1
  37. package/src/types/index.ts +1 -0
  38. package/src/types/services/index.ts +1 -0
  39. package/src/types/streaming/index.ts +16 -3
  40. package/src/types/sync/index.ts +145 -0
  41. package/src/utils/errors/index.ts +2 -0
  42. package/src/utils/errors/sync.ts +25 -0
@@ -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 {
4
6
  RealtimeTranscriber,
@@ -23,6 +25,7 @@ import {
23
25
 
24
26
  const defaultBaseUrl = "https://api.assemblyai.com";
25
27
  const defaultStreamingUrl = "https://streaming.assemblyai.com";
28
+ const defaultSyncUrl = "https://sync.assemblyai.com";
26
29
 
27
30
  class AssemblyAI {
28
31
  /**
@@ -50,6 +53,11 @@ class AssemblyAI {
50
53
  */
51
54
  public streaming: StreamingTranscriberFactory;
52
55
 
56
+ /**
57
+ * The synchronous transcription service.
58
+ */
59
+ public sync: SyncTranscriber;
60
+
53
61
  /**
54
62
  * Create a new AssemblyAI client.
55
63
  * @param params - The parameters for the service, including the API key and base URL, if any.
@@ -69,11 +77,22 @@ class AssemblyAI {
69
77
  ...params,
70
78
  baseUrl: params.streamingBaseUrl || defaultStreamingUrl,
71
79
  });
80
+
81
+ let syncBaseUrl = params.syncBaseUrl || defaultSyncUrl;
82
+ if (syncBaseUrl.endsWith("/")) {
83
+ syncBaseUrl = syncBaseUrl.slice(0, -1);
84
+ }
85
+ this.sync = new SyncTranscriber({
86
+ ...params,
87
+ baseUrl: syncBaseUrl,
88
+ });
72
89
  }
73
90
  }
74
91
 
75
92
  export {
76
93
  AssemblyAI,
94
+ SyncTranscriber,
95
+ SyncTranscriptError,
77
96
  LemurService,
78
97
  RealtimeTranscriberFactory,
79
98
  RealtimeTranscriber,
@@ -20,6 +20,7 @@ import {
20
20
  StreamingForceEndpoint,
21
21
  StreamingKeepAlive,
22
22
  WarningEvent,
23
+ HeartbeatEvent,
23
24
  } from "../..";
24
25
  import type { VadDetector, VadFrame } from "../../types/streaming/dual-channel";
25
26
  import { EnergyVad } from "./energy-vad";
@@ -164,10 +165,16 @@ export class StreamingTranscriber {
164
165
  throw new Error("API key or temporary token is required.");
165
166
  }
166
167
 
167
- const isOpus = params.encoding === "opus" || params.encoding === "ogg_opus";
168
- if (params.sampleRate === undefined && (!isOpus || params.channels)) {
168
+ const isSelfDescribing =
169
+ params.encoding === "opus" ||
170
+ params.encoding === "ogg_opus" ||
171
+ params.encoding === "aac";
172
+ if (
173
+ params.sampleRate === undefined &&
174
+ (!isSelfDescribing || params.channels)
175
+ ) {
169
176
  throw new Error(
170
- '`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.',
177
+ '`sampleRate` is required; it may only be omitted when `encoding` is "opus", "ogg_opus", or "aac" (these streams are self-describing) and dual-channel mode is not used.',
171
178
  );
172
179
  }
173
180
 
@@ -283,6 +290,13 @@ export class StreamingTranscriber {
283
290
  searchParams.set("format_turns", this.params.formatTurns.toString());
284
291
  }
285
292
 
293
+ if (this.params.sessionHeartbeat !== undefined) {
294
+ searchParams.set(
295
+ "session_heartbeat",
296
+ this.params.sessionHeartbeat.toString(),
297
+ );
298
+ }
299
+
286
300
  if (this.params.encoding) {
287
301
  searchParams.set("encoding", this.params.encoding.toString());
288
302
  }
@@ -474,6 +488,7 @@ export class StreamingTranscriber {
474
488
  listener: (event: SpeakerRevisionEvent) => void,
475
489
  ): void;
476
490
  on(event: "warning", listener: (event: WarningEvent) => void): void;
491
+ on(event: "heartbeat", listener: (event: HeartbeatEvent) => void): void;
477
492
  on(event: "vad", listener: (event: VadFrame) => void): void;
478
493
  on(event: "error", listener: (error: Error) => void): void;
479
494
  on(event: "close", listener: (code: number, reason: string) => void): void;
@@ -693,6 +708,10 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
693
708
  this.listeners.warning?.(warning);
694
709
  break;
695
710
  }
711
+ case "Heartbeat": {
712
+ this.listeners.heartbeat?.(message);
713
+ break;
714
+ }
696
715
  case "Termination": {
697
716
  this.sessionTerminatedResolve?.();
698
717
  break;
@@ -0,0 +1 @@
1
+ export * from "./service";
@@ -0,0 +1,369 @@
1
+ import { readFile } from "#fs";
2
+ import { BaseService } from "../base";
3
+ import {
4
+ BaseServiceParams,
5
+ SyncAudioInput,
6
+ SyncTranscribeOptions,
7
+ SyncTranscriptResponse,
8
+ SyncTranscriptionConfig,
9
+ } from "../..";
10
+ import { defaultSyncSpeechModel } from "../../types/sync";
11
+ import { SyncTranscriptError } from "../../utils/errors/sync";
12
+ import { getPath } from "../../utils/path";
13
+
14
+ // Canonical paths since the sync API gained a /v1 prefix (#18103); the
15
+ // unprefixed routes remain served for SDK versions that predate it.
16
+ const transcribeEndpoint = "/v1/transcribe";
17
+ const warmEndpoint = "/v1/warm";
18
+ const modelHeader = "X-AAI-Model";
19
+ // Kept above the server's 30 s deadline so the client doesn't race it.
20
+ const defaultTimeoutMs = 60_000;
21
+ const warmTimeoutMs = 10_000;
22
+ const maxPromptLength = 4096;
23
+ const maxKeytermsPromptLength = 2048;
24
+ const maxContextTurns = 100;
25
+ const maxContextLength = 4096;
26
+ // Extensions that signal raw S16LE PCM rather than a WAV container.
27
+ const pcmSuffixes = [".pcm", ".raw"];
28
+
29
+ /**
30
+ * The synchronous transcription service: audio in, transcript out,
31
+ * one request.
32
+ *
33
+ * Unlike `client.transcripts` (which submits a job to the async API and
34
+ * polls for completion), `SyncTranscriber` posts the audio to the sync
35
+ * API and returns the finished transcript in the HTTP response. There is no
36
+ * job id or status to poll. Accepts a local file path, raw audio bytes, a
37
+ * Blob, or a readable stream — but not a URL.
38
+ */
39
+ export class SyncTranscriber extends BaseService {
40
+ /**
41
+ * Create a new synchronous transcription service.
42
+ * @param params - The parameters to use for the service.
43
+ */
44
+ constructor(params: BaseServiceParams) {
45
+ super(params);
46
+ }
47
+
48
+ /**
49
+ * Transcribe audio and return the finished transcript in one request.
50
+ * @param audio - A local file path, raw audio bytes, a Blob, or a readable
51
+ * stream. Raw PCM also requires `sample_rate` and `channels` on the config.
52
+ * @param config - Options for this transcription request.
53
+ * @param options - Client-side options, such as the request timeout.
54
+ * @returns A promise that resolves to the finished transcript.
55
+ * @throws SyncTranscriptError when the request fails.
56
+ */
57
+ async transcribe(
58
+ audio: SyncAudioInput,
59
+ config: SyncTranscriptionConfig = {},
60
+ options: SyncTranscribeOptions = {},
61
+ ): Promise<SyncTranscriptResponse> {
62
+ const { bytes, filename, contentType } = await resolveAudio(audio, config);
63
+
64
+ const body = new FormData();
65
+ body.append(
66
+ "audio",
67
+ new Blob([bytes as BlobPart], { type: contentType }),
68
+ filename,
69
+ );
70
+ const configJson = buildConfigJson(config);
71
+ if (configJson) {
72
+ body.append(
73
+ "config",
74
+ new Blob([JSON.stringify(configJson)], { type: "application/json" }),
75
+ );
76
+ }
77
+
78
+ const response = await this.fetchResponse(transcribeEndpoint, {
79
+ method: "POST",
80
+ body,
81
+ headers: { [modelHeader]: config.model ?? defaultSyncSpeechModel },
82
+ signal: AbortSignal.timeout(options.timeout ?? defaultTimeoutMs),
83
+ });
84
+ if (response.status !== 200) throw await errorFromResponse(response);
85
+ return (await response.json()) as SyncTranscriptResponse;
86
+ }
87
+
88
+ /**
89
+ * Open the connection to the sync API ahead of time.
90
+ *
91
+ * The sync API is a single request/response, so a `transcribe()` that
92
+ * opens its connection on demand pays the full DNS + TCP + TLS handshake
93
+ * on the critical path. Call `warm()` as soon as you know audio is coming —
94
+ * typically while the clip is still being recorded — so the next
95
+ * `transcribe()` reuses the already-open connection. `warm()` is idempotent
96
+ * and cheap; call it shortly before `transcribe()` so the pooled connection
97
+ * hasn't idled out.
98
+ * @param params - Optionally the model to route the probe to, so the warmed
99
+ * connection lands on the same backend as the eventual transcription.
100
+ * @returns A promise that resolves to `true` once the connection is open
101
+ * (any HTTP response — even a non-200 — means the socket is
102
+ * established), or `false` if the connection could not be opened.
103
+ */
104
+ async warm(params?: { model?: string }): Promise<boolean> {
105
+ try {
106
+ await this.fetchResponse(warmEndpoint, {
107
+ method: "GET",
108
+ headers: { [modelHeader]: params?.model ?? defaultSyncSpeechModel },
109
+ signal: AbortSignal.timeout(warmTimeoutMs),
110
+ });
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+ }
117
+
118
+ type ResolvedAudio = {
119
+ bytes: Uint8Array;
120
+ filename: string;
121
+ contentType: string;
122
+ };
123
+
124
+ /**
125
+ * Read the audio input into bytes and decide its multipart content type.
126
+ *
127
+ * PCM is selected when the source has a `.pcm`/`.raw` extension or when
128
+ * `sample_rate`/`channels` are set on the config (the fields the sync API
129
+ * requires only for raw PCM) — and both must then be present. Everything
130
+ * else is treated as a WAV container. URLs are rejected — the sync API has
131
+ * no URL ingestion.
132
+ */
133
+ async function resolveAudio(
134
+ input: SyncAudioInput,
135
+ config: SyncTranscriptionConfig,
136
+ ): Promise<ResolvedAudio> {
137
+ let bytes: Uint8Array;
138
+ let filename: string | undefined;
139
+ let suffix = "";
140
+
141
+ if (typeof input === "string") {
142
+ if (/^https?:\/\//i.test(input)) {
143
+ throw new Error(
144
+ "SyncTranscriber does not accept URLs. Pass a local file path or " +
145
+ "audio bytes, or use client.transcripts for URL/async transcription.",
146
+ );
147
+ }
148
+ if (input.startsWith("data:")) {
149
+ bytes = dataUrlToBytes(input);
150
+ } else {
151
+ const path = getPath(input) ?? input;
152
+ bytes = await readStream(await readFile(path));
153
+ filename = basename(path);
154
+ suffix = extname(filename);
155
+ }
156
+ } else if (input instanceof Uint8Array) {
157
+ bytes = input;
158
+ } else if (input instanceof ArrayBuffer) {
159
+ bytes = new Uint8Array(input);
160
+ } else if (input instanceof Blob) {
161
+ bytes = new Uint8Array(await input.arrayBuffer());
162
+ // File instances carry a name; the File global itself needs Node >= 20.
163
+ const name = (input as { name?: string }).name;
164
+ if (name) {
165
+ filename = basename(name);
166
+ suffix = extname(filename);
167
+ }
168
+ } else if (isWebReadableStream(input)) {
169
+ bytes = await readStream(input);
170
+ } else if (isAsyncIterable(input)) {
171
+ bytes = await readAsyncIterable(input);
172
+ // fs.ReadStream carries the path it was opened from.
173
+ const path = (input as { path?: string | Buffer }).path;
174
+ if (typeof path === "string") {
175
+ filename = basename(path);
176
+ suffix = extname(filename);
177
+ }
178
+ } else {
179
+ throw new TypeError("unsupported audio input type");
180
+ }
181
+
182
+ const wantsPcm =
183
+ config.sample_rate !== undefined || config.channels !== undefined;
184
+ const isPcm = pcmSuffixes.includes(suffix) || wantsPcm;
185
+ if (
186
+ isPcm &&
187
+ (config.sample_rate === undefined || config.channels === undefined)
188
+ ) {
189
+ throw new Error(
190
+ "raw PCM audio requires both sample_rate and channels in the config",
191
+ );
192
+ }
193
+
194
+ const contentType = isPcm ? "audio/pcm" : "audio/wav";
195
+ if (!filename) filename = isPcm ? "audio.pcm" : "audio.wav";
196
+
197
+ return { bytes, filename, contentType };
198
+ }
199
+
200
+ /**
201
+ * Serialize the config to the JSON `config` part, validating and normalizing
202
+ * field values to match the server's caps. The routing `model` is never
203
+ * included — it travels in the `X-AAI-Model` header. Returns `undefined`
204
+ * when there is nothing to send, so the part can be omitted entirely.
205
+ */
206
+ function buildConfigJson(
207
+ config: SyncTranscriptionConfig,
208
+ ): Record<string, unknown> | undefined {
209
+ if (config.prompt !== undefined && config.prompt.length > maxPromptLength) {
210
+ throw new Error(
211
+ `prompt exceeds ${maxPromptLength} characters (got ${config.prompt.length})`,
212
+ );
213
+ }
214
+
215
+ const json: Record<string, unknown> = {};
216
+ if (config.prompt !== undefined) json["prompt"] = config.prompt;
217
+ const keytermsPrompt = normalizeKeytermsPrompt(config.keyterms_prompt);
218
+ if (keytermsPrompt) json["keyterms_prompt"] = keytermsPrompt;
219
+ const context = normalizeConversationContext(config.conversation_context);
220
+ if (context) json["conversation_context"] = context;
221
+ if (config.language_codes !== undefined)
222
+ json["language_codes"] = config.language_codes;
223
+ if (config.sample_rate !== undefined)
224
+ json["sample_rate"] = config.sample_rate;
225
+ if (config.channels !== undefined) json["channels"] = config.channels;
226
+ if (config.timestamps !== undefined) json["timestamps"] = config.timestamps;
227
+
228
+ return Object.keys(json).length > 0 ? json : undefined;
229
+ }
230
+
231
+ function normalizeKeytermsPrompt(
232
+ keytermsPrompt?: string[],
233
+ ): string[] | undefined {
234
+ if (!keytermsPrompt) return undefined;
235
+ const terms = keytermsPrompt
236
+ .map((term) => term.trim())
237
+ .filter((term) => term.length > 0);
238
+ const total = terms.reduce((sum, term) => sum + term.length, 0);
239
+ if (total > maxKeytermsPromptLength) {
240
+ throw new Error(
241
+ `keyterms_prompt exceeds ${maxKeytermsPromptLength} characters (got ${total})`,
242
+ );
243
+ }
244
+ return terms.length > 0 ? terms : undefined;
245
+ }
246
+
247
+ function normalizeConversationContext(
248
+ context?: string | string[],
249
+ ): string[] | undefined {
250
+ if (context === undefined) return undefined;
251
+ let turns = (typeof context === "string" ? [context] : context)
252
+ .map((turn) => turn.trim())
253
+ .filter((turn) => turn.length > 0);
254
+ let total = turns.reduce((sum, turn) => sum + turn.length, 0);
255
+ // Over-cap context is trimmed oldest-first, never rejected.
256
+ while (
257
+ turns.length > 0 &&
258
+ (turns.length > maxContextTurns || total > maxContextLength)
259
+ ) {
260
+ total -= turns[0].length;
261
+ turns = turns.slice(1);
262
+ }
263
+ return turns.length > 0 ? turns : undefined;
264
+ }
265
+
266
+ /**
267
+ * Build a SyncTranscriptError from a non-200 response. The primary format
268
+ * is an RFC 9457 problem-details body (`status`/`title`/`detail`); legacy
269
+ * `{error_code, message}` and `{detail}`-only bodies are also accepted.
270
+ */
271
+ async function errorFromResponse(
272
+ response: Response,
273
+ ): Promise<SyncTranscriptError> {
274
+ let errorCode: string | undefined;
275
+ let message: string | undefined;
276
+
277
+ const text = await response.text();
278
+ try {
279
+ const body = JSON.parse(text);
280
+ if (body && typeof body === "object" && !Array.isArray(body)) {
281
+ if (typeof body.error_code === "string") errorCode = body.error_code;
282
+ if (errorCode === undefined && typeof body.title === "string") {
283
+ errorCode = body.title.toLowerCase().replace(/ /g, "_");
284
+ }
285
+ if (typeof body.detail === "string") message = body.detail;
286
+ else if (typeof body.message === "string") message = body.message;
287
+ }
288
+ } catch {
289
+ if (text) message = text;
290
+ }
291
+ if (!message) {
292
+ message = `sync transcription failed with status ${response.status}`;
293
+ }
294
+
295
+ const retryHeader = response.headers.get("retry-after");
296
+ const retryAfter =
297
+ retryHeader && /^\d+$/.test(retryHeader)
298
+ ? parseInt(retryHeader, 10)
299
+ : undefined;
300
+
301
+ return new SyncTranscriptError(
302
+ message,
303
+ response.status,
304
+ errorCode,
305
+ retryAfter,
306
+ );
307
+ }
308
+
309
+ function basename(path: string): string {
310
+ return path.split(/[\\/]/).pop() ?? path;
311
+ }
312
+
313
+ function extname(filename: string): string {
314
+ const dotIndex = filename.lastIndexOf(".");
315
+ return dotIndex > 0 ? filename.slice(dotIndex).toLowerCase() : "";
316
+ }
317
+
318
+ function dataUrlToBytes(dataUrl: string): Uint8Array {
319
+ const base64 = dataUrl.split(",")[1];
320
+ const binary = atob(base64);
321
+ const bytes = new Uint8Array(binary.length);
322
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
323
+ return bytes;
324
+ }
325
+
326
+ function isWebReadableStream(
327
+ input: unknown,
328
+ ): input is ReadableStream<Uint8Array> {
329
+ return typeof (input as ReadableStream<Uint8Array>)?.getReader === "function";
330
+ }
331
+
332
+ function isAsyncIterable(input: unknown): input is AsyncIterable<Uint8Array> {
333
+ return (
334
+ typeof (input as AsyncIterable<Uint8Array>)?.[Symbol.asyncIterator] ===
335
+ "function"
336
+ );
337
+ }
338
+
339
+ async function readStream(
340
+ stream: ReadableStream<Uint8Array>,
341
+ ): Promise<Uint8Array> {
342
+ const chunks: Uint8Array[] = [];
343
+ const reader = stream.getReader();
344
+ for (;;) {
345
+ const { done, value } = await reader.read();
346
+ if (done) break;
347
+ chunks.push(value);
348
+ }
349
+ return concatChunks(chunks);
350
+ }
351
+
352
+ async function readAsyncIterable(
353
+ iterable: AsyncIterable<Uint8Array>,
354
+ ): Promise<Uint8Array> {
355
+ const chunks: Uint8Array[] = [];
356
+ for await (const chunk of iterable) chunks.push(chunk);
357
+ return concatChunks(chunks);
358
+ }
359
+
360
+ function concatChunks(chunks: Uint8Array[]): Uint8Array {
361
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
362
+ const bytes = new Uint8Array(total);
363
+ let offset = 0;
364
+ for (const chunk of chunks) {
365
+ bytes.set(chunk, offset);
366
+ offset += chunk.length;
367
+ }
368
+ return bytes;
369
+ }
@@ -25,7 +25,12 @@ export type AudioData = ArrayBufferLike;
25
25
  * The encoding of the audio data
26
26
  * @defaultValue "pcm_s16"le
27
27
  */
28
- export type AudioEncoding = "pcm_s16le" | "pcm_mulaw" | "opus" | "ogg_opus";
28
+ export type AudioEncoding =
29
+ | "pcm_s16le"
30
+ | "pcm_mulaw"
31
+ | "opus"
32
+ | "ogg_opus"
33
+ | "aac";
29
34
 
30
35
  /**
31
36
  * Configure the threshold for how long to wait before ending an utterance. Default is 700ms.
@@ -1,3 +1,4 @@
1
+ export * from "./sync";
1
2
  export * from "./files";
2
3
  export * from "./transcripts";
3
4
  export * from "./realtime";
@@ -4,6 +4,7 @@ type BaseServiceParams = {
4
4
  apiKey: string;
5
5
  baseUrl?: string;
6
6
  streamingBaseUrl?: string;
7
+ syncBaseUrl?: string;
7
8
  /**
8
9
  * The AssemblyAI user agent to use for requests.
9
10
  * The provided components will be merged into the default AssemblyAI user agent.
@@ -94,8 +94,8 @@ export type StreamingTranscriberParams = {
94
94
  connectionRetryDelay?: number;
95
95
  /**
96
96
  * Required for PCM encodings (and for dual-channel mode). May be omitted
97
- * for Opus encodings (`opus`, `ogg_opus`) — the stream is self-describing
98
- * and the server ignores the value.
97
+ * for self-describing encodings (`opus`, `ogg_opus`, `aac`) — the stream
98
+ * carries its own rate and the server ignores the value.
99
99
  */
100
100
  sampleRate?: number;
101
101
  encoding?: AudioEncoding;
@@ -108,6 +108,7 @@ export type StreamingTranscriberParams = {
108
108
  maxTurnSilence?: number;
109
109
  vadThreshold?: number;
110
110
  formatTurns?: boolean;
111
+ sessionHeartbeat?: boolean;
111
112
  filterProfanity?: boolean;
112
113
  keyterms?: string[];
113
114
  keytermsPrompt?: string[];
@@ -184,6 +185,7 @@ export type StreamingEvents =
184
185
  | "llmGatewayResponse"
185
186
  | "speakerRevision"
186
187
  | "warning"
188
+ | "heartbeat"
187
189
  | "vad"
188
190
  | "error";
189
191
 
@@ -195,6 +197,7 @@ export type StreamingListeners = {
195
197
  llmGatewayResponse?: (event: LLMGatewayResponseEvent) => void;
196
198
  speakerRevision?: (event: SpeakerRevisionEvent) => void;
197
199
  warning?: (event: WarningEvent) => void;
200
+ heartbeat?: (event: HeartbeatEvent) => void;
198
201
  vad?: (event: VadFrame) => void;
199
202
  error?: (error: Error) => void;
200
203
  };
@@ -370,6 +373,7 @@ export type StreamingUpdateConfiguration = {
370
373
  max_turn_silence?: number;
371
374
  vad_threshold?: number;
372
375
  format_turns?: boolean;
376
+ session_heartbeat?: boolean;
373
377
  keyterms_prompt?: string[];
374
378
  prompt?: string;
375
379
  agent_context?: string;
@@ -404,6 +408,14 @@ export type WarningEvent = {
404
408
  warning: string;
405
409
  };
406
410
 
411
+ export type HeartbeatEvent = {
412
+ type: "Heartbeat";
413
+ total_audio_received_ms: number;
414
+ total_duration_ms: number;
415
+ realtime_factor: number;
416
+ max_speech_probability: number;
417
+ };
418
+
407
419
  export type LLMGatewayResponseEvent = {
408
420
  type: "LLMGatewayResponse";
409
421
  turn_order: number;
@@ -443,7 +455,8 @@ export type StreamingEventMessage =
443
455
  | LLMGatewayResponseEvent
444
456
  | SpeakerRevisionEvent
445
457
  | ErrorEvent
446
- | WarningEvent;
458
+ | WarningEvent
459
+ | HeartbeatEvent;
447
460
 
448
461
  export type StreamingOperationMessage =
449
462
  | StreamingUpdateConfiguration