@absolutejs/voice 0.0.22-beta.12 → 0.0.22-beta.120

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 (146) hide show
  1. package/README.md +411 -3
  2. package/dist/agent.d.ts +2 -0
  3. package/dist/angular/index.d.ts +9 -0
  4. package/dist/angular/index.js +1278 -44
  5. package/dist/angular/voice-app-kit-status.service.d.ts +12 -0
  6. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  7. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  8. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  9. package/dist/angular/voice-provider-status.service.d.ts +12 -0
  10. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  11. package/dist/angular/voice-stream.service.d.ts +2 -0
  12. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  13. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  14. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  15. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  16. package/dist/appKit.d.ts +100 -0
  17. package/dist/assistantHealth.d.ts +81 -0
  18. package/dist/bargeInRoutes.d.ts +56 -0
  19. package/dist/campaign.d.ts +610 -0
  20. package/dist/campaignDialers.d.ts +90 -0
  21. package/dist/client/actions.d.ts +22 -0
  22. package/dist/client/appKitStatus.d.ts +19 -0
  23. package/dist/client/bargeInMonitor.d.ts +7 -0
  24. package/dist/client/campaignDialerProof.d.ts +23 -0
  25. package/dist/client/connection.d.ts +3 -0
  26. package/dist/client/duplex.d.ts +1 -1
  27. package/dist/client/htmxBootstrap.js +587 -13
  28. package/dist/client/index.d.ts +40 -0
  29. package/dist/client/index.js +2028 -8
  30. package/dist/client/liveTurnLatency.d.ts +41 -0
  31. package/dist/client/opsStatusWidget.d.ts +40 -0
  32. package/dist/client/providerCapabilities.d.ts +19 -0
  33. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  34. package/dist/client/providerSimulationControls.d.ts +33 -0
  35. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  36. package/dist/client/providerStatus.d.ts +19 -0
  37. package/dist/client/providerStatusWidget.d.ts +32 -0
  38. package/dist/client/routingStatus.d.ts +19 -0
  39. package/dist/client/routingStatusWidget.d.ts +28 -0
  40. package/dist/client/traceTimeline.d.ts +19 -0
  41. package/dist/client/traceTimelineWidget.d.ts +32 -0
  42. package/dist/client/turnLatency.d.ts +22 -0
  43. package/dist/client/turnLatencyWidget.d.ts +33 -0
  44. package/dist/client/turnQuality.d.ts +19 -0
  45. package/dist/client/turnQualityWidget.d.ts +32 -0
  46. package/dist/client/workflowStatus.d.ts +19 -0
  47. package/dist/diagnosticsRoutes.d.ts +44 -0
  48. package/dist/evalRoutes.d.ts +213 -0
  49. package/dist/fileStore.d.ts +3 -0
  50. package/dist/handoff.d.ts +54 -0
  51. package/dist/handoffHealth.d.ts +94 -0
  52. package/dist/index.d.ts +77 -8
  53. package/dist/index.js +12645 -3061
  54. package/dist/liveLatency.d.ts +78 -0
  55. package/dist/modelAdapters.d.ts +41 -2
  56. package/dist/openaiTTS.d.ts +18 -0
  57. package/dist/opsConsoleRoutes.d.ts +77 -0
  58. package/dist/opsWebhook.d.ts +126 -0
  59. package/dist/outcomeContract.d.ts +112 -0
  60. package/dist/phoneAgent.d.ts +58 -0
  61. package/dist/postgresStore.d.ts +5 -0
  62. package/dist/productionReadiness.d.ts +121 -0
  63. package/dist/providerAdapters.d.ts +48 -0
  64. package/dist/providerCapabilities.d.ts +92 -0
  65. package/dist/providerHealth.d.ts +79 -0
  66. package/dist/qualityRoutes.d.ts +76 -0
  67. package/dist/queue.d.ts +61 -0
  68. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  69. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  70. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  71. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  72. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  73. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  74. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  75. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  76. package/dist/react/index.d.ts +18 -0
  77. package/dist/react/index.js +2606 -12
  78. package/dist/react/useVoiceAppKitStatus.d.ts +8 -0
  79. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  80. package/dist/react/useVoiceController.d.ts +2 -0
  81. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  82. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  83. package/dist/react/useVoiceProviderStatus.d.ts +8 -0
  84. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  85. package/dist/react/useVoiceStream.d.ts +2 -0
  86. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  87. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  88. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  89. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  90. package/dist/resilienceRoutes.d.ts +142 -0
  91. package/dist/sessionReplay.d.ts +175 -0
  92. package/dist/simulationSuite.d.ts +120 -0
  93. package/dist/sqliteStore.d.ts +5 -0
  94. package/dist/svelte/createVoiceAppKitStatus.d.ts +8 -0
  95. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  96. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  97. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  98. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  99. package/dist/svelte/createVoiceProviderStatus.d.ts +10 -0
  100. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  101. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  102. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  103. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  104. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  105. package/dist/svelte/index.d.ts +11 -0
  106. package/dist/svelte/index.js +1849 -4
  107. package/dist/telephony/contract.d.ts +61 -0
  108. package/dist/telephony/matrix.d.ts +97 -0
  109. package/dist/telephony/plivo.d.ts +254 -0
  110. package/dist/telephony/telnyx.d.ts +247 -0
  111. package/dist/telephony/twilio.d.ts +132 -0
  112. package/dist/telephonyOutcome.d.ts +201 -0
  113. package/dist/testing/index.d.ts +2 -0
  114. package/dist/testing/index.js +2640 -21
  115. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  116. package/dist/testing/providerSimulator.d.ts +44 -0
  117. package/dist/toolContract.d.ts +130 -0
  118. package/dist/toolRuntime.d.ts +50 -0
  119. package/dist/trace.d.ts +1 -1
  120. package/dist/traceTimeline.d.ts +93 -0
  121. package/dist/turnLatency.d.ts +95 -0
  122. package/dist/turnQuality.d.ts +94 -0
  123. package/dist/types.d.ts +125 -2
  124. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  125. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  126. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  127. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  128. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  129. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  130. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  131. package/dist/vue/index.d.ts +17 -0
  132. package/dist/vue/index.js +2520 -29
  133. package/dist/vue/useVoiceAppKitStatus.d.ts +9 -0
  134. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  135. package/dist/vue/useVoiceController.d.ts +1 -1
  136. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  137. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  138. package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
  139. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  140. package/dist/vue/useVoiceStream.d.ts +3 -1
  141. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  142. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  143. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  144. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  145. package/dist/workflowContract.d.ts +91 -0
  146. package/package.json +1 -1
@@ -188,6 +188,12 @@ var serverMessageToAction = (message) => {
188
188
  sessionId: message.sessionId,
189
189
  type: "complete"
190
190
  };
191
+ case "call_lifecycle":
192
+ return {
193
+ event: message.event,
194
+ sessionId: message.sessionId,
195
+ type: "call_lifecycle"
196
+ };
191
197
  case "error":
192
198
  return {
193
199
  message: normalizeErrorMessage(message.message),
@@ -231,7 +237,7 @@ var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
231
237
  var noop = () => {};
232
238
  var noopUnsubscribe = () => noop;
233
239
  var NOOP_CONNECTION = {
234
- start: () => {},
240
+ callControl: noop,
235
241
  close: noop,
236
242
  endTurn: noop,
237
243
  getReadyState: () => WS_CLOSED,
@@ -239,6 +245,7 @@ var NOOP_CONNECTION = {
239
245
  getSessionId: () => "",
240
246
  send: noop,
241
247
  sendAudio: noop,
248
+ start: () => {},
242
249
  subscribe: noopUnsubscribe
243
250
  };
244
251
  var createSessionId = () => crypto.randomUUID();
@@ -260,6 +267,7 @@ var isVoiceServerMessage = (value) => {
260
267
  switch (value.type) {
261
268
  case "audio":
262
269
  case "assistant":
270
+ case "call_lifecycle":
263
271
  case "complete":
264
272
  case "error":
265
273
  case "final":
@@ -400,6 +408,12 @@ var createVoiceConnection = (path, options = {}) => {
400
408
  const endTurn = () => {
401
409
  send({ type: "end_turn" });
402
410
  };
411
+ const callControl = (message) => {
412
+ send({
413
+ ...message,
414
+ type: "call_control"
415
+ });
416
+ };
403
417
  const close = () => {
404
418
  clearTimers();
405
419
  if (state.ws) {
@@ -417,7 +431,7 @@ var createVoiceConnection = (path, options = {}) => {
417
431
  };
418
432
  connect();
419
433
  return {
420
- start,
434
+ callControl,
421
435
  close,
422
436
  endTurn,
423
437
  getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
@@ -425,6 +439,7 @@ var createVoiceConnection = (path, options = {}) => {
425
439
  getSessionId: () => state.sessionId,
426
440
  send,
427
441
  sendAudio,
442
+ start,
428
443
  subscribe
429
444
  };
430
445
  };
@@ -433,6 +448,7 @@ var createVoiceConnection = (path, options = {}) => {
433
448
  var createInitialState = () => ({
434
449
  assistantAudio: [],
435
450
  assistantTexts: [],
451
+ call: null,
436
452
  error: null,
437
453
  isConnected: false,
438
454
  scenarioId: null,
@@ -476,6 +492,20 @@ var createVoiceStreamStore = () => {
476
492
  status: "completed"
477
493
  };
478
494
  break;
495
+ case "call_lifecycle":
496
+ state = {
497
+ ...state,
498
+ call: {
499
+ ...state.call,
500
+ disposition: action.event.type === "end" ? action.event.disposition : state.call?.disposition,
501
+ endedAt: action.event.type === "end" ? action.event.at : state.call?.endedAt,
502
+ events: [...state.call?.events ?? [], action.event],
503
+ lastEventAt: action.event.at,
504
+ startedAt: state.call?.startedAt ?? action.event.at
505
+ },
506
+ sessionId: action.sessionId
507
+ };
508
+ break;
479
509
  case "connected":
480
510
  state = {
481
511
  ...state,
@@ -562,6 +592,9 @@ var createVoiceStream = (path, options = {}) => {
562
592
  }
563
593
  });
564
594
  return {
595
+ callControl(message) {
596
+ connection.callControl(message);
597
+ },
565
598
  close() {
566
599
  unsubscribeConnection();
567
600
  connection.close();
@@ -605,6 +638,9 @@ var createVoiceStream = (path, options = {}) => {
605
638
  get assistantAudio() {
606
639
  return store.getSnapshot().assistantAudio;
607
640
  },
641
+ get call() {
642
+ return store.getSnapshot().call;
643
+ },
608
644
  sendAudio(audio) {
609
645
  connection.sendAudio(audio);
610
646
  },
@@ -900,6 +936,7 @@ var resolveVoiceRuntimePreset = (name = "default") => {
900
936
  var createInitialState2 = (stream) => ({
901
937
  assistantAudio: [...stream.assistantAudio],
902
938
  assistantTexts: [...stream.assistantTexts],
939
+ call: stream.call,
903
940
  error: stream.error,
904
941
  isConnected: stream.isConnected,
905
942
  isRecording: false,
@@ -929,6 +966,7 @@ var createVoiceController = (path, options = {}) => {
929
966
  ...state,
930
967
  assistantAudio: [...stream.assistantAudio],
931
968
  assistantTexts: [...stream.assistantTexts],
969
+ call: stream.call,
932
970
  error: stream.error,
933
971
  isConnected: stream.isConnected,
934
972
  partial: stream.partial,
@@ -956,7 +994,13 @@ var createVoiceController = (path, options = {}) => {
956
994
  capture = createMicrophoneCapture({
957
995
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
958
996
  onLevel: options.capture?.onLevel,
959
- onAudio: (audio) => stream.sendAudio(audio),
997
+ onAudio: (audio) => {
998
+ if (options.capture?.onAudio) {
999
+ options.capture.onAudio(audio, stream.sendAudio);
1000
+ return;
1001
+ }
1002
+ stream.sendAudio(audio);
1003
+ },
960
1004
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
961
1005
  });
962
1006
  return capture;
@@ -1006,6 +1050,7 @@ var createVoiceController = (path, options = {}) => {
1006
1050
  bindHTMX(bindingOptions) {
1007
1051
  return bindVoiceHTMX(stream, bindingOptions);
1008
1052
  },
1053
+ callControl: (message) => stream.callControl(message),
1009
1054
  close,
1010
1055
  endTurn: () => stream.endTurn(),
1011
1056
  get error() {
@@ -1058,6 +1103,478 @@ var createVoiceController = (path, options = {}) => {
1058
1103
  },
1059
1104
  get assistantAudio() {
1060
1105
  return state.assistantAudio;
1106
+ },
1107
+ get call() {
1108
+ return state.call;
1109
+ }
1110
+ };
1111
+ };
1112
+
1113
+ // src/client/audioPlayer.ts
1114
+ var DEFAULT_LOOKAHEAD_MS = 15;
1115
+ var createInitialState3 = () => ({
1116
+ activeSourceCount: 0,
1117
+ error: null,
1118
+ isActive: false,
1119
+ isPlaying: false,
1120
+ lastInterruptLatencyMs: undefined,
1121
+ lastPlaybackStopLatencyMs: undefined,
1122
+ processedChunkCount: 0,
1123
+ queuedChunkCount: 0
1124
+ });
1125
+ var getAudioContextCtor = () => {
1126
+ if (typeof window === "undefined") {
1127
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1128
+ }
1129
+ return window.AudioContext ?? window.webkitAudioContext;
1130
+ };
1131
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1132
+ const format = chunk.format;
1133
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1134
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1135
+ }
1136
+ const bytes = chunk.chunk;
1137
+ const channels = Math.max(1, format.channels);
1138
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1139
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1140
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1141
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1142
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1143
+ const channelData = audioBuffer.getChannelData(channelIndex);
1144
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1145
+ const sampleIndex = frameIndex * channels + channelIndex;
1146
+ const sampleOffset = sampleIndex * 2;
1147
+ if (sampleOffset + 1 >= bytes.byteLength) {
1148
+ channelData[frameIndex] = 0;
1149
+ continue;
1150
+ }
1151
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1152
+ }
1153
+ }
1154
+ return audioBuffer;
1155
+ };
1156
+ var createVoiceAudioPlayer = (source, options = {}) => {
1157
+ const subscribers = new Set;
1158
+ const sourceNodes = new Set;
1159
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1160
+ let state = createInitialState3();
1161
+ let audioContext = null;
1162
+ let outputNode = null;
1163
+ let queueEndTime = 0;
1164
+ let syncPromise = Promise.resolve();
1165
+ let interruptStartedAt = null;
1166
+ let interruptPromise = null;
1167
+ let resolveInterruptPromise = null;
1168
+ let interruptFallbackTimer = null;
1169
+ const notify = () => {
1170
+ for (const subscriber of subscribers) {
1171
+ subscriber();
1172
+ }
1173
+ };
1174
+ const setState = (next) => {
1175
+ state = {
1176
+ ...state,
1177
+ ...next
1178
+ };
1179
+ notify();
1180
+ };
1181
+ const clearError = () => {
1182
+ if (state.error !== null) {
1183
+ setState({ error: null });
1184
+ }
1185
+ };
1186
+ const clearInterruptTimer = () => {
1187
+ if (interruptFallbackTimer !== null) {
1188
+ clearTimeout(interruptFallbackTimer);
1189
+ interruptFallbackTimer = null;
1190
+ }
1191
+ };
1192
+ const resolveInterrupt = (latencyMs) => {
1193
+ clearInterruptTimer();
1194
+ interruptStartedAt = null;
1195
+ setState({
1196
+ activeSourceCount: sourceNodes.size,
1197
+ isPlaying: false,
1198
+ lastInterruptLatencyMs: latencyMs,
1199
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1200
+ });
1201
+ resolveInterruptPromise?.();
1202
+ resolveInterruptPromise = null;
1203
+ interruptPromise = null;
1204
+ };
1205
+ const estimateOutputStopLatencyMs = (context) => {
1206
+ if (!context) {
1207
+ return 0;
1208
+ }
1209
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1210
+ };
1211
+ const restoreOutputGain = (context) => {
1212
+ if (!outputNode) {
1213
+ return;
1214
+ }
1215
+ const gainValue = 1;
1216
+ if (outputNode.gain.setValueAtTime) {
1217
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1218
+ return;
1219
+ }
1220
+ outputNode.gain.value = gainValue;
1221
+ };
1222
+ const muteOutputGain = (context) => {
1223
+ if (!outputNode) {
1224
+ return;
1225
+ }
1226
+ const gainValue = 0;
1227
+ if (outputNode.gain.setValueAtTime) {
1228
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1229
+ return;
1230
+ }
1231
+ outputNode.gain.value = gainValue;
1232
+ };
1233
+ const maybeResolveInterrupt = () => {
1234
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1235
+ return;
1236
+ }
1237
+ resolveInterrupt(Date.now() - interruptStartedAt);
1238
+ };
1239
+ const ensureAudioContext = async () => {
1240
+ if (audioContext) {
1241
+ return audioContext;
1242
+ }
1243
+ if (options.createAudioContext) {
1244
+ audioContext = options.createAudioContext();
1245
+ } else {
1246
+ const AudioContextCtor = getAudioContextCtor();
1247
+ if (!AudioContextCtor) {
1248
+ throw new Error("Assistant audio playback requires AudioContext support.");
1249
+ }
1250
+ audioContext = new AudioContextCtor;
1251
+ }
1252
+ if (audioContext.createGain) {
1253
+ outputNode = audioContext.createGain();
1254
+ outputNode.connect?.(audioContext.destination);
1255
+ }
1256
+ queueEndTime = audioContext.currentTime;
1257
+ return audioContext;
1258
+ };
1259
+ const scheduleChunk = async (chunk) => {
1260
+ const context = await ensureAudioContext();
1261
+ const buffer = decodePCM16LEChunk(context, chunk);
1262
+ const node = context.createBufferSource();
1263
+ node.buffer = buffer;
1264
+ node.connect(outputNode ?? context.destination);
1265
+ node.onended = () => {
1266
+ sourceNodes.delete(node);
1267
+ node.disconnect?.();
1268
+ setState({
1269
+ activeSourceCount: sourceNodes.size,
1270
+ isPlaying: sourceNodes.size > 0 && state.isActive
1271
+ });
1272
+ maybeResolveInterrupt();
1273
+ };
1274
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1275
+ queueEndTime = startAt + buffer.duration;
1276
+ sourceNodes.add(node);
1277
+ setState({
1278
+ activeSourceCount: sourceNodes.size,
1279
+ isPlaying: true
1280
+ });
1281
+ node.start(startAt);
1282
+ };
1283
+ const stopQueuedPlayback = (options2) => {
1284
+ for (const node of [...sourceNodes]) {
1285
+ node.stop?.();
1286
+ }
1287
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1288
+ if (options2?.forceClear) {
1289
+ for (const node of sourceNodes) {
1290
+ node.disconnect?.();
1291
+ }
1292
+ sourceNodes.clear();
1293
+ maybeResolveInterrupt();
1294
+ }
1295
+ };
1296
+ const sync = async () => {
1297
+ if (!state.isActive) {
1298
+ return;
1299
+ }
1300
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1301
+ if (nextChunks.length === 0) {
1302
+ return;
1303
+ }
1304
+ try {
1305
+ clearError();
1306
+ for (const chunk of nextChunks) {
1307
+ await scheduleChunk(chunk);
1308
+ }
1309
+ setState({
1310
+ processedChunkCount: source.assistantAudio.length,
1311
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1312
+ });
1313
+ } catch (error) {
1314
+ setState({
1315
+ error: error instanceof Error ? error.message : String(error)
1316
+ });
1317
+ }
1318
+ };
1319
+ const queueSync = () => {
1320
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1321
+ return syncPromise;
1322
+ };
1323
+ const unsubscribeSource = source.subscribe(() => {
1324
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1325
+ player.start();
1326
+ return;
1327
+ }
1328
+ if (state.isActive) {
1329
+ queueSync();
1330
+ }
1331
+ });
1332
+ const player = {
1333
+ close: async () => {
1334
+ unsubscribeSource();
1335
+ stopQueuedPlayback({ forceClear: true });
1336
+ clearInterruptTimer();
1337
+ resolveInterruptPromise?.();
1338
+ resolveInterruptPromise = null;
1339
+ interruptPromise = null;
1340
+ interruptStartedAt = null;
1341
+ if (audioContext && audioContext.state !== "closed") {
1342
+ await audioContext.close();
1343
+ }
1344
+ audioContext = null;
1345
+ outputNode?.disconnect?.();
1346
+ outputNode = null;
1347
+ queueEndTime = 0;
1348
+ setState({
1349
+ activeSourceCount: 0,
1350
+ isActive: false,
1351
+ isPlaying: false
1352
+ });
1353
+ },
1354
+ get activeSourceCount() {
1355
+ return state.activeSourceCount;
1356
+ },
1357
+ get error() {
1358
+ return state.error;
1359
+ },
1360
+ getSnapshot: () => state,
1361
+ get isActive() {
1362
+ return state.isActive;
1363
+ },
1364
+ get isPlaying() {
1365
+ return state.isPlaying;
1366
+ },
1367
+ interrupt: async () => {
1368
+ const startedAt = Date.now();
1369
+ const context = await ensureAudioContext();
1370
+ interruptStartedAt = startedAt;
1371
+ muteOutputGain(context);
1372
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
1373
+ setState({
1374
+ isActive: false,
1375
+ isPlaying: sourceNodes.size > 0,
1376
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
1377
+ });
1378
+ if (sourceNodes.size === 0) {
1379
+ resolveInterrupt(playbackStopLatencyMs);
1380
+ return;
1381
+ }
1382
+ if (!interruptPromise) {
1383
+ interruptPromise = new Promise((resolve) => {
1384
+ resolveInterruptPromise = resolve;
1385
+ });
1386
+ }
1387
+ clearInterruptTimer();
1388
+ interruptFallbackTimer = setTimeout(() => {
1389
+ for (const node of sourceNodes) {
1390
+ node.disconnect?.();
1391
+ }
1392
+ sourceNodes.clear();
1393
+ resolveInterrupt(Date.now() - startedAt);
1394
+ }, 250);
1395
+ stopQueuedPlayback();
1396
+ await interruptPromise;
1397
+ },
1398
+ get lastInterruptLatencyMs() {
1399
+ return state.lastInterruptLatencyMs;
1400
+ },
1401
+ get lastPlaybackStopLatencyMs() {
1402
+ return state.lastPlaybackStopLatencyMs;
1403
+ },
1404
+ pause: async () => {
1405
+ if (!audioContext) {
1406
+ setState({
1407
+ activeSourceCount: 0,
1408
+ isActive: false,
1409
+ isPlaying: false
1410
+ });
1411
+ return;
1412
+ }
1413
+ await audioContext.suspend();
1414
+ setState({
1415
+ activeSourceCount: sourceNodes.size,
1416
+ isActive: false,
1417
+ isPlaying: false
1418
+ });
1419
+ },
1420
+ get processedChunkCount() {
1421
+ return state.processedChunkCount;
1422
+ },
1423
+ get queuedChunkCount() {
1424
+ return state.queuedChunkCount;
1425
+ },
1426
+ start: async () => {
1427
+ try {
1428
+ clearError();
1429
+ const context = await ensureAudioContext();
1430
+ restoreOutputGain(context);
1431
+ if (context.state === "suspended") {
1432
+ await context.resume();
1433
+ }
1434
+ setState({
1435
+ activeSourceCount: sourceNodes.size,
1436
+ isActive: true,
1437
+ isPlaying: context.state === "running"
1438
+ });
1439
+ await queueSync();
1440
+ } catch (error) {
1441
+ setState({
1442
+ error: error instanceof Error ? error.message : String(error),
1443
+ isActive: false,
1444
+ isPlaying: false
1445
+ });
1446
+ throw error;
1447
+ }
1448
+ },
1449
+ subscribe: (subscriber) => {
1450
+ subscribers.add(subscriber);
1451
+ return () => {
1452
+ subscribers.delete(subscriber);
1453
+ };
1454
+ }
1455
+ };
1456
+ return player;
1457
+ };
1458
+
1459
+ // src/client/bargeInMonitor.ts
1460
+ var DEFAULT_THRESHOLD_MS = 250;
1461
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1462
+ var summarize = (events, thresholdMs) => {
1463
+ const stopped = events.filter((event) => event.status === "stopped");
1464
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1465
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1466
+ const passed = stopped.length - failed;
1467
+ return {
1468
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1469
+ events: [...events],
1470
+ failed,
1471
+ lastEvent: events.at(-1),
1472
+ passed,
1473
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1474
+ thresholdMs,
1475
+ total: stopped.length
1476
+ };
1477
+ };
1478
+ var createVoiceBargeInMonitor = (options = {}) => {
1479
+ const listeners = new Set;
1480
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1481
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1482
+ const events = [];
1483
+ const emit = () => {
1484
+ for (const listener of listeners) {
1485
+ listener();
1486
+ }
1487
+ };
1488
+ const postEvent = (event) => {
1489
+ if (!options.path || typeof fetchImpl !== "function") {
1490
+ return;
1491
+ }
1492
+ fetchImpl(options.path, {
1493
+ body: JSON.stringify(event),
1494
+ headers: {
1495
+ "Content-Type": "application/json"
1496
+ },
1497
+ method: "POST"
1498
+ }).catch(() => {});
1499
+ };
1500
+ const record = (status, input) => {
1501
+ const event = {
1502
+ at: Date.now(),
1503
+ id: createEventId(),
1504
+ latencyMs: input.latencyMs,
1505
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1506
+ reason: input.reason,
1507
+ sessionId: input.sessionId,
1508
+ status,
1509
+ thresholdMs
1510
+ };
1511
+ events.push(event);
1512
+ postEvent(event);
1513
+ emit();
1514
+ return event;
1515
+ };
1516
+ return {
1517
+ getSnapshot: () => summarize(events, thresholdMs),
1518
+ recordRequested: (input) => record("requested", input),
1519
+ recordSkipped: (input) => record("skipped", input),
1520
+ recordStopped: (input) => record("stopped", input),
1521
+ subscribe: (subscriber) => {
1522
+ listeners.add(subscriber);
1523
+ return () => {
1524
+ listeners.delete(subscriber);
1525
+ };
1526
+ }
1527
+ };
1528
+ };
1529
+
1530
+ // src/client/duplex.ts
1531
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1532
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1533
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
1534
+ let lastPartial = controller.partial;
1535
+ const interruptIfPlaying = (reason) => {
1536
+ if (!player.isPlaying || options.enabled === false) {
1537
+ options.monitor?.recordSkipped({
1538
+ reason,
1539
+ sessionId: controller.sessionId
1540
+ });
1541
+ return;
1542
+ }
1543
+ options.monitor?.recordRequested({
1544
+ reason,
1545
+ sessionId: controller.sessionId
1546
+ });
1547
+ player.interrupt().then(() => {
1548
+ options.monitor?.recordStopped({
1549
+ latencyMs: player.lastInterruptLatencyMs,
1550
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1551
+ reason,
1552
+ sessionId: controller.sessionId
1553
+ });
1554
+ });
1555
+ };
1556
+ const unsubscribe = controller.subscribe(() => {
1557
+ if (options.interruptOnPartial === false) {
1558
+ lastPartial = controller.partial;
1559
+ return;
1560
+ }
1561
+ if (!lastPartial && controller.partial) {
1562
+ interruptIfPlaying("partial-transcript");
1563
+ }
1564
+ lastPartial = controller.partial;
1565
+ });
1566
+ return {
1567
+ close: () => {
1568
+ unsubscribe();
1569
+ },
1570
+ handleLevel: (level) => {
1571
+ if (shouldInterruptForLevel(level, options)) {
1572
+ interruptIfPlaying("input-level");
1573
+ }
1574
+ },
1575
+ sendAudio: (audio) => {
1576
+ interruptIfPlaying("manual-audio");
1577
+ controller.sendAudio(audio);
1061
1578
  }
1062
1579
  };
1063
1580
  };
@@ -1174,6 +1691,13 @@ var parsePromptList = (value) => {
1174
1691
  } catch {}
1175
1692
  return DEFAULT_GUIDED_PROMPTS;
1176
1693
  };
1694
+ var parseOptionalNumber = (value) => {
1695
+ if (!value) {
1696
+ return;
1697
+ }
1698
+ const parsed = Number(value);
1699
+ return Number.isFinite(parsed) ? parsed : undefined;
1700
+ };
1177
1701
  var requireElement = (root, selector, ctor, name) => {
1178
1702
  const value = selector ? document.querySelector(selector) : null;
1179
1703
  if (value instanceof ctor) {
@@ -1224,6 +1748,13 @@ var initVoiceHTMXRoot = (root) => {
1224
1748
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1225
1749
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1226
1750
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
1751
+ const bargeInPath = root.dataset.voiceBargeInPath;
1752
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
1753
+ path: bargeInPath,
1754
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
1755
+ }) : null;
1756
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
1757
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1227
1758
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1228
1759
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1229
1760
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
@@ -1237,9 +1768,27 @@ var initVoiceHTMXRoot = (root) => {
1237
1768
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1238
1769
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1239
1770
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
1771
+ let activeMode = null;
1772
+ let hasStartedModes = {
1773
+ general: false,
1774
+ guided: false
1775
+ };
1776
+ let isCapturing = false;
1777
+ let micError = null;
1778
+ let waveLevels = createInitialVoiceWaveLevels();
1779
+ let guidedBargeInBinding = null;
1780
+ let generalBargeInBinding = null;
1240
1781
  const guidedVoice = createVoiceController(guidedPath, {
1241
1782
  capture: {
1783
+ onAudio: (audio, sendAudio) => {
1784
+ if (guidedBargeInBinding) {
1785
+ guidedBargeInBinding.sendAudio(audio);
1786
+ return;
1787
+ }
1788
+ sendAudio(audio);
1789
+ },
1242
1790
  onLevel: (level) => {
1791
+ guidedBargeInBinding?.handleLevel(level);
1243
1792
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1244
1793
  renderWave();
1245
1794
  }
@@ -1248,7 +1797,15 @@ var initVoiceHTMXRoot = (root) => {
1248
1797
  });
1249
1798
  const generalVoice = createVoiceController(generalPath, {
1250
1799
  capture: {
1800
+ onAudio: (audio, sendAudio) => {
1801
+ if (generalBargeInBinding) {
1802
+ generalBargeInBinding.sendAudio(audio);
1803
+ return;
1804
+ }
1805
+ sendAudio(audio);
1806
+ },
1251
1807
  onLevel: (level) => {
1808
+ generalBargeInBinding?.handleLevel(level);
1252
1809
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1253
1810
  renderWave();
1254
1811
  }
@@ -1257,15 +1814,18 @@ var initVoiceHTMXRoot = (root) => {
1257
1814
  });
1258
1815
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1259
1816
  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();
1817
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
1818
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
1819
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
1820
+ interruptThreshold: bargeInSpeechThreshold,
1821
+ monitor: bargeInMonitor ?? undefined
1822
+ });
1823
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
1824
+ interruptThreshold: bargeInSpeechThreshold,
1825
+ monitor: bargeInMonitor ?? undefined
1826
+ });
1268
1827
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
1828
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1269
1829
  const renderWave = () => {
1270
1830
  const path = createVoiceWavePath(waveLevels);
1271
1831
  voiceWaveGlow.setAttribute("d", path);
@@ -1343,8 +1903,18 @@ var initVoiceHTMXRoot = (root) => {
1343
1903
  render();
1344
1904
  }
1345
1905
  };
1346
- guidedVoice.subscribe(render);
1347
- generalVoice.subscribe(render);
1906
+ guidedVoice.subscribe(() => {
1907
+ if (guidedVoice.assistantAudio.length > 0) {
1908
+ guidedAudioPlayer.start().catch(() => {});
1909
+ }
1910
+ render();
1911
+ });
1912
+ generalVoice.subscribe(() => {
1913
+ if (generalVoice.assistantAudio.length > 0) {
1914
+ generalAudioPlayer.start().catch(() => {});
1915
+ }
1916
+ render();
1917
+ });
1348
1918
  startGuidedButton.addEventListener("click", () => {
1349
1919
  startMode("guided");
1350
1920
  });
@@ -1357,6 +1927,10 @@ var initVoiceHTMXRoot = (root) => {
1357
1927
  window.addEventListener("beforeunload", () => {
1358
1928
  guidedVoice.stopRecording();
1359
1929
  generalVoice.stopRecording();
1930
+ guidedBargeInBinding?.close();
1931
+ generalBargeInBinding?.close();
1932
+ guidedAudioPlayer.close();
1933
+ generalAudioPlayer.close();
1360
1934
  stopGuidedBinding();
1361
1935
  stopGeneralBinding();
1362
1936
  guidedVoice.close();