@voiceloop/web-sdk 0.2.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,642 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
10
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11
+ var __spreadValues = (a, b) => {
12
+ for (var prop in b || (b = {}))
13
+ if (__hasOwnProp.call(b, prop))
14
+ __defNormalProp(a, prop, b[prop]);
15
+ if (__getOwnPropSymbols)
16
+ for (var prop of __getOwnPropSymbols(b)) {
17
+ if (__propIsEnum.call(b, prop))
18
+ __defNormalProp(a, prop, b[prop]);
19
+ }
20
+ return a;
21
+ };
22
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
23
+ var __objRest = (source, exclude) => {
24
+ var target = {};
25
+ for (var prop in source)
26
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
27
+ target[prop] = source[prop];
28
+ if (source != null && __getOwnPropSymbols)
29
+ for (var prop of __getOwnPropSymbols(source)) {
30
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
31
+ target[prop] = source[prop];
32
+ }
33
+ return target;
34
+ };
35
+ var __export = (target, all) => {
36
+ for (var name in all)
37
+ __defProp(target, name, { get: all[name], enumerable: true });
38
+ };
39
+ var __copyProps = (to, from, except, desc) => {
40
+ if (from && typeof from === "object" || typeof from === "function") {
41
+ for (let key of __getOwnPropNames(from))
42
+ if (!__hasOwnProp.call(to, key) && key !== except)
43
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
44
+ }
45
+ return to;
46
+ };
47
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
48
+
49
+ // src/index.ts
50
+ var src_exports = {};
51
+ __export(src_exports, {
52
+ MIC_SAMPLE_RATE: () => MIC_SAMPLE_RATE,
53
+ TTS_SAMPLE_RATE: () => TTS_SAMPLE_RATE,
54
+ VoiceLoopClient: () => VoiceLoopClient,
55
+ clearLog: () => clearLog,
56
+ createInitialState: () => createInitialState,
57
+ createPcmPlayer: () => createPcmPlayer,
58
+ createStateMachineContext: () => createStateMachineContext,
59
+ formatForCopy: () => formatForCopy,
60
+ getEntries: () => getEntries,
61
+ log: () => log,
62
+ logError: () => logError,
63
+ reduceServerEvent: () => reduceServerEvent,
64
+ startMicCapture: () => startMicCapture,
65
+ subscribe: () => subscribe,
66
+ warn: () => warn
67
+ });
68
+ module.exports = __toCommonJS(src_exports);
69
+
70
+ // src/logger.ts
71
+ var MAX_ENTRIES = 300;
72
+ var seq = 0;
73
+ var entries = [];
74
+ var listeners = /* @__PURE__ */ new Set();
75
+ function emit() {
76
+ listeners.forEach((fn) => fn());
77
+ }
78
+ function log(tag, msg, detail, level = "info") {
79
+ entries.push({ id: seq++, ts: Date.now(), level, tag, msg, detail });
80
+ if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES);
81
+ emit();
82
+ }
83
+ function warn(tag, msg, detail) {
84
+ log(tag, msg, detail, "warn");
85
+ }
86
+ function logError(tag, msg, detail) {
87
+ log(tag, msg, detail, "error");
88
+ }
89
+ function getEntries() {
90
+ return entries;
91
+ }
92
+ function clearLog() {
93
+ entries.length = 0;
94
+ seq = 0;
95
+ emit();
96
+ }
97
+ function subscribe(fn) {
98
+ listeners.add(fn);
99
+ return () => listeners.delete(fn);
100
+ }
101
+ function formatForCopy() {
102
+ return entries.map((e) => {
103
+ const d = new Date(e.ts);
104
+ const ts = [
105
+ d.getHours().toString().padStart(2, "0"),
106
+ d.getMinutes().toString().padStart(2, "0"),
107
+ d.getSeconds().toString().padStart(2, "0")
108
+ ].join(":") + "." + d.getMilliseconds().toString().padStart(3, "0");
109
+ const detail = e.detail ? ` ${e.detail}` : "";
110
+ return `[${ts}] ${e.tag.padEnd(8)} ${e.msg}${detail}`;
111
+ }).join("\n");
112
+ }
113
+
114
+ // src/audio.ts
115
+ var MIC_SAMPLE_RATE = 16e3;
116
+ var TTS_SAMPLE_RATE = 24e3;
117
+ var SCRIPT_PROCESSOR_FRAMES = 1024;
118
+ async function startMicCapture(onPcm) {
119
+ const stream = await navigator.mediaDevices.getUserMedia({
120
+ audio: {
121
+ sampleRate: MIC_SAMPLE_RATE,
122
+ channelCount: 1,
123
+ echoCancellation: true,
124
+ noiseSuppression: true,
125
+ autoGainControl: true
126
+ }
127
+ });
128
+ const ctx = new AudioContext({ sampleRate: MIC_SAMPLE_RATE });
129
+ const source = ctx.createMediaStreamSource(stream);
130
+ const processor = ctx.createScriptProcessor(SCRIPT_PROCESSOR_FRAMES, 1, 1);
131
+ let lastVolume = 0;
132
+ processor.onaudioprocess = (e) => {
133
+ const f32 = e.inputBuffer.getChannelData(0);
134
+ let sum = 0;
135
+ for (let i = 0; i < f32.length; i++) sum += f32[i] * f32[i];
136
+ lastVolume = Math.min(1, Math.sqrt(sum / f32.length) * 10);
137
+ const i16 = new Int16Array(f32.length);
138
+ for (let i = 0; i < f32.length; i++) {
139
+ i16[i] = Math.max(-32768, Math.min(32767, f32[i] * 32768));
140
+ }
141
+ onPcm(i16.buffer);
142
+ };
143
+ source.connect(processor);
144
+ processor.connect(ctx.destination);
145
+ return {
146
+ stop() {
147
+ processor.disconnect();
148
+ source.disconnect();
149
+ ctx.close();
150
+ stream.getTracks().forEach((t) => t.stop());
151
+ lastVolume = 0;
152
+ },
153
+ getVolume: () => lastVolume
154
+ };
155
+ }
156
+ function createPcmPlayer() {
157
+ let ctx = null;
158
+ let nextPlayTime = 0;
159
+ function ensureCtx() {
160
+ if (!ctx) ctx = new AudioContext({ sampleRate: TTS_SAMPLE_RATE });
161
+ if (ctx.state === "suspended") ctx.resume();
162
+ return ctx;
163
+ }
164
+ return {
165
+ resume() {
166
+ ensureCtx();
167
+ },
168
+ enqueue(pcm) {
169
+ const ac = ensureCtx();
170
+ const byteLen = pcm.byteLength & ~1;
171
+ if (byteLen === 0) return;
172
+ const buf = new ArrayBuffer(byteLen);
173
+ new Uint8Array(buf).set(pcm.subarray(0, byteLen));
174
+ const i16 = new Int16Array(buf);
175
+ const f32 = new Float32Array(i16.length);
176
+ for (let i = 0; i < i16.length; i++) f32[i] = i16[i] / 32768;
177
+ const audioBuf = ac.createBuffer(1, f32.length, TTS_SAMPLE_RATE);
178
+ audioBuf.copyToChannel(f32, 0);
179
+ const src = ac.createBufferSource();
180
+ src.buffer = audioBuf;
181
+ src.connect(ac.destination);
182
+ const startAt = Math.max(ac.currentTime, nextPlayTime);
183
+ src.start(startAt);
184
+ nextPlayTime = startAt + audioBuf.duration;
185
+ },
186
+ flush() {
187
+ if (ctx) {
188
+ ctx.close();
189
+ ctx = null;
190
+ }
191
+ nextPlayTime = 0;
192
+ }
193
+ };
194
+ }
195
+
196
+ // src/state-machine.ts
197
+ function createStateMachineContext() {
198
+ return {
199
+ assistantBubbleOpen: false,
200
+ userTurnActive: false,
201
+ incompleteAccum: "",
202
+ hasIncompleteUserTurn: false,
203
+ preLLMText: ""
204
+ };
205
+ }
206
+ function patch(state, p) {
207
+ return __spreadValues(__spreadValues({}, state), p);
208
+ }
209
+ function finaliseLastUtterance(state, role, finalText) {
210
+ const list = [...state.utterances];
211
+ for (let i = list.length - 1; i >= 0; i--) {
212
+ if (list[i].role === role && list[i].partial) {
213
+ list[i] = __spreadProps(__spreadValues({}, list[i]), {
214
+ text: finalText != null ? finalText : list[i].text,
215
+ partial: false,
216
+ fillerPrefix: void 0
217
+ });
218
+ break;
219
+ }
220
+ }
221
+ return __spreadProps(__spreadValues({}, state), { utterances: list });
222
+ }
223
+ function upsertAssistantBubble(state, update) {
224
+ const list = [...state.utterances];
225
+ for (let i = list.length - 1; i >= 0; i--) {
226
+ if (list[i].role === "assistant" && list[i].partial) {
227
+ list[i] = update(list[i]);
228
+ return __spreadProps(__spreadValues({}, state), { utterances: list });
229
+ }
230
+ }
231
+ const created = update(null);
232
+ return __spreadProps(__spreadValues({}, state), { utterances: [...list, created] });
233
+ }
234
+ function reduceServerEvent(state, ctx, evt) {
235
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
236
+ ctx = __spreadValues({}, ctx);
237
+ const effects = [];
238
+ switch (evt.type) {
239
+ case "session.config":
240
+ log("session", `config brain=${evt.brain_mode} vad_stop=${evt.turn_policy.vad_stop_secs * 1e3}ms eot=${evt.turn_policy.eot_strategy} smart_eot=${evt.capabilities.smart_eot}`);
241
+ state = patch(state, { session: "idle", error: null });
242
+ break;
243
+ case "transcript.partial": {
244
+ state = patch(state, { session: "listening" });
245
+ const list = [...state.utterances];
246
+ const last = list.length > 0 ? list[list.length - 1] : null;
247
+ const prefix = ctx.incompleteAccum;
248
+ const display = prefix ? prefix + evt.text : evt.text;
249
+ if ((last == null ? void 0 : last.role) === "user" && last.partial) {
250
+ list[list.length - 1] = __spreadProps(__spreadValues({}, last), { text: display });
251
+ } else {
252
+ list.push({ role: "user", text: display, partial: true });
253
+ }
254
+ state = __spreadProps(__spreadValues({}, state), { utterances: list });
255
+ break;
256
+ }
257
+ case "transcript.final": {
258
+ const prefix = ctx.incompleteAccum;
259
+ const display = prefix ? prefix + evt.text : evt.text;
260
+ log("asr", `final "${evt.text}"`);
261
+ ctx.preLLMText = ctx.preLLMText ? ctx.preLLMText + display : display;
262
+ ctx.userTurnActive = false;
263
+ break;
264
+ }
265
+ case "transcript.timing":
266
+ log("asr", `timing wait=${evt.wait_ms}ms`);
267
+ {
268
+ const list = [...state.utterances];
269
+ for (let i = list.length - 1; i >= 0; i--) {
270
+ if (list[i].role === "user") {
271
+ const prev = list[i];
272
+ list[i] = __spreadProps(__spreadValues({}, prev), { waitTimes: [...(_a = prev.waitTimes) != null ? _a : [], evt.wait_ms] });
273
+ break;
274
+ }
275
+ }
276
+ state = __spreadProps(__spreadValues({}, state), { utterances: list });
277
+ }
278
+ break;
279
+ case "transcript.incomplete":
280
+ log("asr", `incomplete "${evt.text}" wait=${evt.wait_ms}ms`);
281
+ ctx.incompleteAccum = (ctx.incompleteAccum || "") + evt.text;
282
+ ctx.hasIncompleteUserTurn = true;
283
+ {
284
+ const list = [...state.utterances];
285
+ for (let i = list.length - 1; i >= 0; i--) {
286
+ if (list[i].role === "user") {
287
+ const prev = list[i];
288
+ list[i] = __spreadProps(__spreadValues({}, prev), {
289
+ text: ctx.incompleteAccum,
290
+ partial: true,
291
+ incomplete: true,
292
+ waitTimes: [...(_b = prev.waitTimes) != null ? _b : [], ...evt.wait_ms != null ? [evt.wait_ms] : []]
293
+ });
294
+ break;
295
+ }
296
+ }
297
+ state = __spreadProps(__spreadValues({}, state), { utterances: list });
298
+ }
299
+ state = patch(state, { session: "idle" });
300
+ break;
301
+ case "turn.started":
302
+ log("turn", "started");
303
+ ctx.userTurnActive = true;
304
+ if (ctx.assistantBubbleOpen) {
305
+ ctx.assistantBubbleOpen = false;
306
+ state = finaliseLastUtterance(state, "assistant");
307
+ }
308
+ if (!ctx.hasIncompleteUserTurn) {
309
+ ctx.incompleteAccum = "";
310
+ }
311
+ state = patch(state, { session: "listening" });
312
+ break;
313
+ case "turn.ended":
314
+ log("turn", `ended transcript="${(_c = evt.transcript) != null ? _c : ""}"`);
315
+ if (!ctx.hasIncompleteUserTurn) {
316
+ state = finaliseLastUtterance(state, "user", ctx.preLLMText || void 0);
317
+ ctx.incompleteAccum = "";
318
+ ctx.preLLMText = "";
319
+ }
320
+ state = patch(state, { session: "thinking" });
321
+ break;
322
+ case "filler.started":
323
+ log("filler", `started id=${evt.id} text="${evt.text}"`);
324
+ state = patch(state, { session: "speaking" });
325
+ state = upsertAssistantBubble(state, (prev) => {
326
+ if (!prev) {
327
+ return { role: "assistant", text: evt.text, partial: true, fillerPrefix: evt.text };
328
+ }
329
+ if (prev.fillerPrefix || prev.text.startsWith(evt.text)) {
330
+ return prev;
331
+ }
332
+ const body = prev.text;
333
+ return __spreadProps(__spreadValues({}, prev), {
334
+ fillerPrefix: evt.text,
335
+ text: body ? `${evt.text} ${body}` : evt.text
336
+ });
337
+ });
338
+ ctx.assistantBubbleOpen = true;
339
+ break;
340
+ case "filler.stopped":
341
+ log("filler", `stopped id=${evt.id}`);
342
+ break;
343
+ case "filler.skipped":
344
+ log("filler", `skipped wait_ms=${evt.wait_ms}`);
345
+ break;
346
+ case "assistant.delta":
347
+ state = patch(state, { session: "speaking" });
348
+ state = upsertAssistantBubble(state, (prev) => {
349
+ var _a2;
350
+ if (!prev) {
351
+ return { role: "assistant", text: evt.text, partial: true };
352
+ }
353
+ const prefix = (_a2 = prev.fillerPrefix) != null ? _a2 : "";
354
+ const body = prefix && prev.text.startsWith(prefix) ? prev.text.slice(prefix.length).trimStart() : prev.text;
355
+ const joined = body + evt.text;
356
+ return __spreadProps(__spreadValues({}, prev), {
357
+ text: prefix ? `${prefix} ${joined}` : joined
358
+ });
359
+ });
360
+ ctx.assistantBubbleOpen = true;
361
+ break;
362
+ case "tts.started":
363
+ log("tts", "started");
364
+ state = patch(state, { session: "speaking" });
365
+ break;
366
+ case "tts.stopped":
367
+ log("tts", `stopped reason=${evt.reason}`);
368
+ break;
369
+ case "turn.metrics": {
370
+ const prevCurrent = state.latency.current;
371
+ const merged = prevCurrent && prevCurrent.turn_id === evt.turn_id ? { turn_id: evt.turn_id, ftl: (_d = evt.ftl) != null ? _d : prevCurrent.ftl, il: (_e = evt.il) != null ? _e : prevCurrent.il, ts: Date.now() } : { turn_id: evt.turn_id, ftl: evt.ftl, il: evt.il, ts: Date.now() };
372
+ const history = ((_f = state.latency.history[0]) == null ? void 0 : _f.turn_id) === evt.turn_id ? [merged, ...state.latency.history.slice(1)] : [merged, ...state.latency.history].slice(0, 50);
373
+ state = __spreadProps(__spreadValues({}, state), { latency: { current: merged, history } });
374
+ log("turn", `metrics ftl=${(_h = (_g = evt.ftl) == null ? void 0 : _g.total_ms) != null ? _h : "-"}ms il=${(_j = (_i = evt.il) == null ? void 0 : _i.total_ms) != null ? _j : "-"}ms`);
375
+ break;
376
+ }
377
+ case "assistant.done":
378
+ log("turn", "assistant done");
379
+ ctx.assistantBubbleOpen = false;
380
+ state = finaliseLastUtterance(state, "assistant");
381
+ if (ctx.hasIncompleteUserTurn) {
382
+ state = finaliseLastUtterance(state, "user");
383
+ ctx.incompleteAccum = "";
384
+ ctx.hasIncompleteUserTurn = false;
385
+ }
386
+ ctx.preLLMText = "";
387
+ state = patch(state, { session: "idle" });
388
+ break;
389
+ case "interrupted":
390
+ log("turn", "interrupted");
391
+ effects.push({ kind: "flush_player" });
392
+ ctx.assistantBubbleOpen = false;
393
+ state = finaliseLastUtterance(state, "assistant");
394
+ if (ctx.hasIncompleteUserTurn) {
395
+ ctx.incompleteAccum = "";
396
+ ctx.hasIncompleteUserTurn = false;
397
+ }
398
+ ctx.preLLMText = "";
399
+ state = patch(state, { session: "listening" });
400
+ break;
401
+ case "error":
402
+ logError("ws", evt.message);
403
+ state = patch(state, { error: evt.message });
404
+ break;
405
+ case "brain.request":
406
+ warn("ws", "unexpected brain.request in state machine");
407
+ break;
408
+ }
409
+ return { state, ctx, effects };
410
+ }
411
+
412
+ // src/session-state.ts
413
+ function createInitialState() {
414
+ return {
415
+ connection: "disconnected",
416
+ session: "idle",
417
+ utterances: [],
418
+ volume: 0,
419
+ error: null,
420
+ latency: { current: null, history: [] }
421
+ };
422
+ }
423
+
424
+ // src/client.ts
425
+ var TypedEmitter = class {
426
+ constructor() {
427
+ this._listeners = /* @__PURE__ */ new Map();
428
+ }
429
+ on(event, fn) {
430
+ if (!this._listeners.has(event)) this._listeners.set(event, /* @__PURE__ */ new Set());
431
+ this._listeners.get(event).add(fn);
432
+ return this;
433
+ }
434
+ off(event, fn) {
435
+ var _a;
436
+ (_a = this._listeners.get(event)) == null ? void 0 : _a.delete(fn);
437
+ return this;
438
+ }
439
+ emit(event, ...args) {
440
+ var _a;
441
+ (_a = this._listeners.get(event)) == null ? void 0 : _a.forEach((fn) => fn(...args));
442
+ }
443
+ };
444
+ var DEFAULT_SERVER = "http://localhost:9000";
445
+ var VOLUME_INTERVAL_MS = 50;
446
+ var VoiceLoopClient = class extends TypedEmitter {
447
+ constructor(opts = {}) {
448
+ var _a, _b, _c, _d;
449
+ super();
450
+ this._state = createInitialState();
451
+ this._ctx = createStateMachineContext();
452
+ this._ws = null;
453
+ this._mic = null;
454
+ this._volTimer = null;
455
+ this._serverUrl = (_a = opts.serverUrl) != null ? _a : DEFAULT_SERVER;
456
+ this._apiKey = (_b = opts.apiKey) != null ? _b : "dev-key";
457
+ this._turnPolicy = opts.turnPolicy;
458
+ this._onBrainRequest = opts.onBrainRequest;
459
+ this._micCapture = (_c = opts.micCapture) != null ? _c : startMicCapture;
460
+ this._player = ((_d = opts.playerFactory) != null ? _d : createPcmPlayer)();
461
+ }
462
+ get state() {
463
+ return this._state;
464
+ }
465
+ // ── Connection ──────────────────────────────────────────────────────────────
466
+ async connect() {
467
+ var _a;
468
+ if (this._ws) return;
469
+ this._player.resume();
470
+ log("session", `create server=${this._serverUrl}`);
471
+ this._applyPatch({ connection: "connecting", error: null, utterances: [] });
472
+ this._ctx = createStateMachineContext();
473
+ let token;
474
+ try {
475
+ let res;
476
+ try {
477
+ const _b = (_a = this._turnPolicy) != null ? _a : {}, { tts_options } = _b, policyRest = __objRest(_b, ["tts_options"]);
478
+ const sessionBody = __spreadValues(__spreadValues({
479
+ brain_mode: "client"
480
+ }, Object.keys(policyRest).length > 0 ? { turn_policy: policyRest } : {}), tts_options ? { tts_options } : {});
481
+ log("session", `create body=${JSON.stringify(sessionBody)}`);
482
+ res = await fetch(`${this._serverUrl}/v1/sessions`, {
483
+ method: "POST",
484
+ headers: {
485
+ "Content-Type": "application/json",
486
+ "Authorization": `Bearer ${this._apiKey}`
487
+ },
488
+ body: JSON.stringify(sessionBody)
489
+ });
490
+ } catch (e) {
491
+ throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u670D\u52A1\u5668\uFF08${this._serverUrl}\uFF09\uFF0C\u8BF7\u68C0\u67E5 serverUrl \u662F\u5426\u6B63\u786E\uFF0C\u4EE5\u53CA\u670D\u52A1\u662F\u5426\u5728\u8FD0\u884C`);
492
+ }
493
+ if (res.status === 401) throw new Error(`\u9274\u6743\u5931\u8D25\uFF08401\uFF09\uFF0C\u8BF7\u68C0\u67E5 apiKey \u662F\u5426\u6B63\u786E`);
494
+ if (res.status === 403) throw new Error(`\u65E0\u8BBF\u95EE\u6743\u9650\uFF08403\uFF09\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u786E\u8BA4 apiKey \u6743\u9650`);
495
+ if (res.status === 404) throw new Error(`\u63A5\u53E3\u4E0D\u5B58\u5728\uFF08404\uFF09\uFF0C\u8BF7\u68C0\u67E5 serverUrl \u662F\u5426\u6B63\u786E\uFF0C\u4E0D\u8981\u5E26\u8DEF\u5F84\u540E\u7F00`);
496
+ if (!res.ok) throw new Error(`\u670D\u52A1\u5668\u9519\u8BEF\uFF08${res.status}\uFF09\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u6392\u67E5\u670D\u52A1\u7AEF\u65E5\u5FD7`);
497
+ const data = await res.json();
498
+ token = data.session_token;
499
+ log("session", `created id=${data.session_id}`);
500
+ } catch (e) {
501
+ logError("session", `create failed ${e}`);
502
+ this._applyPatch({ connection: "disconnected", error: String(e) });
503
+ return;
504
+ }
505
+ const wsUrl = this._serverUrl.replace(/^http/, "ws") + `/ws?token=${token}`;
506
+ log("ws", `connecting ${wsUrl}`);
507
+ const ws = new WebSocket(wsUrl);
508
+ ws.binaryType = "arraybuffer";
509
+ this._ws = ws;
510
+ ws.onopen = async () => {
511
+ log("ws", "open");
512
+ this._applyPatch({ connection: "connected" });
513
+ try {
514
+ this._mic = await this._micCapture((chunk) => {
515
+ if (ws.readyState === WebSocket.OPEN) ws.send(chunk);
516
+ });
517
+ log("mic", "capture started");
518
+ this._volTimer = setInterval(() => {
519
+ var _a2, _b2;
520
+ this._applyPatch({ volume: (_b2 = (_a2 = this._mic) == null ? void 0 : _a2.getVolume()) != null ? _b2 : 0 });
521
+ }, VOLUME_INTERVAL_MS);
522
+ } catch (e) {
523
+ logError("mic", `capture failed ${e}`);
524
+ const name = e instanceof Error ? e.name : "";
525
+ const msg = name === "NotAllowedError" ? "\u9EA6\u514B\u98CE\u6743\u9650\u88AB\u62D2\u7EDD\uFF0C\u8BF7\u5728\u6D4F\u89C8\u5668\u5730\u5740\u680F\u5141\u8BB8\u9EA6\u514B\u98CE\u8BBF\u95EE" : name === "NotFoundError" ? "\u672A\u627E\u5230\u9EA6\u514B\u98CE\u8BBE\u5907\uFF0C\u8BF7\u786E\u8BA4\u8BBE\u5907\u5DF2\u8FDE\u63A5" : `\u9EA6\u514B\u98CE\u542F\u52A8\u5931\u8D25\uFF1A${e}`;
526
+ this._applyPatch({ error: msg });
527
+ ws.close();
528
+ }
529
+ };
530
+ ws.onmessage = (e) => {
531
+ if (e.data instanceof ArrayBuffer) {
532
+ this._player.enqueue(new Uint8Array(e.data));
533
+ return;
534
+ }
535
+ try {
536
+ const evt = JSON.parse(e.data);
537
+ if (evt.type === "brain.request") {
538
+ if (this._onBrainRequest) {
539
+ this._onBrainRequest(evt, (frame) => {
540
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(frame));
541
+ });
542
+ } else {
543
+ warn("ws", "brain.request received but no onBrainRequest handler provided");
544
+ this._applyPatch({ error: "\u6536\u5230 brain.request \u4F46\u672A\u63D0\u4F9B onBrainRequest \u56DE\u8C03\uFF0CAI \u5C06\u65E0\u6CD5\u56DE\u7B54\u3002\u8BF7\u5728 VoiceLoopClient \u521D\u59CB\u5316\u65F6\u4F20\u5165 onBrainRequest" });
545
+ }
546
+ return;
547
+ }
548
+ this._handleEvent(evt);
549
+ } catch (e2) {
550
+ warn("ws", "malformed frame", typeof e.data === "string" ? e.data.slice(0, 100) : "binary");
551
+ }
552
+ };
553
+ ws.onclose = (e) => {
554
+ log(
555
+ "ws",
556
+ `closed code=${e.code} reason=${e.reason || "(none)"}`,
557
+ void 0,
558
+ e.code >= 4e3 ? "warn" : "info"
559
+ );
560
+ this._cleanup();
561
+ if (this._ws !== null || this._state.connection !== "disconnected") {
562
+ this._ws = null;
563
+ this._applyPatch({ connection: "disconnected", session: "idle", volume: 0 });
564
+ }
565
+ };
566
+ ws.onerror = () => {
567
+ logError("ws", "error");
568
+ this._applyPatch({ error: `WebSocket \u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u8054\u7CFB\u7BA1\u7406\u5458\uFF08serverUrl: ${this._serverUrl}\uFF09` });
569
+ };
570
+ }
571
+ disconnect() {
572
+ log("session", "disconnect");
573
+ this._cleanup();
574
+ const ws = this._ws;
575
+ this._ws = null;
576
+ ws == null ? void 0 : ws.close();
577
+ this._applyPatch({ connection: "disconnected", session: "idle", volume: 0 });
578
+ }
579
+ // ── Control frames ──────────────────────────────────────────────────────────
580
+ barge() {
581
+ this._send({ type: "barge" });
582
+ }
583
+ endTurn(transcript) {
584
+ this._send(__spreadValues({ type: "turn.end" }, transcript ? { transcript } : {}));
585
+ }
586
+ say(text, voice) {
587
+ this._send(__spreadValues({ type: "say", text }, voice ? { voice } : {}));
588
+ }
589
+ // ── Private ─────────────────────────────────────────────────────────────────
590
+ _send(frame) {
591
+ var _a;
592
+ if (((_a = this._ws) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
593
+ this._ws.send(JSON.stringify(frame));
594
+ }
595
+ }
596
+ _handleEvent(evt) {
597
+ const { state, ctx, effects } = reduceServerEvent(this._state, this._ctx, evt);
598
+ this._ctx = ctx;
599
+ if (state !== this._state) {
600
+ this._state = state;
601
+ this.emit("state", state);
602
+ }
603
+ for (const effect of effects) {
604
+ if (effect.kind === "flush_player") {
605
+ this._player.flush();
606
+ }
607
+ }
608
+ }
609
+ _applyPatch(p) {
610
+ this._state = __spreadValues(__spreadValues({}, this._state), p);
611
+ this.emit("state", this._state);
612
+ }
613
+ _cleanup() {
614
+ var _a;
615
+ if (this._volTimer) {
616
+ clearInterval(this._volTimer);
617
+ this._volTimer = null;
618
+ }
619
+ (_a = this._mic) == null ? void 0 : _a.stop();
620
+ this._mic = null;
621
+ this._player.flush();
622
+ }
623
+ };
624
+ // Annotate the CommonJS export names for ESM import in node:
625
+ 0 && (module.exports = {
626
+ MIC_SAMPLE_RATE,
627
+ TTS_SAMPLE_RATE,
628
+ VoiceLoopClient,
629
+ clearLog,
630
+ createInitialState,
631
+ createPcmPlayer,
632
+ createStateMachineContext,
633
+ formatForCopy,
634
+ getEntries,
635
+ log,
636
+ logError,
637
+ reduceServerEvent,
638
+ startMicCapture,
639
+ subscribe,
640
+ warn
641
+ });
642
+ //# sourceMappingURL=index.cjs.map