@absolutejs/voice 0.0.22-beta.13 → 0.0.22-beta.130

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 (159) hide show
  1. package/README.md +779 -5
  2. package/dist/agent.d.ts +24 -0
  3. package/dist/agentSquadContract.d.ts +64 -0
  4. package/dist/angular/index.d.ts +9 -0
  5. package/dist/angular/index.js +1394 -46
  6. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  7. package/dist/angular/voice-controller.service.d.ts +1 -0
  8. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  9. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  10. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  11. package/dist/angular/voice-provider-status.service.d.ts +12 -0
  12. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  13. package/dist/angular/voice-stream.service.d.ts +3 -0
  14. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  15. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  16. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  17. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  18. package/dist/assistantHealth.d.ts +81 -0
  19. package/dist/audit.d.ts +128 -0
  20. package/dist/auditDeliveryRoutes.d.ts +85 -0
  21. package/dist/auditExport.d.ts +34 -0
  22. package/dist/auditRoutes.d.ts +66 -0
  23. package/dist/auditSinks.d.ts +133 -0
  24. package/dist/bargeInRoutes.d.ts +56 -0
  25. package/dist/campaign.d.ts +610 -0
  26. package/dist/campaignDialers.d.ts +90 -0
  27. package/dist/client/actions.d.ts +105 -0
  28. package/dist/client/bargeInMonitor.d.ts +7 -0
  29. package/dist/client/campaignDialerProof.d.ts +23 -0
  30. package/dist/client/connection.d.ts +3 -0
  31. package/dist/client/duplex.d.ts +1 -1
  32. package/dist/client/htmxBootstrap.js +697 -15
  33. package/dist/client/index.d.ts +40 -0
  34. package/dist/client/index.js +2138 -10
  35. package/dist/client/liveTurnLatency.d.ts +41 -0
  36. package/dist/client/opsStatus.d.ts +19 -0
  37. package/dist/client/opsStatusWidget.d.ts +40 -0
  38. package/dist/client/providerCapabilities.d.ts +19 -0
  39. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  40. package/dist/client/providerSimulationControls.d.ts +33 -0
  41. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  42. package/dist/client/providerStatus.d.ts +19 -0
  43. package/dist/client/providerStatusWidget.d.ts +32 -0
  44. package/dist/client/routingStatus.d.ts +19 -0
  45. package/dist/client/routingStatusWidget.d.ts +28 -0
  46. package/dist/client/traceTimeline.d.ts +19 -0
  47. package/dist/client/traceTimelineWidget.d.ts +32 -0
  48. package/dist/client/turnLatency.d.ts +22 -0
  49. package/dist/client/turnLatencyWidget.d.ts +33 -0
  50. package/dist/client/turnQuality.d.ts +19 -0
  51. package/dist/client/turnQualityWidget.d.ts +32 -0
  52. package/dist/client/workflowStatus.d.ts +19 -0
  53. package/dist/dataControl.d.ts +47 -0
  54. package/dist/demoReadyRoutes.d.ts +98 -0
  55. package/dist/diagnosticsRoutes.d.ts +44 -0
  56. package/dist/evalRoutes.d.ts +213 -0
  57. package/dist/fileStore.d.ts +11 -2
  58. package/dist/handoff.d.ts +54 -0
  59. package/dist/handoffHealth.d.ts +94 -0
  60. package/dist/index.d.ts +103 -9
  61. package/dist/index.js +17158 -4684
  62. package/dist/liveLatency.d.ts +78 -0
  63. package/dist/modelAdapters.d.ts +26 -2
  64. package/dist/openaiRealtime.d.ts +27 -0
  65. package/dist/openaiTTS.d.ts +18 -0
  66. package/dist/opsConsoleRoutes.d.ts +77 -0
  67. package/dist/opsStatus.d.ts +65 -0
  68. package/dist/opsStatusRoutes.d.ts +33 -0
  69. package/dist/opsWebhook.d.ts +126 -0
  70. package/dist/outcomeContract.d.ts +112 -0
  71. package/dist/phoneAgent.d.ts +62 -0
  72. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  73. package/dist/postgresStore.d.ts +13 -2
  74. package/dist/productionReadiness.d.ts +227 -0
  75. package/dist/providerAdapters.d.ts +48 -0
  76. package/dist/providerCapabilities.d.ts +92 -0
  77. package/dist/providerHealth.d.ts +79 -0
  78. package/dist/providerRoutingContract.d.ts +38 -0
  79. package/dist/qualityRoutes.d.ts +76 -0
  80. package/dist/queue.d.ts +61 -0
  81. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  82. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  83. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  84. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  85. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  86. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  87. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  88. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  89. package/dist/react/index.d.ts +18 -0
  90. package/dist/react/index.js +2726 -14
  91. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  92. package/dist/react/useVoiceController.d.ts +3 -0
  93. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  94. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  95. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  96. package/dist/react/useVoiceProviderStatus.d.ts +8 -0
  97. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  98. package/dist/react/useVoiceStream.d.ts +3 -0
  99. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  100. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  101. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  102. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  103. package/dist/resilienceRoutes.d.ts +142 -0
  104. package/dist/sessionReplay.d.ts +175 -0
  105. package/dist/simulationSuite.d.ts +120 -0
  106. package/dist/sqliteStore.d.ts +13 -2
  107. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  108. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  109. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  110. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  111. package/dist/svelte/createVoiceProviderStatus.d.ts +10 -0
  112. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  113. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  114. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  115. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  116. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  117. package/dist/svelte/index.d.ts +10 -0
  118. package/dist/svelte/index.js +2152 -202
  119. package/dist/telephony/contract.d.ts +61 -0
  120. package/dist/telephony/matrix.d.ts +97 -0
  121. package/dist/telephony/plivo.d.ts +254 -0
  122. package/dist/telephony/telnyx.d.ts +247 -0
  123. package/dist/telephony/twilio.d.ts +135 -2
  124. package/dist/telephonyOutcome.d.ts +201 -0
  125. package/dist/testing/index.d.ts +2 -0
  126. package/dist/testing/index.js +2830 -37
  127. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  128. package/dist/testing/providerSimulator.d.ts +44 -0
  129. package/dist/toolContract.d.ts +130 -0
  130. package/dist/toolRuntime.d.ts +50 -0
  131. package/dist/trace.d.ts +1 -1
  132. package/dist/traceDeliveryRoutes.d.ts +86 -0
  133. package/dist/traceTimeline.d.ts +93 -0
  134. package/dist/turnLatency.d.ts +95 -0
  135. package/dist/turnQuality.d.ts +94 -0
  136. package/dist/types.d.ts +169 -4
  137. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  138. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  139. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  140. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  141. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  142. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  143. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  144. package/dist/vue/index.d.ts +17 -0
  145. package/dist/vue/index.js +2636 -31
  146. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  147. package/dist/vue/useVoiceController.d.ts +2 -1
  148. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  149. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  150. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  151. package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
  152. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  153. package/dist/vue/useVoiceStream.d.ts +4 -1
  154. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  155. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  156. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  157. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  158. package/dist/workflowContract.d.ts +91 -0
  159. package/package.json +1 -1
@@ -188,6 +188,17 @@ var serverMessageToAction = (message) => {
188
188
  sessionId: message.sessionId,
189
189
  type: "complete"
190
190
  };
191
+ case "connection":
192
+ return {
193
+ reconnect: message.reconnect,
194
+ type: "connection"
195
+ };
196
+ case "call_lifecycle":
197
+ return {
198
+ event: message.event,
199
+ sessionId: message.sessionId,
200
+ type: "call_lifecycle"
201
+ };
191
202
  case "error":
192
203
  return {
193
204
  message: normalizeErrorMessage(message.message),
@@ -203,6 +214,17 @@ var serverMessageToAction = (message) => {
203
214
  transcript: message.transcript,
204
215
  type: "partial"
205
216
  };
217
+ case "replay":
218
+ return {
219
+ assistantTexts: message.assistantTexts,
220
+ call: message.call,
221
+ partial: message.partial,
222
+ scenarioId: message.scenarioId,
223
+ sessionId: message.sessionId,
224
+ status: message.status,
225
+ turns: message.turns,
226
+ type: "replay"
227
+ };
206
228
  case "session":
207
229
  return {
208
230
  sessionId: message.sessionId,
@@ -231,7 +253,7 @@ var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
231
253
  var noop = () => {};
232
254
  var noopUnsubscribe = () => noop;
233
255
  var NOOP_CONNECTION = {
234
- start: () => {},
256
+ callControl: noop,
235
257
  close: noop,
236
258
  endTurn: noop,
237
259
  getReadyState: () => WS_CLOSED,
@@ -239,6 +261,7 @@ var NOOP_CONNECTION = {
239
261
  getSessionId: () => "",
240
262
  send: noop,
241
263
  sendAudio: noop,
264
+ start: () => {},
242
265
  subscribe: noopUnsubscribe
243
266
  };
244
267
  var createSessionId = () => crypto.randomUUID();
@@ -260,11 +283,14 @@ var isVoiceServerMessage = (value) => {
260
283
  switch (value.type) {
261
284
  case "audio":
262
285
  case "assistant":
286
+ case "call_lifecycle":
263
287
  case "complete":
288
+ case "connection":
264
289
  case "error":
265
290
  case "final":
266
291
  case "partial":
267
292
  case "pong":
293
+ case "replay":
268
294
  case "session":
269
295
  case "turn":
270
296
  return true;
@@ -301,6 +327,9 @@ var createVoiceConnection = (path, options = {}) => {
301
327
  sessionId: options.sessionId ?? createSessionId(),
302
328
  ws: null
303
329
  };
330
+ const emitConnection = (reconnect) => {
331
+ listeners.forEach((listener) => listener(reconnect));
332
+ };
304
333
  const clearTimers = () => {
305
334
  if (state.pingInterval) {
306
335
  clearInterval(state.pingInterval);
@@ -323,9 +352,28 @@ var createVoiceConnection = (path, options = {}) => {
323
352
  }
324
353
  };
325
354
  const scheduleReconnect = () => {
355
+ const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
326
356
  state.reconnectAttempts += 1;
357
+ emitConnection({
358
+ reconnect: {
359
+ attempts: state.reconnectAttempts,
360
+ lastDisconnectAt: Date.now(),
361
+ maxAttempts: maxReconnectAttempts,
362
+ nextAttemptAt,
363
+ status: "reconnecting"
364
+ },
365
+ type: "connection"
366
+ });
327
367
  state.reconnectTimeout = setTimeout(() => {
328
368
  if (state.reconnectAttempts > maxReconnectAttempts) {
369
+ emitConnection({
370
+ reconnect: {
371
+ attempts: state.reconnectAttempts,
372
+ maxAttempts: maxReconnectAttempts,
373
+ status: "exhausted"
374
+ },
375
+ type: "connection"
376
+ });
329
377
  return;
330
378
  }
331
379
  connect();
@@ -335,9 +383,21 @@ var createVoiceConnection = (path, options = {}) => {
335
383
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
336
384
  ws.binaryType = "arraybuffer";
337
385
  ws.onopen = () => {
386
+ const wasReconnecting = state.reconnectAttempts > 0;
338
387
  state.isConnected = true;
339
- state.reconnectAttempts = 0;
340
388
  flushPendingMessages();
389
+ if (wasReconnecting) {
390
+ emitConnection({
391
+ reconnect: {
392
+ attempts: state.reconnectAttempts,
393
+ lastResumedAt: Date.now(),
394
+ maxAttempts: maxReconnectAttempts,
395
+ status: "resumed"
396
+ },
397
+ type: "connection"
398
+ });
399
+ state.reconnectAttempts = 0;
400
+ }
341
401
  listeners.forEach((listener) => listener({
342
402
  scenarioId: state.scenarioId ?? undefined,
343
403
  sessionId: state.sessionId,
@@ -367,6 +427,16 @@ var createVoiceConnection = (path, options = {}) => {
367
427
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
368
428
  if (reconnectable) {
369
429
  scheduleReconnect();
430
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
431
+ emitConnection({
432
+ reconnect: {
433
+ attempts: state.reconnectAttempts,
434
+ lastDisconnectAt: Date.now(),
435
+ maxAttempts: maxReconnectAttempts,
436
+ status: "exhausted"
437
+ },
438
+ type: "connection"
439
+ });
370
440
  }
371
441
  };
372
442
  state.ws = ws;
@@ -400,6 +470,12 @@ var createVoiceConnection = (path, options = {}) => {
400
470
  const endTurn = () => {
401
471
  send({ type: "end_turn" });
402
472
  };
473
+ const callControl = (message) => {
474
+ send({
475
+ ...message,
476
+ type: "call_control"
477
+ });
478
+ };
403
479
  const close = () => {
404
480
  clearTimers();
405
481
  if (state.ws) {
@@ -417,7 +493,7 @@ var createVoiceConnection = (path, options = {}) => {
417
493
  };
418
494
  connect();
419
495
  return {
420
- start,
496
+ callControl,
421
497
  close,
422
498
  endTurn,
423
499
  getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
@@ -425,18 +501,26 @@ var createVoiceConnection = (path, options = {}) => {
425
501
  getSessionId: () => state.sessionId,
426
502
  send,
427
503
  sendAudio,
504
+ start,
428
505
  subscribe
429
506
  };
430
507
  };
431
508
 
432
509
  // src/client/store.ts
510
+ var createInitialReconnectState = () => ({
511
+ attempts: 0,
512
+ maxAttempts: 0,
513
+ status: "idle"
514
+ });
433
515
  var createInitialState = () => ({
434
516
  assistantAudio: [],
435
517
  assistantTexts: [],
518
+ call: null,
436
519
  error: null,
437
520
  isConnected: false,
438
521
  scenarioId: null,
439
522
  partial: "",
523
+ reconnect: createInitialReconnectState(),
440
524
  sessionId: null,
441
525
  status: "idle",
442
526
  turns: []
@@ -476,10 +560,36 @@ var createVoiceStreamStore = () => {
476
560
  status: "completed"
477
561
  };
478
562
  break;
563
+ case "call_lifecycle":
564
+ state = {
565
+ ...state,
566
+ call: {
567
+ ...state.call,
568
+ disposition: action.event.type === "end" ? action.event.disposition : state.call?.disposition,
569
+ endedAt: action.event.type === "end" ? action.event.at : state.call?.endedAt,
570
+ events: [...state.call?.events ?? [], action.event],
571
+ lastEventAt: action.event.at,
572
+ startedAt: state.call?.startedAt ?? action.event.at
573
+ },
574
+ sessionId: action.sessionId
575
+ };
576
+ break;
479
577
  case "connected":
480
578
  state = {
481
579
  ...state,
482
- isConnected: true
580
+ isConnected: true,
581
+ reconnect: state.reconnect.status === "reconnecting" ? {
582
+ ...state.reconnect,
583
+ lastResumedAt: Date.now(),
584
+ nextAttemptAt: undefined,
585
+ status: "resumed"
586
+ } : state.reconnect
587
+ };
588
+ break;
589
+ case "connection":
590
+ state = {
591
+ ...state,
592
+ reconnect: action.reconnect
483
593
  };
484
594
  break;
485
595
  case "disconnected":
@@ -507,6 +617,26 @@ var createVoiceStreamStore = () => {
507
617
  partial: action.transcript.text
508
618
  };
509
619
  break;
620
+ case "replay":
621
+ state = {
622
+ ...state,
623
+ assistantTexts: [...action.assistantTexts],
624
+ call: action.call ?? null,
625
+ error: null,
626
+ isConnected: action.status === "active",
627
+ partial: action.partial,
628
+ reconnect: state.reconnect.status === "reconnecting" ? {
629
+ ...state.reconnect,
630
+ lastResumedAt: Date.now(),
631
+ nextAttemptAt: undefined,
632
+ status: "resumed"
633
+ } : state.reconnect,
634
+ scenarioId: action.scenarioId ?? state.scenarioId,
635
+ sessionId: action.sessionId,
636
+ status: action.status,
637
+ turns: [...action.turns]
638
+ };
639
+ break;
510
640
  case "session":
511
641
  state = {
512
642
  ...state,
@@ -562,6 +692,9 @@ var createVoiceStream = (path, options = {}) => {
562
692
  }
563
693
  });
564
694
  return {
695
+ callControl(message) {
696
+ connection.callControl(message);
697
+ },
565
698
  close() {
566
699
  unsubscribeConnection();
567
700
  connection.close();
@@ -590,6 +723,9 @@ var createVoiceStream = (path, options = {}) => {
590
723
  get partial() {
591
724
  return store.getSnapshot().partial;
592
725
  },
726
+ get reconnect() {
727
+ return store.getSnapshot().reconnect;
728
+ },
593
729
  get sessionId() {
594
730
  return connection.getSessionId();
595
731
  },
@@ -605,6 +741,9 @@ var createVoiceStream = (path, options = {}) => {
605
741
  get assistantAudio() {
606
742
  return store.getSnapshot().assistantAudio;
607
743
  },
744
+ get call() {
745
+ return store.getSnapshot().call;
746
+ },
608
747
  sendAudio(audio) {
609
748
  connection.sendAudio(audio);
610
749
  },
@@ -900,10 +1039,12 @@ var resolveVoiceRuntimePreset = (name = "default") => {
900
1039
  var createInitialState2 = (stream) => ({
901
1040
  assistantAudio: [...stream.assistantAudio],
902
1041
  assistantTexts: [...stream.assistantTexts],
1042
+ call: stream.call,
903
1043
  error: stream.error,
904
1044
  isConnected: stream.isConnected,
905
1045
  isRecording: false,
906
1046
  partial: stream.partial,
1047
+ reconnect: stream.reconnect,
907
1048
  recordingError: null,
908
1049
  sessionId: stream.sessionId,
909
1050
  scenarioId: stream.scenarioId,
@@ -929,9 +1070,11 @@ var createVoiceController = (path, options = {}) => {
929
1070
  ...state,
930
1071
  assistantAudio: [...stream.assistantAudio],
931
1072
  assistantTexts: [...stream.assistantTexts],
1073
+ call: stream.call,
932
1074
  error: stream.error,
933
1075
  isConnected: stream.isConnected,
934
1076
  partial: stream.partial,
1077
+ reconnect: stream.reconnect,
935
1078
  sessionId: stream.sessionId,
936
1079
  scenarioId: stream.scenarioId,
937
1080
  status: stream.status,
@@ -956,7 +1099,13 @@ var createVoiceController = (path, options = {}) => {
956
1099
  capture = createMicrophoneCapture({
957
1100
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
958
1101
  onLevel: options.capture?.onLevel,
959
- onAudio: (audio) => stream.sendAudio(audio),
1102
+ onAudio: (audio) => {
1103
+ if (options.capture?.onAudio) {
1104
+ options.capture.onAudio(audio, stream.sendAudio);
1105
+ return;
1106
+ }
1107
+ stream.sendAudio(audio);
1108
+ },
960
1109
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
961
1110
  });
962
1111
  return capture;
@@ -1006,6 +1155,7 @@ var createVoiceController = (path, options = {}) => {
1006
1155
  bindHTMX(bindingOptions) {
1007
1156
  return bindVoiceHTMX(stream, bindingOptions);
1008
1157
  },
1158
+ callControl: (message) => stream.callControl(message),
1009
1159
  close,
1010
1160
  endTurn: () => stream.endTurn(),
1011
1161
  get error() {
@@ -1025,6 +1175,9 @@ var createVoiceController = (path, options = {}) => {
1025
1175
  get recordingError() {
1026
1176
  return state.recordingError;
1027
1177
  },
1178
+ get reconnect() {
1179
+ return state.reconnect;
1180
+ },
1028
1181
  sendAudio: (audio) => stream.sendAudio(audio),
1029
1182
  get sessionId() {
1030
1183
  return state.sessionId;
@@ -1058,6 +1211,478 @@ var createVoiceController = (path, options = {}) => {
1058
1211
  },
1059
1212
  get assistantAudio() {
1060
1213
  return state.assistantAudio;
1214
+ },
1215
+ get call() {
1216
+ return state.call;
1217
+ }
1218
+ };
1219
+ };
1220
+
1221
+ // src/client/audioPlayer.ts
1222
+ var DEFAULT_LOOKAHEAD_MS = 15;
1223
+ var createInitialState3 = () => ({
1224
+ activeSourceCount: 0,
1225
+ error: null,
1226
+ isActive: false,
1227
+ isPlaying: false,
1228
+ lastInterruptLatencyMs: undefined,
1229
+ lastPlaybackStopLatencyMs: undefined,
1230
+ processedChunkCount: 0,
1231
+ queuedChunkCount: 0
1232
+ });
1233
+ var getAudioContextCtor = () => {
1234
+ if (typeof window === "undefined") {
1235
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1236
+ }
1237
+ return window.AudioContext ?? window.webkitAudioContext;
1238
+ };
1239
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1240
+ const format = chunk.format;
1241
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1242
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1243
+ }
1244
+ const bytes = chunk.chunk;
1245
+ const channels = Math.max(1, format.channels);
1246
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1247
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1248
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1249
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1250
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1251
+ const channelData = audioBuffer.getChannelData(channelIndex);
1252
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1253
+ const sampleIndex = frameIndex * channels + channelIndex;
1254
+ const sampleOffset = sampleIndex * 2;
1255
+ if (sampleOffset + 1 >= bytes.byteLength) {
1256
+ channelData[frameIndex] = 0;
1257
+ continue;
1258
+ }
1259
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1260
+ }
1261
+ }
1262
+ return audioBuffer;
1263
+ };
1264
+ var createVoiceAudioPlayer = (source, options = {}) => {
1265
+ const subscribers = new Set;
1266
+ const sourceNodes = new Set;
1267
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1268
+ let state = createInitialState3();
1269
+ let audioContext = null;
1270
+ let outputNode = null;
1271
+ let queueEndTime = 0;
1272
+ let syncPromise = Promise.resolve();
1273
+ let interruptStartedAt = null;
1274
+ let interruptPromise = null;
1275
+ let resolveInterruptPromise = null;
1276
+ let interruptFallbackTimer = null;
1277
+ const notify = () => {
1278
+ for (const subscriber of subscribers) {
1279
+ subscriber();
1280
+ }
1281
+ };
1282
+ const setState = (next) => {
1283
+ state = {
1284
+ ...state,
1285
+ ...next
1286
+ };
1287
+ notify();
1288
+ };
1289
+ const clearError = () => {
1290
+ if (state.error !== null) {
1291
+ setState({ error: null });
1292
+ }
1293
+ };
1294
+ const clearInterruptTimer = () => {
1295
+ if (interruptFallbackTimer !== null) {
1296
+ clearTimeout(interruptFallbackTimer);
1297
+ interruptFallbackTimer = null;
1298
+ }
1299
+ };
1300
+ const resolveInterrupt = (latencyMs) => {
1301
+ clearInterruptTimer();
1302
+ interruptStartedAt = null;
1303
+ setState({
1304
+ activeSourceCount: sourceNodes.size,
1305
+ isPlaying: false,
1306
+ lastInterruptLatencyMs: latencyMs,
1307
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1308
+ });
1309
+ resolveInterruptPromise?.();
1310
+ resolveInterruptPromise = null;
1311
+ interruptPromise = null;
1312
+ };
1313
+ const estimateOutputStopLatencyMs = (context) => {
1314
+ if (!context) {
1315
+ return 0;
1316
+ }
1317
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1318
+ };
1319
+ const restoreOutputGain = (context) => {
1320
+ if (!outputNode) {
1321
+ return;
1322
+ }
1323
+ const gainValue = 1;
1324
+ if (outputNode.gain.setValueAtTime) {
1325
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1326
+ return;
1327
+ }
1328
+ outputNode.gain.value = gainValue;
1329
+ };
1330
+ const muteOutputGain = (context) => {
1331
+ if (!outputNode) {
1332
+ return;
1333
+ }
1334
+ const gainValue = 0;
1335
+ if (outputNode.gain.setValueAtTime) {
1336
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1337
+ return;
1338
+ }
1339
+ outputNode.gain.value = gainValue;
1340
+ };
1341
+ const maybeResolveInterrupt = () => {
1342
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1343
+ return;
1344
+ }
1345
+ resolveInterrupt(Date.now() - interruptStartedAt);
1346
+ };
1347
+ const ensureAudioContext = async () => {
1348
+ if (audioContext) {
1349
+ return audioContext;
1350
+ }
1351
+ if (options.createAudioContext) {
1352
+ audioContext = options.createAudioContext();
1353
+ } else {
1354
+ const AudioContextCtor = getAudioContextCtor();
1355
+ if (!AudioContextCtor) {
1356
+ throw new Error("Assistant audio playback requires AudioContext support.");
1357
+ }
1358
+ audioContext = new AudioContextCtor;
1359
+ }
1360
+ if (audioContext.createGain) {
1361
+ outputNode = audioContext.createGain();
1362
+ outputNode.connect?.(audioContext.destination);
1363
+ }
1364
+ queueEndTime = audioContext.currentTime;
1365
+ return audioContext;
1366
+ };
1367
+ const scheduleChunk = async (chunk) => {
1368
+ const context = await ensureAudioContext();
1369
+ const buffer = decodePCM16LEChunk(context, chunk);
1370
+ const node = context.createBufferSource();
1371
+ node.buffer = buffer;
1372
+ node.connect(outputNode ?? context.destination);
1373
+ node.onended = () => {
1374
+ sourceNodes.delete(node);
1375
+ node.disconnect?.();
1376
+ setState({
1377
+ activeSourceCount: sourceNodes.size,
1378
+ isPlaying: sourceNodes.size > 0 && state.isActive
1379
+ });
1380
+ maybeResolveInterrupt();
1381
+ };
1382
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1383
+ queueEndTime = startAt + buffer.duration;
1384
+ sourceNodes.add(node);
1385
+ setState({
1386
+ activeSourceCount: sourceNodes.size,
1387
+ isPlaying: true
1388
+ });
1389
+ node.start(startAt);
1390
+ };
1391
+ const stopQueuedPlayback = (options2) => {
1392
+ for (const node of [...sourceNodes]) {
1393
+ node.stop?.();
1394
+ }
1395
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1396
+ if (options2?.forceClear) {
1397
+ for (const node of sourceNodes) {
1398
+ node.disconnect?.();
1399
+ }
1400
+ sourceNodes.clear();
1401
+ maybeResolveInterrupt();
1402
+ }
1403
+ };
1404
+ const sync = async () => {
1405
+ if (!state.isActive) {
1406
+ return;
1407
+ }
1408
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1409
+ if (nextChunks.length === 0) {
1410
+ return;
1411
+ }
1412
+ try {
1413
+ clearError();
1414
+ for (const chunk of nextChunks) {
1415
+ await scheduleChunk(chunk);
1416
+ }
1417
+ setState({
1418
+ processedChunkCount: source.assistantAudio.length,
1419
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1420
+ });
1421
+ } catch (error) {
1422
+ setState({
1423
+ error: error instanceof Error ? error.message : String(error)
1424
+ });
1425
+ }
1426
+ };
1427
+ const queueSync = () => {
1428
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1429
+ return syncPromise;
1430
+ };
1431
+ const unsubscribeSource = source.subscribe(() => {
1432
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1433
+ player.start();
1434
+ return;
1435
+ }
1436
+ if (state.isActive) {
1437
+ queueSync();
1438
+ }
1439
+ });
1440
+ const player = {
1441
+ close: async () => {
1442
+ unsubscribeSource();
1443
+ stopQueuedPlayback({ forceClear: true });
1444
+ clearInterruptTimer();
1445
+ resolveInterruptPromise?.();
1446
+ resolveInterruptPromise = null;
1447
+ interruptPromise = null;
1448
+ interruptStartedAt = null;
1449
+ if (audioContext && audioContext.state !== "closed") {
1450
+ await audioContext.close();
1451
+ }
1452
+ audioContext = null;
1453
+ outputNode?.disconnect?.();
1454
+ outputNode = null;
1455
+ queueEndTime = 0;
1456
+ setState({
1457
+ activeSourceCount: 0,
1458
+ isActive: false,
1459
+ isPlaying: false
1460
+ });
1461
+ },
1462
+ get activeSourceCount() {
1463
+ return state.activeSourceCount;
1464
+ },
1465
+ get error() {
1466
+ return state.error;
1467
+ },
1468
+ getSnapshot: () => state,
1469
+ get isActive() {
1470
+ return state.isActive;
1471
+ },
1472
+ get isPlaying() {
1473
+ return state.isPlaying;
1474
+ },
1475
+ interrupt: async () => {
1476
+ const startedAt = Date.now();
1477
+ const context = await ensureAudioContext();
1478
+ interruptStartedAt = startedAt;
1479
+ muteOutputGain(context);
1480
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
1481
+ setState({
1482
+ isActive: false,
1483
+ isPlaying: sourceNodes.size > 0,
1484
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
1485
+ });
1486
+ if (sourceNodes.size === 0) {
1487
+ resolveInterrupt(playbackStopLatencyMs);
1488
+ return;
1489
+ }
1490
+ if (!interruptPromise) {
1491
+ interruptPromise = new Promise((resolve) => {
1492
+ resolveInterruptPromise = resolve;
1493
+ });
1494
+ }
1495
+ clearInterruptTimer();
1496
+ interruptFallbackTimer = setTimeout(() => {
1497
+ for (const node of sourceNodes) {
1498
+ node.disconnect?.();
1499
+ }
1500
+ sourceNodes.clear();
1501
+ resolveInterrupt(Date.now() - startedAt);
1502
+ }, 250);
1503
+ stopQueuedPlayback();
1504
+ await interruptPromise;
1505
+ },
1506
+ get lastInterruptLatencyMs() {
1507
+ return state.lastInterruptLatencyMs;
1508
+ },
1509
+ get lastPlaybackStopLatencyMs() {
1510
+ return state.lastPlaybackStopLatencyMs;
1511
+ },
1512
+ pause: async () => {
1513
+ if (!audioContext) {
1514
+ setState({
1515
+ activeSourceCount: 0,
1516
+ isActive: false,
1517
+ isPlaying: false
1518
+ });
1519
+ return;
1520
+ }
1521
+ await audioContext.suspend();
1522
+ setState({
1523
+ activeSourceCount: sourceNodes.size,
1524
+ isActive: false,
1525
+ isPlaying: false
1526
+ });
1527
+ },
1528
+ get processedChunkCount() {
1529
+ return state.processedChunkCount;
1530
+ },
1531
+ get queuedChunkCount() {
1532
+ return state.queuedChunkCount;
1533
+ },
1534
+ start: async () => {
1535
+ try {
1536
+ clearError();
1537
+ const context = await ensureAudioContext();
1538
+ restoreOutputGain(context);
1539
+ if (context.state === "suspended") {
1540
+ await context.resume();
1541
+ }
1542
+ setState({
1543
+ activeSourceCount: sourceNodes.size,
1544
+ isActive: true,
1545
+ isPlaying: context.state === "running"
1546
+ });
1547
+ await queueSync();
1548
+ } catch (error) {
1549
+ setState({
1550
+ error: error instanceof Error ? error.message : String(error),
1551
+ isActive: false,
1552
+ isPlaying: false
1553
+ });
1554
+ throw error;
1555
+ }
1556
+ },
1557
+ subscribe: (subscriber) => {
1558
+ subscribers.add(subscriber);
1559
+ return () => {
1560
+ subscribers.delete(subscriber);
1561
+ };
1562
+ }
1563
+ };
1564
+ return player;
1565
+ };
1566
+
1567
+ // src/client/bargeInMonitor.ts
1568
+ var DEFAULT_THRESHOLD_MS = 250;
1569
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1570
+ var summarize = (events, thresholdMs) => {
1571
+ const stopped = events.filter((event) => event.status === "stopped");
1572
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1573
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1574
+ const passed = stopped.length - failed;
1575
+ return {
1576
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1577
+ events: [...events],
1578
+ failed,
1579
+ lastEvent: events.at(-1),
1580
+ passed,
1581
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1582
+ thresholdMs,
1583
+ total: stopped.length
1584
+ };
1585
+ };
1586
+ var createVoiceBargeInMonitor = (options = {}) => {
1587
+ const listeners = new Set;
1588
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1589
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1590
+ const events = [];
1591
+ const emit = () => {
1592
+ for (const listener of listeners) {
1593
+ listener();
1594
+ }
1595
+ };
1596
+ const postEvent = (event) => {
1597
+ if (!options.path || typeof fetchImpl !== "function") {
1598
+ return;
1599
+ }
1600
+ fetchImpl(options.path, {
1601
+ body: JSON.stringify(event),
1602
+ headers: {
1603
+ "Content-Type": "application/json"
1604
+ },
1605
+ method: "POST"
1606
+ }).catch(() => {});
1607
+ };
1608
+ const record = (status, input) => {
1609
+ const event = {
1610
+ at: Date.now(),
1611
+ id: createEventId(),
1612
+ latencyMs: input.latencyMs,
1613
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1614
+ reason: input.reason,
1615
+ sessionId: input.sessionId,
1616
+ status,
1617
+ thresholdMs
1618
+ };
1619
+ events.push(event);
1620
+ postEvent(event);
1621
+ emit();
1622
+ return event;
1623
+ };
1624
+ return {
1625
+ getSnapshot: () => summarize(events, thresholdMs),
1626
+ recordRequested: (input) => record("requested", input),
1627
+ recordSkipped: (input) => record("skipped", input),
1628
+ recordStopped: (input) => record("stopped", input),
1629
+ subscribe: (subscriber) => {
1630
+ listeners.add(subscriber);
1631
+ return () => {
1632
+ listeners.delete(subscriber);
1633
+ };
1634
+ }
1635
+ };
1636
+ };
1637
+
1638
+ // src/client/duplex.ts
1639
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1640
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1641
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
1642
+ let lastPartial = controller.partial;
1643
+ const interruptIfPlaying = (reason) => {
1644
+ if (!player.isPlaying || options.enabled === false) {
1645
+ options.monitor?.recordSkipped({
1646
+ reason,
1647
+ sessionId: controller.sessionId
1648
+ });
1649
+ return;
1650
+ }
1651
+ options.monitor?.recordRequested({
1652
+ reason,
1653
+ sessionId: controller.sessionId
1654
+ });
1655
+ player.interrupt().then(() => {
1656
+ options.monitor?.recordStopped({
1657
+ latencyMs: player.lastInterruptLatencyMs,
1658
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1659
+ reason,
1660
+ sessionId: controller.sessionId
1661
+ });
1662
+ });
1663
+ };
1664
+ const unsubscribe = controller.subscribe(() => {
1665
+ if (options.interruptOnPartial === false) {
1666
+ lastPartial = controller.partial;
1667
+ return;
1668
+ }
1669
+ if (!lastPartial && controller.partial) {
1670
+ interruptIfPlaying("partial-transcript");
1671
+ }
1672
+ lastPartial = controller.partial;
1673
+ });
1674
+ return {
1675
+ close: () => {
1676
+ unsubscribe();
1677
+ },
1678
+ handleLevel: (level) => {
1679
+ if (shouldInterruptForLevel(level, options)) {
1680
+ interruptIfPlaying("input-level");
1681
+ }
1682
+ },
1683
+ sendAudio: (audio) => {
1684
+ interruptIfPlaying("manual-audio");
1685
+ controller.sendAudio(audio);
1061
1686
  }
1062
1687
  };
1063
1688
  };
@@ -1174,6 +1799,13 @@ var parsePromptList = (value) => {
1174
1799
  } catch {}
1175
1800
  return DEFAULT_GUIDED_PROMPTS;
1176
1801
  };
1802
+ var parseOptionalNumber = (value) => {
1803
+ if (!value) {
1804
+ return;
1805
+ }
1806
+ const parsed = Number(value);
1807
+ return Number.isFinite(parsed) ? parsed : undefined;
1808
+ };
1177
1809
  var requireElement = (root, selector, ctor, name) => {
1178
1810
  const value = selector ? document.querySelector(selector) : null;
1179
1811
  if (value instanceof ctor) {
@@ -1224,6 +1856,13 @@ var initVoiceHTMXRoot = (root) => {
1224
1856
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1225
1857
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1226
1858
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
1859
+ const bargeInPath = root.dataset.voiceBargeInPath;
1860
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
1861
+ path: bargeInPath,
1862
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
1863
+ }) : null;
1864
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
1865
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1227
1866
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1228
1867
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1229
1868
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
@@ -1237,9 +1876,27 @@ var initVoiceHTMXRoot = (root) => {
1237
1876
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1238
1877
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1239
1878
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
1879
+ let activeMode = null;
1880
+ let hasStartedModes = {
1881
+ general: false,
1882
+ guided: false
1883
+ };
1884
+ let isCapturing = false;
1885
+ let micError = null;
1886
+ let waveLevels = createInitialVoiceWaveLevels();
1887
+ let guidedBargeInBinding = null;
1888
+ let generalBargeInBinding = null;
1240
1889
  const guidedVoice = createVoiceController(guidedPath, {
1241
1890
  capture: {
1891
+ onAudio: (audio, sendAudio) => {
1892
+ if (guidedBargeInBinding) {
1893
+ guidedBargeInBinding.sendAudio(audio);
1894
+ return;
1895
+ }
1896
+ sendAudio(audio);
1897
+ },
1242
1898
  onLevel: (level) => {
1899
+ guidedBargeInBinding?.handleLevel(level);
1243
1900
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1244
1901
  renderWave();
1245
1902
  }
@@ -1248,7 +1905,15 @@ var initVoiceHTMXRoot = (root) => {
1248
1905
  });
1249
1906
  const generalVoice = createVoiceController(generalPath, {
1250
1907
  capture: {
1908
+ onAudio: (audio, sendAudio) => {
1909
+ if (generalBargeInBinding) {
1910
+ generalBargeInBinding.sendAudio(audio);
1911
+ return;
1912
+ }
1913
+ sendAudio(audio);
1914
+ },
1251
1915
  onLevel: (level) => {
1916
+ generalBargeInBinding?.handleLevel(level);
1252
1917
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1253
1918
  renderWave();
1254
1919
  }
@@ -1257,15 +1922,18 @@ var initVoiceHTMXRoot = (root) => {
1257
1922
  });
1258
1923
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1259
1924
  const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
1260
- let activeMode = null;
1261
- let hasStartedModes = {
1262
- general: false,
1263
- guided: false
1264
- };
1265
- let isCapturing = false;
1266
- let micError = null;
1267
- let waveLevels = createInitialVoiceWaveLevels();
1925
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
1926
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
1927
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
1928
+ interruptThreshold: bargeInSpeechThreshold,
1929
+ monitor: bargeInMonitor ?? undefined
1930
+ });
1931
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
1932
+ interruptThreshold: bargeInSpeechThreshold,
1933
+ monitor: bargeInMonitor ?? undefined
1934
+ });
1268
1935
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
1936
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1269
1937
  const renderWave = () => {
1270
1938
  const path = createVoiceWavePath(waveLevels);
1271
1939
  voiceWaveGlow.setAttribute("d", path);
@@ -1343,8 +2011,18 @@ var initVoiceHTMXRoot = (root) => {
1343
2011
  render();
1344
2012
  }
1345
2013
  };
1346
- guidedVoice.subscribe(render);
1347
- generalVoice.subscribe(render);
2014
+ guidedVoice.subscribe(() => {
2015
+ if (guidedVoice.assistantAudio.length > 0) {
2016
+ guidedAudioPlayer.start().catch(() => {});
2017
+ }
2018
+ render();
2019
+ });
2020
+ generalVoice.subscribe(() => {
2021
+ if (generalVoice.assistantAudio.length > 0) {
2022
+ generalAudioPlayer.start().catch(() => {});
2023
+ }
2024
+ render();
2025
+ });
1348
2026
  startGuidedButton.addEventListener("click", () => {
1349
2027
  startMode("guided");
1350
2028
  });
@@ -1357,6 +2035,10 @@ var initVoiceHTMXRoot = (root) => {
1357
2035
  window.addEventListener("beforeunload", () => {
1358
2036
  guidedVoice.stopRecording();
1359
2037
  generalVoice.stopRecording();
2038
+ guidedBargeInBinding?.close();
2039
+ generalBargeInBinding?.close();
2040
+ guidedAudioPlayer.close();
2041
+ generalAudioPlayer.close();
1360
2042
  stopGuidedBinding();
1361
2043
  stopGeneralBinding();
1362
2044
  guidedVoice.close();