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,282 @@
1
+ import { o as mulawDecodeSample, s as mulawEncodeSample } from "./mulaw--cwU2c9L.mjs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region src/audio/gain.ts
6
+ /**
7
+ * Gain application on μ-law frames, used for background-audio fades and volume.
8
+ * μ-law is logarithmic, so gain must ramp in the linear domain:
9
+ * decode → scale → re-encode.
10
+ */
11
+ /** Scale an entire μ-law frame by a constant linear gain. */
12
+ function scaleMulaw(frame, gain) {
13
+ if (gain === 1) return frame;
14
+ const out = new Uint8Array(frame.length);
15
+ for (let i = 0; i < frame.length; i++) out[i] = mulawEncodeSample(clamp16(mulawDecodeSample(frame[i]) * gain));
16
+ return out;
17
+ }
18
+ /**
19
+ * Apply a linearly interpolated gain ramp across a μ-law frame
20
+ * (`startGain` at the first sample → `endGain` at the last).
21
+ */
22
+ function fadeMulaw(frame, startGain, endGain) {
23
+ if (startGain === endGain) return scaleMulaw(frame, startGain);
24
+ const out = new Uint8Array(frame.length);
25
+ const step = frame.length > 1 ? (endGain - startGain) / (frame.length - 1) : 0;
26
+ for (let i = 0; i < frame.length; i++) {
27
+ const gain = startGain + step * i;
28
+ out[i] = mulawEncodeSample(clamp16(mulawDecodeSample(frame[i]) * gain));
29
+ }
30
+ return out;
31
+ }
32
+ function clamp16(v) {
33
+ const r = Math.round(v);
34
+ if (r > 32767) return 32767;
35
+ if (r < -32768) return -32768;
36
+ return r;
37
+ }
38
+ //#endregion
39
+ //#region src/audio/background/loader.ts
40
+ /**
41
+ * Preset asset loading. Assets are raw 8 kHz μ-law loops in the package's
42
+ * `assets/` directory (generated by scripts/generate-tones.ts and shipped in
43
+ * the npm tarball). Custom sources accept a Buffer or a file path.
44
+ */
45
+ const PRESET_FILES = {
46
+ "elevator-jazz": "elevator-jazz.ulaw",
47
+ lofi: "lofi.ulaw",
48
+ "keyboard-typing": "keyboard-typing.ulaw",
49
+ "thinking-hum": "thinking-hum.ulaw",
50
+ ringing: "ringing.ulaw"
51
+ };
52
+ let cachedAssetsDir = null;
53
+ const bufferCache = /* @__PURE__ */ new Map();
54
+ function assetsDir() {
55
+ if (cachedAssetsDir) return cachedAssetsDir;
56
+ const here = path.dirname(fileURLToPath(import.meta.url));
57
+ const candidates = [
58
+ path.resolve(here, "../assets"),
59
+ path.resolve(here, "../../assets"),
60
+ path.resolve(here, "../../../assets")
61
+ ];
62
+ for (const candidate of candidates) if (existsSync(candidate)) {
63
+ cachedAssetsDir = candidate;
64
+ return candidate;
65
+ }
66
+ throw new Error(`background-audio assets directory not found (looked in: ${candidates.join(", ")})`);
67
+ }
68
+ function isPresetName(spec) {
69
+ return typeof spec === "string";
70
+ }
71
+ /** Resolve a spec to raw μ-law bytes (cached per preset/path). */
72
+ function loadBackgroundAudio(spec) {
73
+ if (isPresetName(spec)) {
74
+ const file = PRESET_FILES[spec];
75
+ if (!file) throw new Error(`unknown background-audio preset "${spec}"`);
76
+ const full = path.join(assetsDir(), file);
77
+ let buffer = bufferCache.get(full);
78
+ if (!buffer) {
79
+ buffer = readFileSync(full);
80
+ bufferCache.set(full, buffer);
81
+ }
82
+ return buffer;
83
+ }
84
+ if (Buffer.isBuffer(spec.custom)) return spec.custom;
85
+ let buffer = bufferCache.get(spec.custom);
86
+ if (!buffer) {
87
+ buffer = readFileSync(spec.custom);
88
+ bufferCache.set(spec.custom, buffer);
89
+ }
90
+ return buffer;
91
+ }
92
+ function presetLabel(spec) {
93
+ return isPresetName(spec) ? spec : void 0;
94
+ }
95
+ //#endregion
96
+ //#region src/audio/background/BackgroundAudioPlayer.ts
97
+ /**
98
+ * BackgroundAudioPlayer — paced μ-law loop injection into the Twilio stream.
99
+ *
100
+ * Twilio has ONE sequential playout buffer, so background audio must be paced
101
+ * in near-realtime (never bulk-queued): frames-due are computed from the wall
102
+ * clock each tick (drift-corrected), with a small burst cap, keeping at most
103
+ * a few frames buffered ahead. That way, when real agent audio arrives it
104
+ * queues at most ~2 frames behind the hold loop instead of seconds.
105
+ *
106
+ * Safety rails from production systems: a generation counter invalidates
107
+ * orphan intervals after stop/restart races; a hard max-duration failsafe
108
+ * stops a hung tool from looping forever; refcounted acquire/release lets
109
+ * concurrent tools share one loop; a start delay keeps fast tools silent.
110
+ */
111
+ const FRAME_MS = 20;
112
+ const MAX_BURST_FRAMES = 5;
113
+ var BackgroundAudioPlayer = class {
114
+ deps;
115
+ now;
116
+ generation = 0;
117
+ active = null;
118
+ startDelayTimer = null;
119
+ /** Tool-call ids currently holding the loop. */
120
+ holders = /* @__PURE__ */ new Set();
121
+ pendingSpec = null;
122
+ constructor(deps) {
123
+ this.deps = deps;
124
+ this.now = deps.now ?? Date.now;
125
+ }
126
+ get isPlaying() {
127
+ return this.active !== null;
128
+ }
129
+ /**
130
+ * Refcounted acquisition (one holder per tool call). The loop starts after
131
+ * `startDelayMs` unless every holder releases first.
132
+ */
133
+ acquire(holderId, spec, options = {}) {
134
+ this.holders.add(holderId);
135
+ if (this.active || this.startDelayTimer) return;
136
+ this.pendingSpec = {
137
+ spec,
138
+ options
139
+ };
140
+ const delay = options.startDelayMs ?? 1e3;
141
+ if (delay <= 0) {
142
+ this.startNow();
143
+ return;
144
+ }
145
+ this.startDelayTimer = setTimeout(() => {
146
+ this.startDelayTimer = null;
147
+ if (this.holders.size > 0) this.startNow();
148
+ }, delay);
149
+ this.startDelayTimer.unref?.();
150
+ }
151
+ /** Release one holder; the loop stops when the last holder releases. */
152
+ release(holderId, opts = {}) {
153
+ this.holders.delete(holderId);
154
+ if (this.holders.size > 0) return;
155
+ this.stop({ immediate: opts.immediate });
156
+ }
157
+ /** Direct start (facade `playBackgroundAudio`). Bypasses refcounting. */
158
+ start(spec, options = {}) {
159
+ this.holders.add("__manual__");
160
+ this.cancelDelay();
161
+ this.pendingSpec = {
162
+ spec,
163
+ options
164
+ };
165
+ this.startNow();
166
+ }
167
+ /**
168
+ * Stop the loop. Fade-out is applied when `immediate` is false and a
169
+ * fadeOutMs was configured; agent-audio preemption should pass
170
+ * `immediate: true` (a fade would delay real speech behind it).
171
+ */
172
+ stop(opts = {}) {
173
+ this.holders.clear();
174
+ this.cancelDelay();
175
+ this.pendingSpec = null;
176
+ const active = this.active;
177
+ if (!active) return;
178
+ this.generation++;
179
+ clearInterval(active.interval);
180
+ clearTimeout(active.failsafe);
181
+ this.active = null;
182
+ const fadeOutMs = opts.immediate ? 0 : active.fadeOutMs ?? 0;
183
+ if (fadeOutMs > 0) {
184
+ const frames = Math.min(Math.ceil(fadeOutMs / FRAME_MS), 25);
185
+ let offset = active.offset;
186
+ for (let i = 0; i < frames; i++) {
187
+ const frame = this.extractFrame(active.scaled, offset);
188
+ offset = frame.nextOffset;
189
+ const gainStart = 1 - i / frames;
190
+ const gainEnd = 1 - (i + 1) / frames;
191
+ this.deps.sendMedia(Buffer.from(fadeMulaw(frame.frame, gainStart, gainEnd)).toString("base64"));
192
+ }
193
+ }
194
+ this.deps.onStopped?.({ preset: active.preset });
195
+ }
196
+ /** Real agent audio arrived — kill the loop instantly, no fade, no clear. */
197
+ notifyAgentAudio() {
198
+ if (!this.active && !this.startDelayTimer) return;
199
+ this.stop({ immediate: true });
200
+ }
201
+ cancelDelay() {
202
+ if (this.startDelayTimer) {
203
+ clearTimeout(this.startDelayTimer);
204
+ this.startDelayTimer = null;
205
+ }
206
+ }
207
+ startNow() {
208
+ if (this.active || !this.pendingSpec) return;
209
+ const { spec, options } = this.pendingSpec;
210
+ this.pendingSpec = null;
211
+ let buffer;
212
+ try {
213
+ buffer = loadBackgroundAudio(spec);
214
+ } catch {
215
+ return;
216
+ }
217
+ if (buffer.length < 160) return;
218
+ const volume = options.volume ?? .4;
219
+ const scaled = volume === 1 ? buffer : Buffer.from(scaleMulaw(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length), volume));
220
+ const generation = ++this.generation;
221
+ const fadeInMs = options.fadeInMs ?? 200;
222
+ const loop = {
223
+ generation,
224
+ buffer,
225
+ scaled,
226
+ preset: presetLabel(spec),
227
+ offset: 0,
228
+ startedAt: this.now(),
229
+ framesSent: 0,
230
+ fadeInFramesLeft: Math.ceil(fadeInMs / FRAME_MS),
231
+ volume,
232
+ fadeInMs,
233
+ fadeOutMs: options.fadeOutMs ?? 200,
234
+ interval: setInterval(() => this.tick(generation), FRAME_MS),
235
+ failsafe: setTimeout(() => {
236
+ if (this.active?.generation === generation) this.stop({ immediate: true });
237
+ }, options.maxDurationMs ?? 6e4)
238
+ };
239
+ loop.interval.unref?.();
240
+ loop.failsafe.unref?.();
241
+ this.active = loop;
242
+ this.deps.onStarted?.({ preset: loop.preset });
243
+ }
244
+ tick(generation) {
245
+ const active = this.active;
246
+ if (!active || active.generation !== generation) return;
247
+ const elapsed = this.now() - active.startedAt;
248
+ const framesDue = Math.floor(elapsed / FRAME_MS) + 1 - active.framesSent;
249
+ const toSend = Math.min(Math.max(framesDue, 0), MAX_BURST_FRAMES);
250
+ for (let i = 0; i < toSend; i++) {
251
+ const { frame, nextOffset } = this.extractFrame(active.scaled, active.offset);
252
+ active.offset = nextOffset;
253
+ let payload = frame;
254
+ if (active.fadeInFramesLeft > 0) {
255
+ const total = Math.ceil(active.fadeInMs / FRAME_MS);
256
+ const index = total - active.fadeInFramesLeft;
257
+ payload = fadeMulaw(frame, index / total, (index + 1) / total);
258
+ active.fadeInFramesLeft--;
259
+ }
260
+ this.deps.sendMedia(Buffer.from(payload).toString("base64"));
261
+ active.framesSent++;
262
+ }
263
+ }
264
+ /** Wrap-around frame extraction for seamless looping. */
265
+ extractFrame(buffer, offset) {
266
+ const size = 160;
267
+ if (offset + size <= buffer.length) return {
268
+ frame: new Uint8Array(buffer.buffer, buffer.byteOffset + offset, size),
269
+ nextOffset: (offset + size) % buffer.length
270
+ };
271
+ const frame = new Uint8Array(size);
272
+ const tail = buffer.length - offset;
273
+ frame.set(new Uint8Array(buffer.buffer, buffer.byteOffset + offset, tail), 0);
274
+ frame.set(new Uint8Array(buffer.buffer, buffer.byteOffset, size - tail), tail);
275
+ return {
276
+ frame,
277
+ nextOffset: size - tail
278
+ };
279
+ }
280
+ };
281
+ //#endregion
282
+ export { scaleMulaw as a, fadeMulaw as i, isPresetName as n, loadBackgroundAudio as r, BackgroundAudioPlayer as t };
@@ -0,0 +1,313 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
2
+ const require_mulaw = require("./mulaw-DLUObjdP.cjs");
3
+ let node_fs = require("node:fs");
4
+ let node_path = require("node:path");
5
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
6
+ let node_url = require("node:url");
7
+ //#region src/audio/gain.ts
8
+ /**
9
+ * Gain application on μ-law frames, used for background-audio fades and volume.
10
+ * μ-law is logarithmic, so gain must ramp in the linear domain:
11
+ * decode → scale → re-encode.
12
+ */
13
+ /** Scale an entire μ-law frame by a constant linear gain. */
14
+ function scaleMulaw(frame, gain) {
15
+ if (gain === 1) return frame;
16
+ const out = new Uint8Array(frame.length);
17
+ for (let i = 0; i < frame.length; i++) out[i] = require_mulaw.mulawEncodeSample(clamp16(require_mulaw.mulawDecodeSample(frame[i]) * gain));
18
+ return out;
19
+ }
20
+ /**
21
+ * Apply a linearly interpolated gain ramp across a μ-law frame
22
+ * (`startGain` at the first sample → `endGain` at the last).
23
+ */
24
+ function fadeMulaw(frame, startGain, endGain) {
25
+ if (startGain === endGain) return scaleMulaw(frame, startGain);
26
+ const out = new Uint8Array(frame.length);
27
+ const step = frame.length > 1 ? (endGain - startGain) / (frame.length - 1) : 0;
28
+ for (let i = 0; i < frame.length; i++) {
29
+ const gain = startGain + step * i;
30
+ out[i] = require_mulaw.mulawEncodeSample(clamp16(require_mulaw.mulawDecodeSample(frame[i]) * gain));
31
+ }
32
+ return out;
33
+ }
34
+ function clamp16(v) {
35
+ const r = Math.round(v);
36
+ if (r > 32767) return 32767;
37
+ if (r < -32768) return -32768;
38
+ return r;
39
+ }
40
+ //#endregion
41
+ //#region src/audio/background/loader.ts
42
+ /**
43
+ * Preset asset loading. Assets are raw 8 kHz μ-law loops in the package's
44
+ * `assets/` directory (generated by scripts/generate-tones.ts and shipped in
45
+ * the npm tarball). Custom sources accept a Buffer or a file path.
46
+ */
47
+ const PRESET_FILES = {
48
+ "elevator-jazz": "elevator-jazz.ulaw",
49
+ lofi: "lofi.ulaw",
50
+ "keyboard-typing": "keyboard-typing.ulaw",
51
+ "thinking-hum": "thinking-hum.ulaw",
52
+ ringing: "ringing.ulaw"
53
+ };
54
+ let cachedAssetsDir = null;
55
+ const bufferCache = /* @__PURE__ */ new Map();
56
+ function assetsDir() {
57
+ if (cachedAssetsDir) return cachedAssetsDir;
58
+ const here = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
59
+ const candidates = [
60
+ node_path.default.resolve(here, "../assets"),
61
+ node_path.default.resolve(here, "../../assets"),
62
+ node_path.default.resolve(here, "../../../assets")
63
+ ];
64
+ for (const candidate of candidates) if ((0, node_fs.existsSync)(candidate)) {
65
+ cachedAssetsDir = candidate;
66
+ return candidate;
67
+ }
68
+ throw new Error(`background-audio assets directory not found (looked in: ${candidates.join(", ")})`);
69
+ }
70
+ function isPresetName(spec) {
71
+ return typeof spec === "string";
72
+ }
73
+ /** Resolve a spec to raw μ-law bytes (cached per preset/path). */
74
+ function loadBackgroundAudio(spec) {
75
+ if (isPresetName(spec)) {
76
+ const file = PRESET_FILES[spec];
77
+ if (!file) throw new Error(`unknown background-audio preset "${spec}"`);
78
+ const full = node_path.default.join(assetsDir(), file);
79
+ let buffer = bufferCache.get(full);
80
+ if (!buffer) {
81
+ buffer = (0, node_fs.readFileSync)(full);
82
+ bufferCache.set(full, buffer);
83
+ }
84
+ return buffer;
85
+ }
86
+ if (Buffer.isBuffer(spec.custom)) return spec.custom;
87
+ let buffer = bufferCache.get(spec.custom);
88
+ if (!buffer) {
89
+ buffer = (0, node_fs.readFileSync)(spec.custom);
90
+ bufferCache.set(spec.custom, buffer);
91
+ }
92
+ return buffer;
93
+ }
94
+ function presetLabel(spec) {
95
+ return isPresetName(spec) ? spec : void 0;
96
+ }
97
+ //#endregion
98
+ //#region src/audio/background/BackgroundAudioPlayer.ts
99
+ /**
100
+ * BackgroundAudioPlayer — paced μ-law loop injection into the Twilio stream.
101
+ *
102
+ * Twilio has ONE sequential playout buffer, so background audio must be paced
103
+ * in near-realtime (never bulk-queued): frames-due are computed from the wall
104
+ * clock each tick (drift-corrected), with a small burst cap, keeping at most
105
+ * a few frames buffered ahead. That way, when real agent audio arrives it
106
+ * queues at most ~2 frames behind the hold loop instead of seconds.
107
+ *
108
+ * Safety rails from production systems: a generation counter invalidates
109
+ * orphan intervals after stop/restart races; a hard max-duration failsafe
110
+ * stops a hung tool from looping forever; refcounted acquire/release lets
111
+ * concurrent tools share one loop; a start delay keeps fast tools silent.
112
+ */
113
+ const FRAME_MS = 20;
114
+ const MAX_BURST_FRAMES = 5;
115
+ var BackgroundAudioPlayer = class {
116
+ deps;
117
+ now;
118
+ generation = 0;
119
+ active = null;
120
+ startDelayTimer = null;
121
+ /** Tool-call ids currently holding the loop. */
122
+ holders = /* @__PURE__ */ new Set();
123
+ pendingSpec = null;
124
+ constructor(deps) {
125
+ this.deps = deps;
126
+ this.now = deps.now ?? Date.now;
127
+ }
128
+ get isPlaying() {
129
+ return this.active !== null;
130
+ }
131
+ /**
132
+ * Refcounted acquisition (one holder per tool call). The loop starts after
133
+ * `startDelayMs` unless every holder releases first.
134
+ */
135
+ acquire(holderId, spec, options = {}) {
136
+ this.holders.add(holderId);
137
+ if (this.active || this.startDelayTimer) return;
138
+ this.pendingSpec = {
139
+ spec,
140
+ options
141
+ };
142
+ const delay = options.startDelayMs ?? 1e3;
143
+ if (delay <= 0) {
144
+ this.startNow();
145
+ return;
146
+ }
147
+ this.startDelayTimer = setTimeout(() => {
148
+ this.startDelayTimer = null;
149
+ if (this.holders.size > 0) this.startNow();
150
+ }, delay);
151
+ this.startDelayTimer.unref?.();
152
+ }
153
+ /** Release one holder; the loop stops when the last holder releases. */
154
+ release(holderId, opts = {}) {
155
+ this.holders.delete(holderId);
156
+ if (this.holders.size > 0) return;
157
+ this.stop({ immediate: opts.immediate });
158
+ }
159
+ /** Direct start (facade `playBackgroundAudio`). Bypasses refcounting. */
160
+ start(spec, options = {}) {
161
+ this.holders.add("__manual__");
162
+ this.cancelDelay();
163
+ this.pendingSpec = {
164
+ spec,
165
+ options
166
+ };
167
+ this.startNow();
168
+ }
169
+ /**
170
+ * Stop the loop. Fade-out is applied when `immediate` is false and a
171
+ * fadeOutMs was configured; agent-audio preemption should pass
172
+ * `immediate: true` (a fade would delay real speech behind it).
173
+ */
174
+ stop(opts = {}) {
175
+ this.holders.clear();
176
+ this.cancelDelay();
177
+ this.pendingSpec = null;
178
+ const active = this.active;
179
+ if (!active) return;
180
+ this.generation++;
181
+ clearInterval(active.interval);
182
+ clearTimeout(active.failsafe);
183
+ this.active = null;
184
+ const fadeOutMs = opts.immediate ? 0 : active.fadeOutMs ?? 0;
185
+ if (fadeOutMs > 0) {
186
+ const frames = Math.min(Math.ceil(fadeOutMs / FRAME_MS), 25);
187
+ let offset = active.offset;
188
+ for (let i = 0; i < frames; i++) {
189
+ const frame = this.extractFrame(active.scaled, offset);
190
+ offset = frame.nextOffset;
191
+ const gainStart = 1 - i / frames;
192
+ const gainEnd = 1 - (i + 1) / frames;
193
+ this.deps.sendMedia(Buffer.from(fadeMulaw(frame.frame, gainStart, gainEnd)).toString("base64"));
194
+ }
195
+ }
196
+ this.deps.onStopped?.({ preset: active.preset });
197
+ }
198
+ /** Real agent audio arrived — kill the loop instantly, no fade, no clear. */
199
+ notifyAgentAudio() {
200
+ if (!this.active && !this.startDelayTimer) return;
201
+ this.stop({ immediate: true });
202
+ }
203
+ cancelDelay() {
204
+ if (this.startDelayTimer) {
205
+ clearTimeout(this.startDelayTimer);
206
+ this.startDelayTimer = null;
207
+ }
208
+ }
209
+ startNow() {
210
+ if (this.active || !this.pendingSpec) return;
211
+ const { spec, options } = this.pendingSpec;
212
+ this.pendingSpec = null;
213
+ let buffer;
214
+ try {
215
+ buffer = loadBackgroundAudio(spec);
216
+ } catch {
217
+ return;
218
+ }
219
+ if (buffer.length < 160) return;
220
+ const volume = options.volume ?? .4;
221
+ const scaled = volume === 1 ? buffer : Buffer.from(scaleMulaw(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length), volume));
222
+ const generation = ++this.generation;
223
+ const fadeInMs = options.fadeInMs ?? 200;
224
+ const loop = {
225
+ generation,
226
+ buffer,
227
+ scaled,
228
+ preset: presetLabel(spec),
229
+ offset: 0,
230
+ startedAt: this.now(),
231
+ framesSent: 0,
232
+ fadeInFramesLeft: Math.ceil(fadeInMs / FRAME_MS),
233
+ volume,
234
+ fadeInMs,
235
+ fadeOutMs: options.fadeOutMs ?? 200,
236
+ interval: setInterval(() => this.tick(generation), FRAME_MS),
237
+ failsafe: setTimeout(() => {
238
+ if (this.active?.generation === generation) this.stop({ immediate: true });
239
+ }, options.maxDurationMs ?? 6e4)
240
+ };
241
+ loop.interval.unref?.();
242
+ loop.failsafe.unref?.();
243
+ this.active = loop;
244
+ this.deps.onStarted?.({ preset: loop.preset });
245
+ }
246
+ tick(generation) {
247
+ const active = this.active;
248
+ if (!active || active.generation !== generation) return;
249
+ const elapsed = this.now() - active.startedAt;
250
+ const framesDue = Math.floor(elapsed / FRAME_MS) + 1 - active.framesSent;
251
+ const toSend = Math.min(Math.max(framesDue, 0), MAX_BURST_FRAMES);
252
+ for (let i = 0; i < toSend; i++) {
253
+ const { frame, nextOffset } = this.extractFrame(active.scaled, active.offset);
254
+ active.offset = nextOffset;
255
+ let payload = frame;
256
+ if (active.fadeInFramesLeft > 0) {
257
+ const total = Math.ceil(active.fadeInMs / FRAME_MS);
258
+ const index = total - active.fadeInFramesLeft;
259
+ payload = fadeMulaw(frame, index / total, (index + 1) / total);
260
+ active.fadeInFramesLeft--;
261
+ }
262
+ this.deps.sendMedia(Buffer.from(payload).toString("base64"));
263
+ active.framesSent++;
264
+ }
265
+ }
266
+ /** Wrap-around frame extraction for seamless looping. */
267
+ extractFrame(buffer, offset) {
268
+ const size = 160;
269
+ if (offset + size <= buffer.length) return {
270
+ frame: new Uint8Array(buffer.buffer, buffer.byteOffset + offset, size),
271
+ nextOffset: (offset + size) % buffer.length
272
+ };
273
+ const frame = new Uint8Array(size);
274
+ const tail = buffer.length - offset;
275
+ frame.set(new Uint8Array(buffer.buffer, buffer.byteOffset + offset, tail), 0);
276
+ frame.set(new Uint8Array(buffer.buffer, buffer.byteOffset, size - tail), tail);
277
+ return {
278
+ frame,
279
+ nextOffset: size - tail
280
+ };
281
+ }
282
+ };
283
+ //#endregion
284
+ Object.defineProperty(exports, "BackgroundAudioPlayer", {
285
+ enumerable: true,
286
+ get: function() {
287
+ return BackgroundAudioPlayer;
288
+ }
289
+ });
290
+ Object.defineProperty(exports, "fadeMulaw", {
291
+ enumerable: true,
292
+ get: function() {
293
+ return fadeMulaw;
294
+ }
295
+ });
296
+ Object.defineProperty(exports, "isPresetName", {
297
+ enumerable: true,
298
+ get: function() {
299
+ return isPresetName;
300
+ }
301
+ });
302
+ Object.defineProperty(exports, "loadBackgroundAudio", {
303
+ enumerable: true,
304
+ get: function() {
305
+ return loadBackgroundAudio;
306
+ }
307
+ });
308
+ Object.defineProperty(exports, "scaleMulaw", {
309
+ enumerable: true,
310
+ get: function() {
311
+ return scaleMulaw;
312
+ }
313
+ });
@@ -0,0 +1,62 @@
1
+ import { t as TypedEmitter } from "./events-BylBSBW-.mjs";
2
+ //#region src/logging/logger.ts
3
+ const noopLogger = {
4
+ debug: () => {},
5
+ info: () => {},
6
+ warn: () => {},
7
+ error: () => {},
8
+ child: () => noopLogger
9
+ };
10
+ /** Console-backed logger, useful in examples and debugging. */
11
+ function consoleLogger(context = {}) {
12
+ const prefix = Object.keys(context).length ? ` ${JSON.stringify(context)}` : "";
13
+ return {
14
+ debug: (msg, data) => console.debug(`[debug]${prefix} ${msg}`, data ?? ""),
15
+ info: (msg, data) => console.info(`[info]${prefix} ${msg}`, data ?? ""),
16
+ warn: (msg, data) => console.warn(`[warn]${prefix} ${msg}`, data ?? ""),
17
+ error: (msg, data) => console.error(`[error]${prefix} ${msg}`, data ?? ""),
18
+ child: (extra) => consoleLogger({
19
+ ...context,
20
+ ...extra
21
+ })
22
+ };
23
+ }
24
+ function childLogger(logger, context) {
25
+ return logger.child ? logger.child(context) : logger;
26
+ }
27
+ //#endregion
28
+ //#region src/internal/merge.ts
29
+ /** Recursive plain-object merge; arrays and non-objects replace wholesale. */
30
+ function deepMerge(base, patch) {
31
+ if (!patch) return base;
32
+ const out = { ...base };
33
+ for (const [key, value] of Object.entries(patch)) {
34
+ const existing = out[key];
35
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) out[key] = deepMerge(existing, value);
36
+ else out[key] = value;
37
+ }
38
+ return out;
39
+ }
40
+ //#endregion
41
+ //#region src/providers/base/BaseRealtimeProvider.ts
42
+ /**
43
+ * The provider contract: μ-law in, μ-law out, normalized events.
44
+ * Transcoding (when the provider speaks PCM) lives inside the provider, so
45
+ * the engine stays codec-free. `connect()` is re-invokable on the same
46
+ * instance — the session layer owns reconnect policy and context re-injection.
47
+ */
48
+ var BaseRealtimeProvider = class extends TypedEmitter {
49
+ /** Interrupt in-flight generation (where the wire protocol supports it). */
50
+ cancelResponse() {}
51
+ /** Trim the last assistant item to what the caller actually heard. */
52
+ truncatePlayback(_itemId, _audioEndMs) {}
53
+ /**
54
+ * Whether the most recent connect restored server-side context (session
55
+ * resumption). When true, the engine skips transcript re-injection.
56
+ */
57
+ get didResume() {
58
+ return false;
59
+ }
60
+ };
61
+ //#endregion
62
+ export { noopLogger as a, consoleLogger as i, deepMerge as n, childLogger as r, BaseRealtimeProvider as t };