realtime-voice-agents 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +298 -0
  3. package/assets/elevator-jazz.ulaw +1 -0
  4. package/assets/keyboard-typing.ulaw +1 -0
  5. package/assets/lofi.ulaw +1 -0
  6. package/assets/ringing.ulaw +1 -0
  7. package/assets/thinking-hum.ulaw +1 -0
  8. package/dist/BackgroundAudioPlayer-iMcivjis.mjs +282 -0
  9. package/dist/BackgroundAudioPlayer-jfRULWKC.cjs +313 -0
  10. package/dist/BaseRealtimeProvider-BQigr5mB.mjs +62 -0
  11. package/dist/BaseRealtimeProvider-BehPNT1r.d.cts +239 -0
  12. package/dist/BaseRealtimeProvider-ClP8Wx1X.d.mts +239 -0
  13. package/dist/BaseRealtimeProvider-DI4pKtOb.cjs +91 -0
  14. package/dist/GeminiLiveProvider-DvkTgzjG.d.cts +101 -0
  15. package/dist/GeminiLiveProvider-x2nyx5aO.d.mts +101 -0
  16. package/dist/InMemorySessionStore-B0_i-DOU.cjs +34 -0
  17. package/dist/InMemorySessionStore-B5_rq61L.d.cts +83 -0
  18. package/dist/InMemorySessionStore-B5_rq61L.d.mts +83 -0
  19. package/dist/InMemorySessionStore-DYParOJO.mjs +29 -0
  20. package/dist/OpenAICompatibleProvider-Bdtl-UXH.mjs +402 -0
  21. package/dist/OpenAICompatibleProvider-NS4cKVQj.cjs +409 -0
  22. package/dist/audio.cjs +21 -0
  23. package/dist/audio.d.cts +165 -0
  24. package/dist/audio.d.mts +165 -0
  25. package/dist/audio.mjs +4 -0
  26. package/dist/env-DSnGaERV.cjs +19 -0
  27. package/dist/env-DUwUWTsg.mjs +14 -0
  28. package/dist/events-BUMYdETO.d.cts +24 -0
  29. package/dist/events-BUMYdETO.d.mts +24 -0
  30. package/dist/events-BxDTIKKq.cjs +48 -0
  31. package/dist/events-BylBSBW-.mjs +43 -0
  32. package/dist/gemini.cjs +401 -0
  33. package/dist/gemini.d.cts +41 -0
  34. package/dist/gemini.d.mts +41 -0
  35. package/dist/gemini.mjs +395 -0
  36. package/dist/index.cjs +2142 -0
  37. package/dist/index.d.cts +920 -0
  38. package/dist/index.d.mts +920 -0
  39. package/dist/index.mjs +2113 -0
  40. package/dist/mulaw--cwU2c9L.mjs +64 -0
  41. package/dist/mulaw-DLUObjdP.cjs +117 -0
  42. package/dist/openai.cjs +61 -0
  43. package/dist/openai.d.cts +121 -0
  44. package/dist/openai.d.mts +121 -0
  45. package/dist/openai.mjs +53 -0
  46. package/dist/presets-Bf75YXs5.d.cts +24 -0
  47. package/dist/presets-Bf75YXs5.d.mts +24 -0
  48. package/dist/rest-BYqiVOhe.mjs +265 -0
  49. package/dist/rest-BvUKut_k.cjs +300 -0
  50. package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
  51. package/dist/session-config-BVLl7-ha.mjs +74 -0
  52. package/dist/session-config-c8sOw1XL.cjs +85 -0
  53. package/dist/store.cjs +3 -0
  54. package/dist/store.d.cts +2 -0
  55. package/dist/store.d.mts +2 -0
  56. package/dist/store.mjs +2 -0
  57. package/dist/testing.cjs +544 -0
  58. package/dist/testing.d.cts +191 -0
  59. package/dist/testing.d.mts +191 -0
  60. package/dist/testing.mjs +538 -0
  61. package/dist/transcode-C9aJG7_W.cjs +215 -0
  62. package/dist/transcode-CsAp97G9.mjs +198 -0
  63. package/dist/transport-B_PJFIVd.d.cts +155 -0
  64. package/dist/transport-CEaLFV4E.d.mts +155 -0
  65. package/dist/twilio.cjs +8 -0
  66. package/dist/twilio.d.cts +3 -0
  67. package/dist/twilio.d.mts +3 -0
  68. package/dist/twilio.mjs +2 -0
  69. package/dist/twiml-z9LjoF4_.d.cts +67 -0
  70. package/dist/twiml-z9LjoF4_.d.mts +67 -0
  71. package/dist/xai.cjs +92 -0
  72. package/dist/xai.d.cts +35 -0
  73. package/dist/xai.d.mts +35 -0
  74. package/dist/xai.mjs +86 -0
  75. package/package.json +183 -0
@@ -0,0 +1,409 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
2
+ const require_BaseRealtimeProvider = require("./BaseRealtimeProvider-DI4pKtOb.cjs");
3
+ const require_session_config = require("./session-config-c8sOw1XL.cjs");
4
+ let ws = require("ws");
5
+ ws = require_rolldown_runtime.__toESM(ws, 1);
6
+ //#region src/providers/openai-compatible/OpenAICompatibleProvider.ts
7
+ /**
8
+ * Provider for the OpenAI Realtime API (GA protocol) and API-compatible
9
+ * services (xAI Grok Voice Agent). Speaks `audio/pcmu` in both directions, so
10
+ * Twilio payloads pass through verbatim — zero transcoding on this path.
11
+ *
12
+ * Connect handshake: open WS → receive `session.created` → send
13
+ * `session.update` (audio/pcmu, VAD, tools, instructions) → resolve on
14
+ * `session.updated`. A handshake that hangs rejects after `connectTimeoutMs`
15
+ * so a dead upstream can't leak half-open calls.
16
+ */
17
+ const DEFAULT_BASE_URL = "wss://api.openai.com/v1/realtime";
18
+ /** WS close codes where retrying cannot help (protocol/auth/policy failures). */
19
+ const NON_RETRIABLE_CLOSE_CODES = /* @__PURE__ */ new Set([
20
+ 1002,
21
+ 1003,
22
+ 1007,
23
+ 1008
24
+ ]);
25
+ var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRealtimeProvider {
26
+ name;
27
+ capabilities;
28
+ config;
29
+ logger;
30
+ ws = null;
31
+ sessionInit = null;
32
+ ready = false;
33
+ intentionalClose = false;
34
+ currentResponseId = null;
35
+ /**
36
+ * response.create serialization. The GA API rejects a `response.create`
37
+ * issued while another response is in flight
38
+ * (`conversation_already_has_active_response`), so creates requested
39
+ * mid-response wait in a single pending slot and fire on `response.done`.
40
+ * Later requests coalesce into that slot — one response reads the whole
41
+ * conversation state, so only the instructions payload is worth keeping.
42
+ */
43
+ responseActive = false;
44
+ pendingCreate = null;
45
+ lastCreateSent = null;
46
+ constructor(config, logger = require_BaseRealtimeProvider.noopLogger) {
47
+ super();
48
+ this.config = config;
49
+ this.logger = logger;
50
+ this.name = config.providerName ?? "openai";
51
+ this.capabilities = {
52
+ truncate: true,
53
+ sessionUpdate: true,
54
+ voiceChangeMidSession: false,
55
+ transcodeRequired: false,
56
+ resumption: false,
57
+ agentTranscriptDeltas: true,
58
+ vadInterruptControl: true,
59
+ ...config.capabilityOverrides
60
+ };
61
+ }
62
+ get isConnected() {
63
+ return this.ready && this.ws?.readyState === ws.default.OPEN;
64
+ }
65
+ async connect(init) {
66
+ if (this.ws) await this.close();
67
+ let vad = init.vad !== void 0 ? init.vad : this.config.defaultVad;
68
+ if (init.bridgeOwnsInterruptions && this.capabilities.vadInterruptControl && vad !== null) vad = {
69
+ interruptResponse: false,
70
+ ...vad ?? { type: "server" }
71
+ };
72
+ this.sessionInit = {
73
+ ...init,
74
+ vad,
75
+ transcription: init.transcription !== void 0 ? init.transcription : this.config.defaultTranscription
76
+ };
77
+ this.intentionalClose = false;
78
+ this.ready = false;
79
+ this.responseActive = false;
80
+ this.pendingCreate = null;
81
+ this.lastCreateSent = null;
82
+ this.currentResponseId = null;
83
+ const url = `${this.config.baseUrl ?? DEFAULT_BASE_URL}?model=${encodeURIComponent(this.config.model)}`;
84
+ const ws$1 = new ws.default(url, {
85
+ headers: {
86
+ Authorization: `Bearer ${this.config.apiKey}`,
87
+ ...this.config.headers
88
+ },
89
+ perMessageDeflate: false
90
+ });
91
+ this.ws = ws$1;
92
+ await new Promise((resolve, reject) => {
93
+ const timeoutMs = this.config.connectTimeoutMs ?? 1e4;
94
+ const timer = setTimeout(() => {
95
+ fail(/* @__PURE__ */ new Error(`${this.name} session not ready within ${timeoutMs}ms`));
96
+ }, timeoutMs);
97
+ timer.unref?.();
98
+ let settled = false;
99
+ const fail = (error) => {
100
+ if (settled) return;
101
+ settled = true;
102
+ clearTimeout(timer);
103
+ try {
104
+ ws$1.close();
105
+ } catch {}
106
+ reject(error);
107
+ };
108
+ ws$1.on("unexpected-response", (_request, response) => {
109
+ let body = "";
110
+ response.setEncoding("utf8");
111
+ response.on("data", (chunk) => {
112
+ if (body.length < 512) body += chunk;
113
+ });
114
+ response.on("end", () => {
115
+ const detail = body.trim().slice(0, 500);
116
+ this.logger.error("provider rejected the WebSocket upgrade", {
117
+ status: response.statusCode,
118
+ body: detail
119
+ });
120
+ fail(/* @__PURE__ */ new Error(`${this.name} rejected the WebSocket upgrade: HTTP ${response.statusCode}${detail ? ` — ${detail}` : ""}`));
121
+ });
122
+ });
123
+ ws$1.on("open", () => this.emit("open"));
124
+ ws$1.on("message", (raw) => {
125
+ const event = this.parseEvent(raw);
126
+ if (!event) return;
127
+ if (!settled) {
128
+ if (event.type === "session.created") {
129
+ this.send(this.buildSessionPayload());
130
+ return;
131
+ }
132
+ if (event.type === "session.updated") {
133
+ settled = true;
134
+ clearTimeout(timer);
135
+ this.ready = true;
136
+ resolve();
137
+ return;
138
+ }
139
+ if (event.type === "error") {
140
+ fail(/* @__PURE__ */ new Error(`${this.name} session setup error: ${JSON.stringify(event.error ?? event)}`));
141
+ return;
142
+ }
143
+ }
144
+ this.handleEvent(event);
145
+ });
146
+ ws$1.on("error", (error) => {
147
+ if (!settled) fail(error instanceof Error ? error : new Error(String(error)));
148
+ else this.emit("error", error instanceof Error ? error : new Error(String(error)));
149
+ });
150
+ ws$1.on("close", (code, reasonBuf) => {
151
+ const reason = reasonBuf?.toString();
152
+ this.ready = false;
153
+ if (!settled) {
154
+ fail(/* @__PURE__ */ new Error(`${this.name} socket closed during setup (${code} ${reason ?? ""})`));
155
+ return;
156
+ }
157
+ this.emit("close", {
158
+ code,
159
+ reason,
160
+ retriable: !this.intentionalClose && !NON_RETRIABLE_CLOSE_CODES.has(code)
161
+ });
162
+ });
163
+ });
164
+ }
165
+ async close() {
166
+ this.intentionalClose = true;
167
+ this.ready = false;
168
+ const ws$2 = this.ws;
169
+ this.ws = null;
170
+ if (!ws$2) return;
171
+ if (ws$2.readyState === ws.default.CLOSED) return;
172
+ await new Promise((resolve) => {
173
+ const timer = setTimeout(() => {
174
+ ws$2.terminate();
175
+ resolve();
176
+ }, 1e3);
177
+ timer.unref?.();
178
+ ws$2.once("close", () => {
179
+ clearTimeout(timer);
180
+ resolve();
181
+ });
182
+ try {
183
+ ws$2.close(1e3);
184
+ } catch {
185
+ clearTimeout(timer);
186
+ ws$2.terminate();
187
+ resolve();
188
+ }
189
+ });
190
+ }
191
+ sendAudio(base64Mulaw) {
192
+ if (!this.isConnected) return;
193
+ this.send({
194
+ type: "input_audio_buffer.append",
195
+ audio: base64Mulaw
196
+ });
197
+ }
198
+ sendText(text, options = {}) {
199
+ const role = options.role ?? "user";
200
+ this.send({
201
+ type: "conversation.item.create",
202
+ item: {
203
+ type: "message",
204
+ role,
205
+ content: [{
206
+ type: role === "assistant" ? "output_text" : "input_text",
207
+ text
208
+ }]
209
+ }
210
+ });
211
+ if (options.triggerResponse !== false) this.createResponse();
212
+ }
213
+ sendToolResult(callId, output, options = {}) {
214
+ this.send({
215
+ type: "conversation.item.create",
216
+ item: {
217
+ type: "function_call_output",
218
+ call_id: callId,
219
+ output: typeof output === "string" ? output : JSON.stringify(output ?? null)
220
+ }
221
+ });
222
+ if (options.triggerResponse !== false) this.createResponse();
223
+ }
224
+ createResponse(options = {}) {
225
+ if (this.responseActive) {
226
+ this.pendingCreate = {
227
+ instructions: options.instructions ?? this.pendingCreate?.instructions,
228
+ attempts: 0
229
+ };
230
+ return;
231
+ }
232
+ this.sendCreate({
233
+ instructions: options.instructions,
234
+ attempts: 0
235
+ });
236
+ }
237
+ sendCreate(create) {
238
+ this.lastCreateSent = create;
239
+ this.send({
240
+ type: "response.create",
241
+ ...create.instructions ? { response: { instructions: create.instructions } } : {}
242
+ });
243
+ }
244
+ async updateSession(patch) {
245
+ if (!this.sessionInit) throw new Error("updateSession before connect");
246
+ this.sessionInit = {
247
+ ...this.sessionInit,
248
+ ...patch
249
+ };
250
+ this.send(this.buildSessionPayload());
251
+ }
252
+ buildSessionPayload() {
253
+ return (this.config.buildSession ?? require_session_config.buildSessionUpdate)(this.sessionInit, {
254
+ defaultVoice: this.config.voice,
255
+ extraSessionOptions: this.config.extraSessionOptions
256
+ });
257
+ }
258
+ cancelResponse() {
259
+ if (this.pendingCreate && !this.pendingCreate.instructions) this.pendingCreate = null;
260
+ this.send({ type: "response.cancel" });
261
+ }
262
+ truncatePlayback(itemId, audioEndMs) {
263
+ this.send({
264
+ type: "conversation.item.truncate",
265
+ item_id: itemId,
266
+ content_index: 0,
267
+ audio_end_ms: Math.max(0, Math.round(audioEndMs))
268
+ });
269
+ }
270
+ handleEvent(event) {
271
+ switch (event.type) {
272
+ case "response.created": {
273
+ const responseId = event.response?.id ?? `resp_${Date.now()}`;
274
+ this.currentResponseId = responseId;
275
+ this.responseActive = true;
276
+ this.emit("responseStarted", { responseId });
277
+ break;
278
+ }
279
+ case "response.output_item.added": {
280
+ const itemId = event.item?.id;
281
+ if (itemId) this.emit("outputItemAdded", {
282
+ itemId,
283
+ responseId: event.response_id ?? this.currentResponseId ?? ""
284
+ });
285
+ break;
286
+ }
287
+ case "response.output_audio.delta":
288
+ if (typeof event.delta === "string" && event.delta.length > 0) this.emit("audio", {
289
+ base64Mulaw: event.delta,
290
+ responseId: event.response_id ?? this.currentResponseId ?? "",
291
+ itemId: event.item_id
292
+ });
293
+ break;
294
+ case "response.output_audio_transcript.delta":
295
+ if (typeof event.delta === "string") this.emit("agentTranscriptDelta", {
296
+ responseId: event.response_id ?? this.currentResponseId ?? "",
297
+ delta: event.delta
298
+ });
299
+ break;
300
+ case "response.output_audio_transcript.done":
301
+ if (typeof event.transcript === "string") this.emit("agentTranscript", {
302
+ responseId: event.response_id ?? this.currentResponseId ?? "",
303
+ text: event.transcript
304
+ });
305
+ break;
306
+ case "conversation.item.input_audio_transcription.completed":
307
+ if (typeof event.transcript === "string" && event.transcript.trim().length > 0) this.emit("userTranscript", { text: event.transcript.trim() });
308
+ break;
309
+ case "input_audio_buffer.speech_started":
310
+ if (this.pendingCreate && !this.pendingCreate.instructions) this.pendingCreate = null;
311
+ this.emit("userSpeechStarted");
312
+ break;
313
+ case "input_audio_buffer.speech_stopped":
314
+ this.emit("userSpeechStopped");
315
+ break;
316
+ case "response.done": {
317
+ const responseId = event.response?.id ?? this.currentResponseId ?? "";
318
+ const usage = normalizeUsage(event.response?.usage);
319
+ this.responseActive = false;
320
+ const pending = this.pendingCreate;
321
+ if (pending) {
322
+ this.pendingCreate = null;
323
+ this.sendCreate(pending);
324
+ }
325
+ if (usage) this.emit("usage", usage);
326
+ this.emit("responseDone", {
327
+ responseId,
328
+ usage: usage ?? void 0
329
+ });
330
+ break;
331
+ }
332
+ case "response.function_call_arguments.done":
333
+ this.emit("toolCall", {
334
+ id: event.call_id ?? event.item_id ?? `call_${Date.now()}`,
335
+ name: event.name,
336
+ argumentsJson: typeof event.arguments === "string" ? event.arguments : "{}",
337
+ responseId: event.response_id ?? this.currentResponseId ?? void 0,
338
+ itemId: event.item_id
339
+ });
340
+ break;
341
+ case "error": {
342
+ const code = event.error?.code;
343
+ if (code === "conversation_already_has_active_response") {
344
+ this.responseActive = true;
345
+ const last = this.lastCreateSent;
346
+ if (last && last.attempts < 2 && !this.pendingCreate) this.pendingCreate = {
347
+ ...last,
348
+ attempts: last.attempts + 1
349
+ };
350
+ this.logger.warn("response.create raced an active response; deferred until it completes", { error: event.error });
351
+ break;
352
+ }
353
+ {
354
+ const message = typeof event.error?.message === "string" ? event.error.message : "";
355
+ if (code === "response_cancel_not_active" || /cancellation failed: no active response/i.test(message)) {
356
+ this.logger.warn("response.cancel raced completion (ignored)", { error: event.error });
357
+ break;
358
+ }
359
+ }
360
+ this.logger.warn("provider error event", { error: event.error });
361
+ this.emit("error", /* @__PURE__ */ new Error(`${this.name} error: ${JSON.stringify(event.error ?? event)}`));
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ parseEvent(raw) {
367
+ try {
368
+ return JSON.parse(raw.toString());
369
+ } catch {
370
+ this.logger.warn("unparseable provider frame");
371
+ return null;
372
+ }
373
+ }
374
+ send(payload) {
375
+ if (this.ws?.readyState !== ws.default.OPEN) return;
376
+ try {
377
+ this.ws.send(JSON.stringify(payload));
378
+ } catch (error) {
379
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
380
+ }
381
+ }
382
+ };
383
+ function normalizeUsage(raw) {
384
+ if (!raw || typeof raw !== "object") return null;
385
+ const inputDetails = raw.input_token_details ?? raw.input_tokens_details;
386
+ const outputDetails = raw.output_token_details ?? raw.output_tokens_details;
387
+ return {
388
+ inputTokens: raw.input_tokens ?? 0,
389
+ outputTokens: raw.output_tokens ?? 0,
390
+ totalTokens: raw.total_tokens ?? (raw.input_tokens ?? 0) + (raw.output_tokens ?? 0),
391
+ ...inputDetails ? { inputTokenDetails: {
392
+ textTokens: inputDetails.text_tokens,
393
+ audioTokens: inputDetails.audio_tokens,
394
+ cachedTokens: inputDetails.cached_tokens
395
+ } } : {},
396
+ ...outputDetails ? { outputTokenDetails: {
397
+ textTokens: outputDetails.text_tokens,
398
+ audioTokens: outputDetails.audio_tokens
399
+ } } : {},
400
+ raw
401
+ };
402
+ }
403
+ //#endregion
404
+ Object.defineProperty(exports, "OpenAICompatibleProvider", {
405
+ enumerable: true,
406
+ get: function() {
407
+ return OpenAICompatibleProvider;
408
+ }
409
+ });
package/dist/audio.cjs ADDED
@@ -0,0 +1,21 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_mulaw = require("./mulaw-DLUObjdP.cjs");
3
+ const require_BackgroundAudioPlayer = require("./BackgroundAudioPlayer-jfRULWKC.cjs");
4
+ const require_transcode = require("./transcode-C9aJG7_W.cjs");
5
+ exports.BackgroundAudioPlayer = require_BackgroundAudioPlayer.BackgroundAudioPlayer;
6
+ exports.InboundTranscoder = require_transcode.InboundTranscoder;
7
+ exports.MULAW_FRAME_BYTES_20MS = require_mulaw.MULAW_FRAME_BYTES_20MS;
8
+ exports.MULAW_SAMPLE_RATE = require_mulaw.MULAW_SAMPLE_RATE;
9
+ exports.MULAW_SILENCE_BYTE = require_mulaw.MULAW_SILENCE_BYTE;
10
+ exports.OutboundTranscoder = require_transcode.OutboundTranscoder;
11
+ exports.Resampler = require_transcode.Resampler;
12
+ exports.base64ByteLength = require_mulaw.base64ByteLength;
13
+ exports.fadeMulaw = require_BackgroundAudioPlayer.fadeMulaw;
14
+ exports.isPresetName = require_BackgroundAudioPlayer.isPresetName;
15
+ exports.loadBackgroundAudio = require_BackgroundAudioPlayer.loadBackgroundAudio;
16
+ exports.mulawBytesToMs = require_mulaw.mulawBytesToMs;
17
+ exports.mulawDecodeSample = require_mulaw.mulawDecodeSample;
18
+ exports.mulawEncodeSample = require_mulaw.mulawEncodeSample;
19
+ exports.mulawToPcm16 = require_mulaw.mulawToPcm16;
20
+ exports.pcm16ToMulaw = require_mulaw.pcm16ToMulaw;
21
+ exports.scaleMulaw = require_BackgroundAudioPlayer.scaleMulaw;
@@ -0,0 +1,165 @@
1
+ import { n as BackgroundAudioPreset, r as BackgroundAudioSpec, t as BackgroundAudioOptions } from "./presets-Bf75YXs5.cjs";
2
+ //#region src/audio/mulaw.d.ts
3
+ /**
4
+ * G.711 μ-law codec (the format Twilio Media Streams speak: 8 kHz, 1 byte/sample).
5
+ *
6
+ * Decode uses a 256-entry lookup table; encode uses the canonical Sun g711.c
7
+ * algorithm (bias 0x84, clip 32635). μ-law silence is 0xFF.
8
+ */
9
+ /** One byte of μ-law silence (encodes linear 0). */
10
+ declare const MULAW_SILENCE_BYTE = 255;
11
+ /** Samples per second on the Twilio media stream. μ-law is 1 byte per sample. */
12
+ declare const MULAW_SAMPLE_RATE = 8000;
13
+ /** Bytes per 20 ms Twilio media frame at 8 kHz μ-law. */
14
+ declare const MULAW_FRAME_BYTES_20MS = 160;
15
+ /** Decode a single μ-law byte to a linear PCM16 sample. */
16
+ declare function mulawDecodeSample(byte: number): number;
17
+ /** Encode a single linear PCM16 sample to a μ-law byte. */
18
+ declare function mulawEncodeSample(sample: number): number;
19
+ /** Decode a μ-law buffer to linear PCM16 samples. */
20
+ declare function mulawToPcm16(mulaw: Uint8Array): Int16Array;
21
+ /** Encode linear PCM16 samples to a μ-law buffer. */
22
+ declare function pcm16ToMulaw(pcm: Int16Array): Uint8Array;
23
+ /** Duration in milliseconds of a μ-law byte count at 8 kHz (1 byte = 125 µs). */
24
+ declare function mulawBytesToMs(bytes: number): number;
25
+ /** Byte length of a base64 string without decoding it. */
26
+ declare function base64ByteLength(base64: string): number;
27
+ //#endregion
28
+ //#region src/audio/resampler.d.ts
29
+ /**
30
+ * Stateful polyphase FIR sample-rate converter.
31
+ *
32
+ * Built for streaming telephony audio: the instance carries the FIR history
33
+ * across process() calls, so chunk boundaries produce bit-identical output to
34
+ * single-pass processing — no per-chunk zero-padding, no boundary clicks.
35
+ * Coefficients are computed once per (from, to, quality) in a module cache.
36
+ *
37
+ * Rational L/M design: the prototype lowpass is a Kaiser-windowed sinc at the
38
+ * upsampled rate (from × L), decomposed into L phases of N/L taps. Each phase
39
+ * is normalized to unit DC gain so constant signals pass through exactly.
40
+ */
41
+ interface ResamplerOptions {
42
+ /**
43
+ * Prototype filter length multiplier. The prototype has
44
+ * `quality × max(L, M)` taps (rounded up to a multiple of L); higher is
45
+ * cleaner and slower. Default 48 (≈70–80 dB alias rejection for 24k→8k).
46
+ */
47
+ quality?: number;
48
+ /** Kaiser window beta. Default 8. */
49
+ kaiserBeta?: number;
50
+ /** Passband edge as a fraction of the tighter Nyquist. Default 0.9. */
51
+ rolloff?: number;
52
+ }
53
+ declare class Resampler {
54
+ readonly fromRate: number;
55
+ readonly toRate: number;
56
+ private readonly L;
57
+ private readonly M;
58
+ private readonly bank;
59
+ /** Trailing input samples carried between chunks (tapsPerPhase − 1 long). */
60
+ private hist;
61
+ /** Phase index of the next output sample. */
62
+ private phase;
63
+ /**
64
+ * Input index (relative to the start of the next [hist + chunk] buffer) that
65
+ * the next output sample is anchored on. Always ≥ tapsPerPhase − 1.
66
+ */
67
+ private kRel;
68
+ constructor(fromRate: number, toRate: number, options?: ResamplerOptions);
69
+ /** True when no rate conversion is needed (input is passed through). */
70
+ get isPassthrough(): boolean;
71
+ process(input: Int16Array): Int16Array;
72
+ /** Drain the filter tail by pushing one phase-length of silence. */
73
+ flush(): Int16Array;
74
+ /** Clear all carried state (history, phase) for reuse on a fresh stream. */
75
+ reset(): void;
76
+ }
77
+ //#endregion
78
+ //#region src/audio/transcode.d.ts
79
+ /** Twilio → provider: base64 μ-law 8 kHz in, base64 PCM16LE at `outRate` out. */
80
+ declare class InboundTranscoder {
81
+ private readonly resampler;
82
+ constructor(outRate?: number, options?: ResamplerOptions);
83
+ process(base64Mulaw: string): string;
84
+ reset(): void;
85
+ }
86
+ /** Provider → Twilio: base64 PCM16LE at `inRate` in, base64 μ-law 8 kHz out. */
87
+ declare class OutboundTranscoder {
88
+ private readonly resampler;
89
+ /** Dangling byte from a chunk that split a 16-bit sample. */
90
+ private carry;
91
+ constructor(inRate?: number, options?: ResamplerOptions);
92
+ process(base64Pcm16: string): string;
93
+ reset(): void;
94
+ }
95
+ //#endregion
96
+ //#region src/audio/gain.d.ts
97
+ /**
98
+ * Gain application on μ-law frames, used for background-audio fades and volume.
99
+ * μ-law is logarithmic, so gain must ramp in the linear domain:
100
+ * decode → scale → re-encode.
101
+ */
102
+ /** Scale an entire μ-law frame by a constant linear gain. */
103
+ declare function scaleMulaw(frame: Uint8Array, gain: number): Uint8Array;
104
+ /**
105
+ * Apply a linearly interpolated gain ramp across a μ-law frame
106
+ * (`startGain` at the first sample → `endGain` at the last).
107
+ */
108
+ declare function fadeMulaw(frame: Uint8Array, startGain: number, endGain: number): Uint8Array;
109
+ //#endregion
110
+ //#region src/audio/background/BackgroundAudioPlayer.d.ts
111
+ interface BackgroundAudioPlayerDeps {
112
+ sendMedia: (base64Payload: string) => void;
113
+ onStarted?: (info: {
114
+ preset?: string;
115
+ }) => void;
116
+ onStopped?: (info: {
117
+ preset?: string;
118
+ }) => void;
119
+ now?: () => number;
120
+ }
121
+ declare class BackgroundAudioPlayer {
122
+ private readonly deps;
123
+ private readonly now;
124
+ private generation;
125
+ private active;
126
+ private startDelayTimer;
127
+ /** Tool-call ids currently holding the loop. */
128
+ private readonly holders;
129
+ private pendingSpec;
130
+ constructor(deps: BackgroundAudioPlayerDeps);
131
+ get isPlaying(): boolean;
132
+ /**
133
+ * Refcounted acquisition (one holder per tool call). The loop starts after
134
+ * `startDelayMs` unless every holder releases first.
135
+ */
136
+ acquire(holderId: string, spec: BackgroundAudioSpec, options?: BackgroundAudioOptions): void;
137
+ /** Release one holder; the loop stops when the last holder releases. */
138
+ release(holderId: string, opts?: {
139
+ immediate?: boolean;
140
+ }): void;
141
+ /** Direct start (facade `playBackgroundAudio`). Bypasses refcounting. */
142
+ start(spec: BackgroundAudioSpec, options?: BackgroundAudioOptions): void;
143
+ /**
144
+ * Stop the loop. Fade-out is applied when `immediate` is false and a
145
+ * fadeOutMs was configured; agent-audio preemption should pass
146
+ * `immediate: true` (a fade would delay real speech behind it).
147
+ */
148
+ stop(opts?: {
149
+ immediate?: boolean;
150
+ }): void;
151
+ /** Real agent audio arrived — kill the loop instantly, no fade, no clear. */
152
+ notifyAgentAudio(): void;
153
+ private cancelDelay;
154
+ private startNow;
155
+ private tick;
156
+ /** Wrap-around frame extraction for seamless looping. */
157
+ private extractFrame;
158
+ }
159
+ //#endregion
160
+ //#region src/audio/background/loader.d.ts
161
+ declare function isPresetName(spec: BackgroundAudioSpec): spec is BackgroundAudioPreset;
162
+ /** Resolve a spec to raw μ-law bytes (cached per preset/path). */
163
+ declare function loadBackgroundAudio(spec: BackgroundAudioSpec): Buffer;
164
+ //#endregion
165
+ export { type BackgroundAudioOptions, BackgroundAudioPlayer, type BackgroundAudioPlayerDeps, type BackgroundAudioPreset, type BackgroundAudioSpec, InboundTranscoder, MULAW_FRAME_BYTES_20MS, MULAW_SAMPLE_RATE, MULAW_SILENCE_BYTE, OutboundTranscoder, Resampler, type ResamplerOptions, base64ByteLength, fadeMulaw, isPresetName, loadBackgroundAudio, mulawBytesToMs, mulawDecodeSample, mulawEncodeSample, mulawToPcm16, pcm16ToMulaw, scaleMulaw };