@absolutejs/voice 0.0.22-beta.66 → 0.0.22-beta.660

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 (514) hide show
  1. package/LICENSE +88 -0
  2. package/README.md +4550 -1180
  3. package/dist/angular/index.d.ts +36 -7
  4. package/dist/angular/index.js +4425 -513
  5. package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
  6. package/dist/angular/voice-call-debugger.service.d.ts +12 -0
  7. package/dist/angular/voice-call-player.service.d.ts +19 -0
  8. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  9. package/dist/angular/voice-controller.service.d.ts +4 -1
  10. package/dist/angular/voice-cost-dashboard.service.d.ts +15 -0
  11. package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
  12. package/dist/angular/voice-live-agent-console.service.d.ts +16 -0
  13. package/dist/angular/voice-live-call-viewer.service.d.ts +16 -0
  14. package/dist/angular/voice-live-ops.service.d.ts +11 -0
  15. package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
  16. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  17. package/dist/angular/voice-platform-coverage.service.d.ts +12 -0
  18. package/dist/angular/voice-profile-comparison.service.d.ts +12 -0
  19. package/dist/angular/voice-proof-trends.service.d.ts +12 -0
  20. package/dist/angular/voice-provider-capabilities.service.d.ts +2 -2
  21. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  22. package/dist/angular/voice-provider-status.service.d.ts +2 -2
  23. package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
  24. package/dist/angular/voice-reconnect-profile-evidence.service.d.ts +12 -0
  25. package/dist/angular/voice-replay-timeline.service.d.ts +13 -0
  26. package/dist/angular/voice-routing-status.service.d.ts +1 -1
  27. package/dist/angular/voice-session-observability.service.d.ts +12 -0
  28. package/dist/angular/voice-session-snapshot.service.d.ts +13 -0
  29. package/dist/angular/voice-stream.service.d.ts +6 -2
  30. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  31. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  32. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  33. package/dist/angular/voice-widget.service.d.ts +18 -0
  34. package/dist/angular/voice-workflow-status.service.d.ts +2 -2
  35. package/dist/client/actions.d.ts +140 -2
  36. package/dist/client/agentSquadStatus.d.ts +37 -0
  37. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  38. package/dist/client/audioPlayer.d.ts +12 -2
  39. package/dist/client/bargeInMonitor.d.ts +7 -0
  40. package/dist/client/browserMedia.d.ts +8 -0
  41. package/dist/client/browserNoiseSuppression.d.ts +42 -0
  42. package/dist/client/browserVoiceSupport.d.ts +22 -0
  43. package/dist/client/callDebugger.d.ts +21 -0
  44. package/dist/client/callDebuggerWidget.d.ts +30 -0
  45. package/dist/client/callPlayer.d.ts +41 -0
  46. package/dist/client/campaignDialerProof.d.ts +25 -0
  47. package/dist/client/connection.d.ts +13 -4
  48. package/dist/client/controller.d.ts +1 -1
  49. package/dist/client/conversationAnalytics.d.ts +30 -0
  50. package/dist/client/costDashboard.d.ts +27 -0
  51. package/dist/client/createVoiceStream.d.ts +1 -1
  52. package/dist/client/deliveryRuntime.d.ts +36 -0
  53. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  54. package/dist/client/duplex.d.ts +2 -2
  55. package/dist/client/htmx.d.ts +1 -1
  56. package/dist/client/htmxAttributes.d.ts +24 -0
  57. package/dist/client/htmxBootstrap.js +1760 -147
  58. package/dist/client/htmxDashboardRenderers.d.ts +71 -0
  59. package/dist/client/index.d.ts +109 -29
  60. package/dist/client/index.js +10886 -581
  61. package/dist/client/liveAgentConsole.d.ts +28 -0
  62. package/dist/client/liveCallViewer.d.ts +42 -0
  63. package/dist/client/liveOps.d.ts +22 -0
  64. package/dist/client/liveOpsWidget.d.ts +23 -0
  65. package/dist/client/liveTurnLatency.d.ts +41 -0
  66. package/dist/client/microphone.d.ts +5 -4
  67. package/dist/client/opsActionCenter.d.ts +56 -0
  68. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  69. package/dist/client/opsActionHistory.d.ts +21 -0
  70. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  71. package/dist/client/opsStatus.d.ts +21 -0
  72. package/dist/client/opsStatusWidget.d.ts +10 -10
  73. package/dist/client/platformCoverage.d.ts +21 -0
  74. package/dist/client/platformCoverageWidget.d.ts +38 -0
  75. package/dist/client/profileComparison.d.ts +21 -0
  76. package/dist/client/profileComparisonWidget.d.ts +41 -0
  77. package/dist/client/profileSwitchRecommendation.d.ts +21 -0
  78. package/dist/client/profileSwitchRecommendationWidget.d.ts +12 -0
  79. package/dist/client/proofTrends.d.ts +21 -0
  80. package/dist/client/proofTrendsWidget.d.ts +38 -0
  81. package/dist/client/providerCapabilities.d.ts +5 -3
  82. package/dist/client/providerCapabilitiesWidget.d.ts +6 -6
  83. package/dist/client/providerContracts.d.ts +21 -0
  84. package/dist/client/providerContractsWidget.d.ts +37 -0
  85. package/dist/client/providerSimulationControls.d.ts +3 -3
  86. package/dist/client/providerSimulationControlsWidget.d.ts +5 -5
  87. package/dist/client/providerStatus.d.ts +5 -3
  88. package/dist/client/providerStatusWidget.d.ts +6 -6
  89. package/dist/client/reactiveSource.d.ts +8 -0
  90. package/dist/client/readinessFailures.d.ts +21 -0
  91. package/dist/client/readinessFailuresWidget.d.ts +42 -0
  92. package/dist/client/reconnectProfileEvidence.d.ts +21 -0
  93. package/dist/client/reconnectProfileEvidenceWidget.d.ts +39 -0
  94. package/dist/client/replayTimeline.d.ts +26 -0
  95. package/dist/client/routingStatus.d.ts +5 -3
  96. package/dist/client/routingStatusWidget.d.ts +10 -6
  97. package/dist/client/sessionObservability.d.ts +21 -0
  98. package/dist/client/sessionObservabilityWidget.d.ts +31 -0
  99. package/dist/client/sessionSnapshot.d.ts +23 -0
  100. package/dist/client/sessionSnapshotWidget.d.ts +33 -0
  101. package/dist/client/store.d.ts +1 -1
  102. package/dist/client/timeStretch.d.ts +5 -0
  103. package/dist/client/traceTimeline.d.ts +21 -0
  104. package/dist/client/traceTimelineWidget.d.ts +36 -0
  105. package/dist/client/turnLatency.d.ts +24 -0
  106. package/dist/client/turnLatencyWidget.d.ts +33 -0
  107. package/dist/client/turnQuality.d.ts +21 -0
  108. package/dist/client/turnQualityWidget.d.ts +32 -0
  109. package/dist/client/voiceWidgetView.d.ts +48 -0
  110. package/dist/client/workflowStatus.d.ts +5 -3
  111. package/dist/{agent.d.ts → core/agent.d.ts} +110 -7
  112. package/dist/core/agentPerformanceReport.d.ts +40 -0
  113. package/dist/core/agentSquadContract.d.ts +98 -0
  114. package/dist/core/agentState.d.ts +12 -0
  115. package/dist/core/agentTools.d.ts +132 -0
  116. package/dist/core/aiScorecard.d.ts +32 -0
  117. package/dist/core/aiVoiceModel.d.ts +15 -0
  118. package/dist/core/amdDetector.d.ts +25 -0
  119. package/dist/{assistant.d.ts → core/assistant.d.ts} +21 -14
  120. package/dist/core/assistantExperiment.d.ts +42 -0
  121. package/dist/{assistantHealth.d.ts → core/assistantHealth.d.ts} +9 -9
  122. package/dist/{assistantMemory.d.ts → core/assistantMemory.d.ts} +10 -10
  123. package/dist/core/assistantMode.d.ts +22 -0
  124. package/dist/{audioConditioning.d.ts → core/audioConditioning.d.ts} +2 -2
  125. package/dist/core/audit.d.ts +131 -0
  126. package/dist/core/auditDeliveryRoutes.d.ts +85 -0
  127. package/dist/core/auditExport.d.ts +34 -0
  128. package/dist/core/auditRoutes.d.ts +66 -0
  129. package/dist/core/auditSinks.d.ts +151 -0
  130. package/dist/core/backchannel.d.ts +25 -0
  131. package/dist/core/bargeInDetector.d.ts +51 -0
  132. package/dist/core/bargeInRoutes.d.ts +56 -0
  133. package/dist/core/bookingFlow.d.ts +43 -0
  134. package/dist/core/browserCallProfiles.d.ts +120 -0
  135. package/dist/core/browserMediaRoutes.d.ts +62 -0
  136. package/dist/core/cachedTTS.d.ts +54 -0
  137. package/dist/core/calendarAdapter.d.ts +47 -0
  138. package/dist/core/calendarSlots.d.ts +35 -0
  139. package/dist/core/callDebugger.d.ts +66 -0
  140. package/dist/core/callDisposition.d.ts +38 -0
  141. package/dist/core/callQuota.d.ts +54 -0
  142. package/dist/core/callScorecard.d.ts +53 -0
  143. package/dist/core/callerCRMLinker.d.ts +29 -0
  144. package/dist/core/callerMemory.d.ts +37 -0
  145. package/dist/core/callingWindow.d.ts +26 -0
  146. package/dist/core/campaign.d.ts +795 -0
  147. package/dist/core/campaignControls.d.ts +37 -0
  148. package/dist/core/campaignDialers.d.ts +111 -0
  149. package/dist/core/campaignTemplate.d.ts +16 -0
  150. package/dist/core/competitiveCoverage.d.ts +141 -0
  151. package/dist/core/conversationSimulator.d.ts +73 -0
  152. package/dist/{correction.d.ts → core/correction.d.ts} +5 -4
  153. package/dist/core/costAccounting.d.ts +90 -0
  154. package/dist/core/costPredictor.d.ts +74 -0
  155. package/dist/core/crmCallLogger.d.ts +37 -0
  156. package/dist/core/crmContract.d.ts +70 -0
  157. package/dist/core/dataControl.d.ts +180 -0
  158. package/dist/core/debugTiming.d.ts +11 -0
  159. package/dist/core/defineVoiceAssistant.d.ts +68 -0
  160. package/dist/core/deliveryRuntime.d.ts +159 -0
  161. package/dist/core/deliverySinkRoutes.d.ts +117 -0
  162. package/dist/core/demoReadyRoutes.d.ts +98 -0
  163. package/dist/{diagnosticsRoutes.d.ts → core/diagnosticsRoutes.d.ts} +2 -2
  164. package/dist/core/dncRegistry.d.ts +38 -0
  165. package/dist/core/dtmfCollector.d.ts +37 -0
  166. package/dist/{evalRoutes.d.ts → core/evalRoutes.d.ts} +26 -20
  167. package/dist/{fileStore.d.ts → core/fileStore.d.ts} +34 -20
  168. package/dist/core/guardrails.d.ts +128 -0
  169. package/dist/{handoff.d.ts → core/handoff.d.ts} +10 -10
  170. package/dist/{handoffHealth.d.ts → core/handoffHealth.d.ts} +9 -9
  171. package/dist/core/hardenedFetch.d.ts +1 -0
  172. package/dist/core/holdAudio.d.ts +23 -0
  173. package/dist/{htmx.d.ts → core/htmx.d.ts} +2 -2
  174. package/dist/core/htmxDashboardRoutes.d.ts +250 -0
  175. package/dist/core/iceServers.d.ts +34 -0
  176. package/dist/core/incidentBundle.d.ts +119 -0
  177. package/dist/core/incidentTimeline.d.ts +260 -0
  178. package/dist/core/ivrPlan.d.ts +40 -0
  179. package/dist/core/latencySlo.d.ts +56 -0
  180. package/dist/core/liveCoach.d.ts +43 -0
  181. package/dist/core/liveLatency.d.ts +78 -0
  182. package/dist/core/liveOps.d.ts +190 -0
  183. package/dist/core/llmJudge.d.ts +45 -0
  184. package/dist/{logger.d.ts → core/logger.d.ts} +1 -2
  185. package/dist/core/mcpToolset.d.ts +58 -0
  186. package/dist/core/mediaPipelineRoutes.d.ts +171 -0
  187. package/dist/core/mediaPipelineSurfaces.d.ts +48 -0
  188. package/dist/{memoryStore.d.ts → core/memoryStore.d.ts} +1 -1
  189. package/dist/core/midCallSummary.d.ts +27 -0
  190. package/dist/{modelAdapters.d.ts → core/modelAdapters.d.ts} +64 -7
  191. package/dist/core/monitor.d.ts +148 -0
  192. package/dist/core/multilingualProof.d.ts +77 -0
  193. package/dist/core/noShowPredictor.d.ts +46 -0
  194. package/dist/core/numberNormalizer.d.ts +1 -0
  195. package/dist/core/oauth2TokenSource.d.ts +21 -0
  196. package/dist/core/observabilityExport.d.ts +501 -0
  197. package/dist/core/openaiTTS.d.ts +18 -0
  198. package/dist/core/operationalStatus.d.ts +87 -0
  199. package/dist/core/operationsRecord.d.ts +371 -0
  200. package/dist/{ops.d.ts → core/ops.d.ts} +70 -70
  201. package/dist/core/opsActionAuditRoutes.d.ts +99 -0
  202. package/dist/{opsConsoleRoutes.d.ts → core/opsConsoleRoutes.d.ts} +11 -8
  203. package/dist/{opsPresets.d.ts → core/opsPresets.d.ts} +2 -2
  204. package/dist/core/opsRecovery.d.ts +137 -0
  205. package/dist/{opsRuntime.d.ts → core/opsRuntime.d.ts} +6 -6
  206. package/dist/{opsSinks.d.ts → core/opsSinks.d.ts} +19 -19
  207. package/dist/core/opsStatus.d.ts +76 -0
  208. package/dist/core/opsStatusRoutes.d.ts +33 -0
  209. package/dist/{opsWebhook.d.ts → core/opsWebhook.d.ts} +15 -15
  210. package/dist/core/otelExporter.d.ts +83 -0
  211. package/dist/core/outcomeContract.d.ts +146 -0
  212. package/dist/{outcomeRecipes.d.ts → core/outcomeRecipes.d.ts} +4 -4
  213. package/dist/core/pathway.d.ts +94 -0
  214. package/dist/core/pathwayCompiler.d.ts +31 -0
  215. package/dist/core/pathwayGenerator.d.ts +27 -0
  216. package/dist/core/pathwayRuntime.d.ts +57 -0
  217. package/dist/core/pathwaySlotCollector.d.ts +29 -0
  218. package/dist/core/pathwayVisualizer.d.ts +8 -0
  219. package/dist/core/phoneAgent.d.ts +139 -0
  220. package/dist/core/phoneAgentProductionSmoke.d.ts +115 -0
  221. package/dist/core/phoneProvisioning.d.ts +29 -0
  222. package/dist/core/platformCoverage.d.ts +91 -0
  223. package/dist/{plugin.d.ts → core/plugin.d.ts} +11 -8
  224. package/dist/core/postCallAnalysis.d.ts +98 -0
  225. package/dist/core/postCallSurvey.d.ts +41 -0
  226. package/dist/{postgresStore.d.ts → core/postgresStore.d.ts} +20 -9
  227. package/dist/{presets.d.ts → core/presets.d.ts} +3 -3
  228. package/dist/core/productionReadiness.d.ts +757 -0
  229. package/dist/core/profileSwitchRecommendation.d.ts +350 -0
  230. package/dist/core/promptInjectionGuard.d.ts +30 -0
  231. package/dist/core/proofAssertions.d.ts +32 -0
  232. package/dist/core/proofPack.d.ts +211 -0
  233. package/dist/core/proofRunner.d.ts +79 -0
  234. package/dist/core/proofTrends.d.ts +966 -0
  235. package/dist/{providerAdapters.d.ts → core/providerAdapters.d.ts} +5 -5
  236. package/dist/{providerCapabilities.d.ts → core/providerCapabilities.d.ts} +9 -9
  237. package/dist/core/providerDecisionTraces.d.ts +130 -0
  238. package/dist/{providerHealth.d.ts → core/providerHealth.d.ts} +15 -5
  239. package/dist/core/providerOrchestration.d.ts +109 -0
  240. package/dist/core/providerRouterTraces.d.ts +35 -0
  241. package/dist/core/providerRoutingContract.d.ts +71 -0
  242. package/dist/core/providerSlo.d.ts +142 -0
  243. package/dist/core/providerStackRecommendations.d.ts +188 -0
  244. package/dist/core/qualityDriftDetector.d.ts +44 -0
  245. package/dist/{qualityRoutes.d.ts → core/qualityRoutes.d.ts} +7 -7
  246. package/dist/{queue.d.ts → core/queue.d.ts} +48 -39
  247. package/dist/core/ragTool.d.ts +52 -0
  248. package/dist/core/readinessProfiles.d.ts +45 -0
  249. package/dist/core/realtimeChannel.d.ts +136 -0
  250. package/dist/core/realtimeProviderContracts.d.ts +133 -0
  251. package/dist/core/reconnectContract.d.ts +177 -0
  252. package/dist/core/recordingRedaction.d.ts +47 -0
  253. package/dist/core/recordingStore.d.ts +60 -0
  254. package/dist/core/redaction.d.ts +13 -0
  255. package/dist/core/reminderScheduler.d.ts +43 -0
  256. package/dist/{resilienceRoutes.d.ts → core/resilienceRoutes.d.ts} +34 -5
  257. package/dist/core/retention.d.ts +37 -0
  258. package/dist/core/retryPolicy.d.ts +38 -0
  259. package/dist/core/routeAuth.d.ts +58 -0
  260. package/dist/{routing.d.ts → core/routing.d.ts} +2 -2
  261. package/dist/{runtimeOps.d.ts → core/runtimeOps.d.ts} +3 -3
  262. package/dist/{s3Store.d.ts → core/s3Store.d.ts} +12 -3
  263. package/dist/core/scorecardCalibration.d.ts +31 -0
  264. package/dist/core/scribe.d.ts +50 -0
  265. package/dist/core/semanticTurn.d.ts +37 -0
  266. package/dist/{session.d.ts → core/session.d.ts} +1 -1
  267. package/dist/core/sessionObservability.d.ts +145 -0
  268. package/dist/{sessionReplay.d.ts → core/sessionReplay.d.ts} +31 -19
  269. package/dist/core/sessionSnapshot.d.ts +109 -0
  270. package/dist/core/simulationSuite.d.ts +144 -0
  271. package/dist/core/sloCalibration.d.ts +185 -0
  272. package/dist/{sqliteStore.d.ts → core/sqliteStore.d.ts} +20 -9
  273. package/dist/{store.d.ts → core/store.d.ts} +1 -1
  274. package/dist/core/supervisorPermissions.d.ts +33 -0
  275. package/dist/core/supervisorPresence.d.ts +49 -0
  276. package/dist/core/telephonyMediaRoutes.d.ts +72 -0
  277. package/dist/core/telephonyOutcome.d.ts +269 -0
  278. package/dist/{toolContract.d.ts → core/toolContract.d.ts} +46 -15
  279. package/dist/{toolRuntime.d.ts → core/toolRuntime.d.ts} +4 -4
  280. package/dist/{trace.d.ts → core/trace.d.ts} +61 -22
  281. package/dist/core/traceDeliveryRoutes.d.ts +86 -0
  282. package/dist/core/traceTimeline.d.ts +97 -0
  283. package/dist/core/transcriptAnnotator.d.ts +41 -0
  284. package/dist/{turnDetection.d.ts → core/turnDetection.d.ts} +2 -1
  285. package/dist/core/turnLatency.d.ts +95 -0
  286. package/dist/core/turnProfiles.d.ts +3 -0
  287. package/dist/core/turnQuality.d.ts +94 -0
  288. package/dist/core/types.d.ts +1755 -0
  289. package/dist/core/vapiAdapter.d.ts +160 -0
  290. package/dist/core/variableAnalytics.d.ts +47 -0
  291. package/dist/core/voiceConfiguration.d.ts +8 -0
  292. package/dist/core/voiceMonitoring.d.ts +444 -0
  293. package/dist/core/webhookFanout.d.ts +48 -0
  294. package/dist/core/webhookVerification.d.ts +27 -0
  295. package/dist/core/whisperChannel.d.ts +50 -0
  296. package/dist/{workflowContract.d.ts → core/workflowContract.d.ts} +21 -21
  297. package/dist/core/writeBehindStore.d.ts +41 -0
  298. package/dist/core/zeroDataRetention.d.ts +31 -0
  299. package/dist/drizzle/assistantMemory.d.ts +100 -0
  300. package/dist/drizzle/eval.d.ts +55 -0
  301. package/dist/drizzle/handoff.d.ts +56 -0
  302. package/dist/drizzle/incidentBundle.d.ts +55 -0
  303. package/dist/drizzle/index.d.ts +991 -0
  304. package/dist/drizzle/index.js +3059 -0
  305. package/dist/drizzle/observabilityExport.d.ts +55 -0
  306. package/dist/drizzle/proofTrends.d.ts +114 -0
  307. package/dist/drizzle/runtimeStorage.d.ts +1183 -0
  308. package/dist/drizzle/shared.d.ts +69 -0
  309. package/dist/embed/index.d.ts +38 -0
  310. package/dist/embed/index.js +2064 -0
  311. package/dist/embed/voice-widget.js +10 -0
  312. package/dist/index.d.ts +379 -80
  313. package/dist/index.js +48304 -8042
  314. package/dist/internal/evidence.d.ts +10 -0
  315. package/dist/internal/html.d.ts +6 -0
  316. package/dist/internal/status.d.ts +7 -0
  317. package/dist/manifest.d.ts +17 -0
  318. package/dist/manifest.js +424 -0
  319. package/dist/manifest.json +427 -0
  320. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  321. package/dist/react/VoiceCallDebuggerLaunch.d.ts +6 -0
  322. package/dist/react/VoiceCallPlayer.d.ts +11 -0
  323. package/dist/react/VoiceCostDashboard.d.ts +10 -0
  324. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  325. package/dist/react/VoiceLiveAgentConsole.d.ts +11 -0
  326. package/dist/react/VoiceLiveCallViewer.d.ts +9 -0
  327. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  328. package/dist/react/VoiceOpsStatus.d.ts +1 -1
  329. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  330. package/dist/react/VoiceProfileComparison.d.ts +6 -0
  331. package/dist/react/VoiceProfileSwitchRecommendation.d.ts +6 -0
  332. package/dist/react/VoiceProofTrends.d.ts +6 -0
  333. package/dist/react/VoiceProviderCapabilities.d.ts +1 -1
  334. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  335. package/dist/react/VoiceProviderSimulationControls.d.ts +1 -1
  336. package/dist/react/VoiceProviderStatus.d.ts +1 -1
  337. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  338. package/dist/react/VoiceReconnectProfileEvidence.d.ts +6 -0
  339. package/dist/react/VoiceReplayTimeline.d.ts +6 -0
  340. package/dist/react/VoiceRoutingStatus.d.ts +1 -1
  341. package/dist/react/VoiceSessionObservability.d.ts +6 -0
  342. package/dist/react/VoiceSessionSnapshot.d.ts +6 -0
  343. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  344. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  345. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  346. package/dist/react/VoiceWidget.d.ts +13 -0
  347. package/dist/react/index.d.ts +80 -13
  348. package/dist/react/index.js +13728 -1784
  349. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  350. package/dist/react/useVoiceCallDebugger.d.ts +8 -0
  351. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  352. package/dist/react/useVoiceController.d.ts +6 -2
  353. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  354. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  355. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  356. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  357. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  358. package/dist/react/useVoiceProfileComparison.d.ts +8 -0
  359. package/dist/react/useVoiceProfileSwitchRecommendation.d.ts +8 -0
  360. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  361. package/dist/react/useVoiceProviderCapabilities.d.ts +1 -1
  362. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  363. package/dist/react/useVoiceProviderSimulationControls.d.ts +1 -1
  364. package/dist/react/useVoiceProviderStatus.d.ts +1 -1
  365. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  366. package/dist/react/useVoiceReconnectProfileEvidence.d.ts +8 -0
  367. package/dist/react/useVoiceRoutingStatus.d.ts +1 -1
  368. package/dist/react/useVoiceSessionObservability.d.ts +8 -0
  369. package/dist/react/useVoiceSessionSnapshot.d.ts +9 -0
  370. package/dist/react/useVoiceStream.d.ts +6 -2
  371. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  372. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  373. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  374. package/dist/react/useVoiceWorkflowStatus.d.ts +1 -1
  375. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  376. package/dist/svelte/createVoiceCallDebugger.d.ts +10 -0
  377. package/dist/svelte/createVoiceCallPlayer.d.ts +33 -0
  378. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  379. package/dist/svelte/createVoiceCostDashboard.d.ts +13 -0
  380. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  381. package/dist/svelte/createVoiceLiveAgentConsole.d.ts +23 -0
  382. package/dist/svelte/createVoiceLiveCallViewer.d.ts +26 -0
  383. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  384. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  385. package/dist/svelte/createVoiceOpsStatus.d.ts +4 -4
  386. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  387. package/dist/svelte/createVoiceProfileComparison.d.ts +7 -0
  388. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  389. package/dist/svelte/createVoiceProviderCapabilities.d.ts +2 -2
  390. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  391. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +2 -2
  392. package/dist/svelte/createVoiceProviderStatus.d.ts +2 -2
  393. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  394. package/dist/svelte/createVoiceReconnectProfileEvidence.d.ts +7 -0
  395. package/dist/svelte/createVoiceReplayTimeline.d.ts +13 -0
  396. package/dist/svelte/createVoiceRoutingStatus.d.ts +2 -2
  397. package/dist/svelte/createVoiceSessionObservability.d.ts +10 -0
  398. package/dist/svelte/createVoiceSessionSnapshot.d.ts +11 -0
  399. package/dist/svelte/createVoiceStream.d.ts +1 -1
  400. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  401. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  402. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  403. package/dist/svelte/createVoiceWidget.d.ts +19 -0
  404. package/dist/svelte/createVoiceWorkflowStatus.d.ts +2 -2
  405. package/dist/svelte/index.d.ts +37 -9
  406. package/dist/svelte/index.js +7494 -1540
  407. package/dist/telephony/contract.d.ts +61 -0
  408. package/dist/telephony/matrix.d.ts +97 -0
  409. package/dist/telephony/plivo.d.ts +303 -0
  410. package/dist/telephony/response.d.ts +1 -1
  411. package/dist/telephony/security.d.ts +182 -0
  412. package/dist/telephony/telnyx.d.ts +291 -0
  413. package/dist/telephony/twilio.d.ts +153 -15
  414. package/dist/testing/accuracy.d.ts +18 -1
  415. package/dist/testing/audioMatrix.d.ts +22 -0
  416. package/dist/testing/benchmark.d.ts +24 -15
  417. package/dist/testing/confidenceCalibration.d.ts +19 -0
  418. package/dist/testing/conformance.d.ts +11 -0
  419. package/dist/testing/corrected.d.ts +9 -9
  420. package/dist/testing/criticalFields.d.ts +22 -0
  421. package/dist/testing/duplex.d.ts +5 -5
  422. package/dist/testing/fixtures.d.ts +7 -4
  423. package/dist/testing/index.d.ts +21 -13
  424. package/dist/testing/index.js +8889 -1153
  425. package/dist/testing/ioProviderSimulator.d.ts +5 -5
  426. package/dist/testing/outcomes.d.ts +12 -0
  427. package/dist/testing/provenance.d.ts +44 -0
  428. package/dist/testing/providerSimulator.d.ts +5 -5
  429. package/dist/testing/review.d.ts +8 -8
  430. package/dist/testing/routingBenchmark.d.ts +16 -0
  431. package/dist/testing/sessionBenchmark.d.ts +13 -13
  432. package/dist/testing/statistics.d.ts +30 -0
  433. package/dist/testing/stt.d.ts +3 -3
  434. package/dist/testing/telephony.d.ts +27 -2
  435. package/dist/testing/tts.d.ts +2 -2
  436. package/dist/vue/VoiceCallDebuggerLaunch.d.ts +72 -0
  437. package/dist/vue/VoiceCallPlayer.d.ts +40 -0
  438. package/dist/vue/VoiceCostDashboard.d.ts +57 -0
  439. package/dist/vue/VoiceDeliveryRuntime.d.ts +34 -0
  440. package/dist/vue/VoiceLiveAgentConsole.d.ts +50 -0
  441. package/dist/vue/VoiceLiveCallViewer.d.ts +35 -0
  442. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  443. package/dist/vue/VoiceOpsStatus.d.ts +4 -0
  444. package/dist/vue/VoicePlatformCoverage.d.ts +27 -0
  445. package/dist/vue/VoiceProofTrends.d.ts +25 -0
  446. package/dist/vue/VoiceProviderCapabilities.d.ts +4 -0
  447. package/dist/vue/VoiceProviderContracts.d.ts +25 -0
  448. package/dist/vue/VoiceProviderSimulationControls.d.ts +3 -3
  449. package/dist/vue/VoiceProviderStatus.d.ts +4 -0
  450. package/dist/vue/VoiceReadinessFailures.d.ts +25 -0
  451. package/dist/vue/VoiceReconnectProfileEvidence.d.ts +25 -0
  452. package/dist/vue/VoiceReplayTimeline.d.ts +17 -0
  453. package/dist/vue/VoiceRoutingStatus.d.ts +4 -0
  454. package/dist/vue/VoiceSessionObservability.d.ts +27 -0
  455. package/dist/vue/VoiceSessionSnapshot.d.ts +72 -0
  456. package/dist/vue/VoiceTurnLatency.d.ts +73 -0
  457. package/dist/vue/VoiceTurnQuality.d.ts +55 -0
  458. package/dist/vue/VoiceWidget.d.ts +77 -0
  459. package/dist/vue/index.d.ts +49 -13
  460. package/dist/vue/index.js +13050 -1931
  461. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  462. package/dist/vue/useVoiceCallDebugger.d.ts +10 -0
  463. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  464. package/dist/vue/useVoiceController.d.ts +10 -7
  465. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  466. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  467. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  468. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  469. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  470. package/dist/vue/useVoiceProfileComparison.d.ts +9 -0
  471. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  472. package/dist/vue/useVoiceProviderCapabilities.d.ts +3 -3
  473. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  474. package/dist/vue/useVoiceProviderSimulationControls.d.ts +2 -2
  475. package/dist/vue/useVoiceProviderStatus.d.ts +3 -3
  476. package/dist/vue/useVoiceReadinessFailures.d.ts +959 -0
  477. package/dist/vue/useVoiceReconnectProfileEvidence.d.ts +9 -0
  478. package/dist/vue/useVoiceRoutingStatus.d.ts +2 -2
  479. package/dist/vue/useVoiceSessionObservability.d.ts +9 -0
  480. package/dist/vue/useVoiceSessionSnapshot.d.ts +10 -0
  481. package/dist/vue/useVoiceStream.d.ts +10 -6
  482. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  483. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  484. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  485. package/dist/vue/useVoiceWorkflowStatus.d.ts +3 -3
  486. package/package.json +247 -256
  487. package/dist/angular/voice-app-kit-status.service.d.ts +0 -12
  488. package/dist/angular/voice-ops-status.component.d.ts +0 -15
  489. package/dist/appKit.d.ts +0 -94
  490. package/dist/client/appKitStatus.d.ts +0 -19
  491. package/dist/react/useVoiceAppKitStatus.d.ts +0 -8
  492. package/dist/svelte/createVoiceAppKitStatus.d.ts +0 -8
  493. package/dist/turnProfiles.d.ts +0 -6
  494. package/dist/types.d.ts +0 -968
  495. package/dist/vue/useVoiceAppKitStatus.d.ts +0 -9
  496. package/fixtures/README.md +0 -57
  497. package/fixtures/manifest.json +0 -199
  498. package/fixtures/pcm/dialogue-three-clean.pcm +0 -0
  499. package/fixtures/pcm/dialogue-three-mixed.pcm +0 -0
  500. package/fixtures/pcm/dialogue-two-clean.pcm +0 -0
  501. package/fixtures/pcm/dialogue-two-noisy.pcm +0 -0
  502. package/fixtures/pcm/multiturn-three-mixed.pcm +0 -0
  503. package/fixtures/pcm/multiturn-two-clean.pcm +0 -0
  504. package/fixtures/pcm/quietly-alone-clean.pcm +0 -0
  505. package/fixtures/pcm/rainstorms-noisy.pcm +0 -0
  506. package/fixtures/pcm/stella-bulgaria-bulgarian20.pcm +0 -0
  507. package/fixtures/pcm/stella-ghana-english507.pcm +0 -0
  508. package/fixtures/pcm/stella-india-english37.pcm +0 -0
  509. package/fixtures/pcm/stella-jamaica-jamaican-creole-english1.pcm +0 -0
  510. package/fixtures/pcm/stella-liberia-liberian-pidgin-english2.pcm +0 -0
  511. package/fixtures/pcm/stella-pakistan-english519.pcm +0 -0
  512. package/fixtures/pcm/stella-sierra-leone-krio5.pcm +0 -0
  513. package/fixtures/pcm/stella-singapore-english655.pcm +0 -0
  514. package/fixtures/pcm/traveled-back-route-clean.pcm +0 -0
@@ -1,3 +1,6 @@
1
+ // src/internal/html.ts
2
+ var escapeHtml = (value) => String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
3
+
1
4
  // src/client/htmx.ts
2
5
  var DEFAULT_EVENT_NAME = "voice-refresh";
3
6
  var DEFAULT_QUERY_PARAM = "sessionId";
@@ -100,37 +103,89 @@ var createMicrophoneCapture = (options) => {
100
103
  let sourceNode = null;
101
104
  let processorNode = null;
102
105
  let mediaStream = null;
103
- const start = async () => {
104
- if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
105
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
106
- }
107
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
108
- if (!AudioContextCtor) {
109
- throw new Error("Browser microphone capture requires AudioContext support.");
110
- }
111
- mediaStream = await navigator.mediaDevices.getUserMedia({
112
- audio: {
113
- channelCount: options.channelCount ?? 1
106
+ let lifecycleGeneration = 0;
107
+ let startPromise = null;
108
+ const release = (resources) => {
109
+ resources.processorNode?.disconnect();
110
+ resources.sourceNode?.disconnect();
111
+ resources.mediaStream?.getTracks().forEach((track) => track.stop());
112
+ resources.audioContext?.close();
113
+ };
114
+ const start = () => {
115
+ if (mediaStream)
116
+ return Promise.resolve();
117
+ if (startPromise)
118
+ return startPromise;
119
+ const generation = ++lifecycleGeneration;
120
+ const operation = async () => {
121
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
122
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
123
+ }
124
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
125
+ if (!AudioContextCtor) {
126
+ throw new Error("Browser microphone capture requires AudioContext support.");
127
+ }
128
+ let pendingAudioContext = null;
129
+ let pendingMediaStream = null;
130
+ let pendingProcessorNode = null;
131
+ let pendingSourceNode = null;
132
+ try {
133
+ pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
134
+ audio: {
135
+ autoGainControl: true,
136
+ channelCount: options.channelCount ?? 1,
137
+ echoCancellation: true,
138
+ noiseSuppression: true
139
+ }
140
+ });
141
+ if (generation !== lifecycleGeneration) {
142
+ throw new Error("Microphone capture stopped during startup.");
143
+ }
144
+ pendingAudioContext = new AudioContextCtor;
145
+ if (pendingAudioContext.state === "suspended") {
146
+ await pendingAudioContext.resume();
147
+ }
148
+ if (generation !== lifecycleGeneration) {
149
+ throw new Error("Microphone capture stopped during startup.");
150
+ }
151
+ pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
152
+ pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
153
+ pendingProcessorNode.onaudioprocess = (event) => {
154
+ const channel = event.inputBuffer.getChannelData(0);
155
+ const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
156
+ const pcm = floatTo16BitPCM(downsampled);
157
+ options.onLevel?.(getPcmLevel(pcm));
158
+ options.onAudio(pcm);
159
+ };
160
+ pendingSourceNode.connect(pendingProcessorNode);
161
+ pendingProcessorNode.connect(pendingAudioContext.destination);
162
+ if (generation !== lifecycleGeneration) {
163
+ throw new Error("Microphone capture stopped during startup.");
164
+ }
165
+ audioContext = pendingAudioContext;
166
+ mediaStream = pendingMediaStream;
167
+ processorNode = pendingProcessorNode;
168
+ sourceNode = pendingSourceNode;
169
+ } catch (error) {
170
+ release({
171
+ audioContext: pendingAudioContext,
172
+ mediaStream: pendingMediaStream,
173
+ processorNode: pendingProcessorNode,
174
+ sourceNode: pendingSourceNode
175
+ });
176
+ throw error;
114
177
  }
115
- });
116
- audioContext = new AudioContextCtor;
117
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
118
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
119
- processorNode.onaudioprocess = (event) => {
120
- const channel = event.inputBuffer.getChannelData(0);
121
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
122
- const pcm = floatTo16BitPCM(downsampled);
123
- options.onLevel?.(getPcmLevel(pcm));
124
- options.onAudio(pcm);
125
178
  };
126
- sourceNode.connect(processorNode);
127
- processorNode.connect(audioContext.destination);
179
+ const pending = operation().finally(() => {
180
+ if (startPromise === pending)
181
+ startPromise = null;
182
+ });
183
+ startPromise = pending;
184
+ return pending;
128
185
  };
129
186
  const stop = () => {
130
- processorNode?.disconnect();
131
- sourceNode?.disconnect();
132
- mediaStream?.getTracks().forEach((track) => track.stop());
133
- audioContext?.close();
187
+ lifecycleGeneration += 1;
188
+ release({ audioContext, mediaStream, processorNode, sourceNode });
134
189
  options.onLevel?.(0);
135
190
  audioContext = null;
136
191
  mediaStream = null;
@@ -181,13 +236,25 @@ var serverMessageToAction = (message) => {
181
236
  case "assistant":
182
237
  return {
183
238
  text: message.text,
239
+ turnId: message.turnId,
184
240
  type: "assistant"
185
241
  };
242
+ case "assistant_delta":
243
+ return {
244
+ delta: message.delta,
245
+ turnId: message.turnId,
246
+ type: "assistant_delta"
247
+ };
186
248
  case "complete":
187
249
  return {
188
250
  sessionId: message.sessionId,
189
251
  type: "complete"
190
252
  };
253
+ case "connection":
254
+ return {
255
+ reconnect: message.reconnect,
256
+ type: "connection"
257
+ };
191
258
  case "call_lifecycle":
192
259
  return {
193
260
  event: message.event,
@@ -197,8 +264,11 @@ var serverMessageToAction = (message) => {
197
264
  case "error":
198
265
  return {
199
266
  message: normalizeErrorMessage(message.message),
267
+ recoverable: message.recoverable,
200
268
  type: "error"
201
269
  };
270
+ case "call_control_ack":
271
+ return null;
202
272
  case "final":
203
273
  return {
204
274
  transcript: message.transcript,
@@ -209,9 +279,22 @@ var serverMessageToAction = (message) => {
209
279
  transcript: message.transcript,
210
280
  type: "partial"
211
281
  };
282
+ case "replay":
283
+ return {
284
+ assistantTexts: message.assistantTexts,
285
+ call: message.call,
286
+ partial: message.partial,
287
+ scenarioId: message.scenarioId,
288
+ sessionId: message.sessionId,
289
+ sessionMetadata: message.sessionMetadata,
290
+ status: message.status,
291
+ turns: message.turns,
292
+ type: "replay"
293
+ };
212
294
  case "session":
213
295
  return {
214
296
  sessionId: message.sessionId,
297
+ sessionMetadata: message.sessionMetadata,
215
298
  scenarioId: message.scenarioId,
216
299
  status: message.status,
217
300
  type: "session"
@@ -226,30 +309,276 @@ var serverMessageToAction = (message) => {
226
309
  }
227
310
  };
228
311
 
312
+ // node_modules/@absolutejs/media/dist/index.js
313
+ var TAU = Math.PI * 2;
314
+ var pushIssue = (issues, severity, code, message) => {
315
+ issues.push({ code, message, severity });
316
+ };
317
+ var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
318
+ var max = (values) => values.length === 0 ? undefined : Math.max(...values);
319
+ var numericStat = (stat, key) => {
320
+ const value = stat[key];
321
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
322
+ };
323
+ var booleanStat = (stat, key) => {
324
+ const value = stat[key];
325
+ return typeof value === "boolean" ? value : undefined;
326
+ };
327
+ var stringStat = (stat, key) => {
328
+ const value = stat[key];
329
+ return typeof value === "string" ? value : undefined;
330
+ };
331
+ var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
332
+ var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
333
+ var normalizeWebRTCStat = (stat) => {
334
+ const sample = {};
335
+ for (const [key, value] of Object.entries(stat)) {
336
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
337
+ sample[key] = value;
338
+ }
339
+ }
340
+ return sample;
341
+ };
342
+ var buildMediaWebRTCStatsReport = (input = {}) => {
343
+ const stats = input.stats ?? [];
344
+ const issues = [];
345
+ const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
346
+ const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
347
+ const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
348
+ const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
349
+ const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
350
+ const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
351
+ const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
352
+ const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
353
+ const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
354
+ const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
355
+ const packetLossDenominator = inboundPackets + packetsLost;
356
+ const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
357
+ const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
358
+ const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
359
+ const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
360
+ const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
361
+ const jitterBufferDelayMs = max(inbound.map((stat) => {
362
+ const delay = numericStat(stat, "jitterBufferDelay");
363
+ const emitted = numericStat(stat, "jitterBufferEmittedCount");
364
+ return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
365
+ }).filter((value) => value !== undefined));
366
+ const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
367
+ if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
368
+ pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
369
+ }
370
+ if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
371
+ pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
372
+ }
373
+ if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
374
+ pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
375
+ }
376
+ if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
377
+ pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
378
+ }
379
+ if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
380
+ pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
381
+ }
382
+ return {
383
+ activeCandidatePairs,
384
+ audioLevelAverage: average(audioLevels),
385
+ bytesReceived,
386
+ bytesSent,
387
+ checkedAt: Date.now(),
388
+ endedAudioTracks,
389
+ inboundPackets,
390
+ issues,
391
+ jitterBufferDelayMs,
392
+ jitterMs,
393
+ liveAudioTracks,
394
+ outboundPackets,
395
+ packetLossRatio,
396
+ packetsLost,
397
+ roundTripTimeMs,
398
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
399
+ totalStats: stats.length
400
+ };
401
+ };
402
+ var collectMediaWebRTCStats = async (input) => {
403
+ const report = await input.peerConnection.getStats(input.selector ?? null);
404
+ return [...report.values()].map(normalizeWebRTCStat);
405
+ };
406
+ var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
407
+ const stats = input.stats ?? [];
408
+ const previousStats = input.previousStats ?? [];
409
+ const issues = [];
410
+ const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
411
+ const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
412
+ const streams = audioRtp.map((stat) => {
413
+ const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
414
+ const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
415
+ const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
416
+ const previous = previousByKey.get(statKey(stat));
417
+ const currentPackets = numericStat(stat, packetsKey);
418
+ const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
419
+ const currentBytes = numericStat(stat, bytesKey);
420
+ const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
421
+ const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
422
+ return {
423
+ bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
424
+ currentPackets,
425
+ direction,
426
+ id: statKey(stat),
427
+ packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
428
+ previousPackets,
429
+ timeDeltaMs
430
+ };
431
+ });
432
+ const inbound = streams.filter((stream) => stream.direction === "inbound");
433
+ const outbound = streams.filter((stream) => stream.direction === "outbound");
434
+ const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
435
+ const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
436
+ const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
437
+ if (input.requireInboundAudio && inbound.length === 0) {
438
+ pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
439
+ }
440
+ if (input.requireOutboundAudio && outbound.length === 0) {
441
+ pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
442
+ }
443
+ if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
444
+ pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
445
+ }
446
+ if (stalledInboundStreams > 0) {
447
+ pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
448
+ }
449
+ if (stalledOutboundStreams > 0) {
450
+ pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
451
+ }
452
+ return {
453
+ checkedAt: Date.now(),
454
+ inboundAudioStreams: inbound.length,
455
+ issues,
456
+ maxObservedGapMs,
457
+ outboundAudioStreams: outbound.length,
458
+ stalledInboundStreams,
459
+ stalledOutboundStreams,
460
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
461
+ streams,
462
+ totalStats: stats.length
463
+ };
464
+ };
465
+
466
+ // src/client/browserMedia.ts
467
+ var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
468
+ var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
469
+ var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
470
+ var postBrowserMediaReport = async (payload, options) => {
471
+ const requestFetch = options.fetch ?? globalThis.fetch;
472
+ if (!requestFetch) {
473
+ return;
474
+ }
475
+ await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
476
+ body: JSON.stringify(payload),
477
+ headers: {
478
+ "Content-Type": "application/json"
479
+ },
480
+ keepalive: true,
481
+ method: "POST"
482
+ });
483
+ };
484
+ var createVoiceBrowserMediaReporter = (options) => {
485
+ let interval = null;
486
+ let previousStats = [];
487
+ const reportOnce = async () => {
488
+ const peerConnection = await resolvePeerConnection(options);
489
+ if (!peerConnection) {
490
+ return;
491
+ }
492
+ const stats = await collectMediaWebRTCStats({ peerConnection });
493
+ const report = buildMediaWebRTCStatsReport({
494
+ ...options,
495
+ stats
496
+ });
497
+ const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
498
+ ...options.continuity,
499
+ previousStats,
500
+ stats
501
+ });
502
+ const payload = {
503
+ at: Date.now(),
504
+ continuity,
505
+ report,
506
+ scenarioId: options.getScenarioId?.() ?? null,
507
+ sessionId: options.getSessionId?.() ?? null
508
+ };
509
+ previousStats = stats;
510
+ options.onReport?.(payload);
511
+ await postBrowserMediaReport(payload, options);
512
+ return payload;
513
+ };
514
+ const run = () => {
515
+ reportOnce().catch((error) => {
516
+ options.onError?.(error);
517
+ });
518
+ };
519
+ const stop = () => {
520
+ if (interval) {
521
+ clearInterval(interval);
522
+ interval = null;
523
+ }
524
+ };
525
+ return {
526
+ close: stop,
527
+ reportOnce,
528
+ stop,
529
+ start: () => {
530
+ if (interval) {
531
+ return;
532
+ }
533
+ run();
534
+ interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
535
+ }
536
+ };
537
+ };
538
+
229
539
  // src/client/connection.ts
230
540
  var WS_OPEN = 1;
231
541
  var WS_CLOSED = 3;
232
542
  var WS_NORMAL_CLOSURE = 1000;
233
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
543
+ var WS_UNAUTHORIZED = 4401;
544
+ var WS_FORBIDDEN = 4403;
545
+ var WS_INITIALIZATION_FAILED = 4500;
546
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
234
547
  var DEFAULT_PING_INTERVAL = 30000;
235
- var RECONNECT_DELAY_MS = 500;
548
+ var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
549
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
550
+ var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
551
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
552
+ var RECONNECT_BASE_DELAY_MS = 500;
553
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
554
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
236
555
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
556
+
557
+ class VoiceReconnectRejectedError extends Error {
558
+ constructor(message = "Voice authorization could not be renewed.") {
559
+ super(message);
560
+ this.name = "VoiceReconnectRejectedError";
561
+ }
562
+ }
237
563
  var noop = () => {};
564
+ var noopAsync = () => Promise.resolve();
238
565
  var noopUnsubscribe = () => noop;
239
566
  var NOOP_CONNECTION = {
240
- callControl: noop,
567
+ callControl: noopAsync,
241
568
  close: noop,
569
+ disconnect: noop,
242
570
  endTurn: noop,
571
+ send: noop,
572
+ sendAudio: noop,
573
+ simulateDisconnect: noop,
574
+ subscribe: noopUnsubscribe,
243
575
  getReadyState: () => WS_CLOSED,
244
576
  getScenarioId: () => "",
245
577
  getSessionId: () => "",
246
- send: noop,
247
- sendAudio: noop,
248
- start: () => {},
249
- subscribe: noopUnsubscribe
578
+ start: () => {}
250
579
  };
251
580
  var createSessionId = () => crypto.randomUUID();
252
- var buildWsUrl = (path, sessionId, scenarioId) => {
581
+ var buildWsUrl = (path, sessionId, scenarioId, query) => {
253
582
  const { hostname, port, protocol } = window.location;
254
583
  const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
255
584
  const portSuffix = port ? `:${port}` : "";
@@ -258,6 +587,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
258
587
  if (scenarioId) {
259
588
  url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
260
589
  }
590
+ for (const [key, value] of Object.entries(query)) {
591
+ if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
592
+ url.searchParams.set(key, value);
593
+ }
594
+ }
261
595
  return url.toString();
262
596
  };
263
597
  var isVoiceServerMessage = (value) => {
@@ -268,11 +602,14 @@ var isVoiceServerMessage = (value) => {
268
602
  case "audio":
269
603
  case "assistant":
270
604
  case "call_lifecycle":
605
+ case "call_control_ack":
271
606
  case "complete":
607
+ case "connection":
272
608
  case "error":
273
609
  case "final":
274
610
  case "partial":
275
611
  case "pong":
612
+ case "replay":
276
613
  case "session":
277
614
  case "turn":
278
615
  return true;
@@ -296,19 +633,45 @@ var createVoiceConnection = (path, options = {}) => {
296
633
  return NOOP_CONNECTION;
297
634
  }
298
635
  const listeners = new Set;
636
+ const pendingCallControls = new Map;
299
637
  const shouldReconnect = options.reconnect !== false;
300
638
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
639
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
301
640
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
641
+ const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
642
+ let lifecycleGeneration = 0;
643
+ let disposed = false;
644
+ let reconnectPreparationAbort = null;
645
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
302
646
  const state = {
303
647
  isConnected: false,
304
648
  pendingMessages: [],
305
649
  scenarioId: options.scenarioId ?? null,
306
650
  pingInterval: null,
307
651
  reconnectAttempts: 0,
652
+ reconnectResetTimeout: null,
308
653
  reconnectTimeout: null,
309
654
  sessionId: options.sessionId ?? createSessionId(),
310
655
  ws: null
311
656
  };
657
+ const emitConnection = (reconnect) => {
658
+ listeners.forEach((listener) => listener(reconnect));
659
+ };
660
+ const emitTerminalFailure = (message) => {
661
+ listeners.forEach((listener) => listener({
662
+ message,
663
+ recoverable: false,
664
+ type: "error"
665
+ }));
666
+ emitConnection({
667
+ reconnect: {
668
+ attempts: state.reconnectAttempts,
669
+ maxAttempts: maxReconnectAttempts,
670
+ status: "exhausted"
671
+ },
672
+ type: "connection"
673
+ });
674
+ };
312
675
  const clearTimers = () => {
313
676
  if (state.pingInterval) {
314
677
  clearInterval(state.pingInterval);
@@ -318,6 +681,10 @@ var createVoiceConnection = (path, options = {}) => {
318
681
  clearTimeout(state.reconnectTimeout);
319
682
  state.reconnectTimeout = null;
320
683
  }
684
+ if (state.reconnectResetTimeout) {
685
+ clearTimeout(state.reconnectResetTimeout);
686
+ state.reconnectResetTimeout = null;
687
+ }
321
688
  };
322
689
  const flushPendingMessages = () => {
323
690
  if (state.ws?.readyState !== WS_OPEN) {
@@ -330,22 +697,111 @@ var createVoiceConnection = (path, options = {}) => {
330
697
  }
331
698
  }
332
699
  };
700
+ const rejectPendingCallControls = (message) => {
701
+ for (const pending of pendingCallControls.values()) {
702
+ clearTimeout(pending.timeout);
703
+ pending.reject(new Error(message));
704
+ }
705
+ pendingCallControls.clear();
706
+ };
333
707
  const scheduleReconnect = () => {
708
+ if (disposed)
709
+ return;
710
+ const scheduledGeneration = lifecycleGeneration;
334
711
  state.reconnectAttempts += 1;
335
- state.reconnectTimeout = setTimeout(() => {
712
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
713
+ const nextAttemptAt = Date.now() + delayMs;
714
+ emitConnection({
715
+ reconnect: {
716
+ attempts: state.reconnectAttempts,
717
+ lastDisconnectAt: Date.now(),
718
+ maxAttempts: maxReconnectAttempts,
719
+ nextAttemptAt,
720
+ status: "reconnecting"
721
+ },
722
+ type: "connection"
723
+ });
724
+ state.reconnectTimeout = setTimeout(async () => {
725
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
726
+ return;
336
727
  if (state.reconnectAttempts > maxReconnectAttempts) {
728
+ emitTerminalFailure("Voice connection could not be restored.");
729
+ return;
730
+ }
731
+ try {
732
+ if (options.prepareReconnect) {
733
+ const preparationAbort = new AbortController;
734
+ reconnectPreparationAbort = preparationAbort;
735
+ const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
736
+ try {
737
+ await Promise.race([
738
+ options.prepareReconnect({
739
+ attempt: state.reconnectAttempts,
740
+ path,
741
+ scenarioId: state.scenarioId,
742
+ sessionId: state.sessionId,
743
+ signal: preparationAbort.signal
744
+ }),
745
+ new Promise((_, reject) => {
746
+ preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
747
+ })
748
+ ]);
749
+ } finally {
750
+ clearTimeout(timeout);
751
+ if (reconnectPreparationAbort === preparationAbort) {
752
+ reconnectPreparationAbort = null;
753
+ }
754
+ }
755
+ }
756
+ } catch (error) {
757
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
758
+ return;
759
+ if (error instanceof VoiceReconnectRejectedError) {
760
+ emitTerminalFailure(error.message);
761
+ return;
762
+ }
763
+ if (state.reconnectAttempts < maxReconnectAttempts) {
764
+ scheduleReconnect();
765
+ } else {
766
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
767
+ }
337
768
  return;
338
769
  }
770
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
771
+ return;
339
772
  connect();
340
- }, RECONNECT_DELAY_MS);
773
+ }, delayMs);
341
774
  };
342
775
  const connect = () => {
343
- const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
776
+ if (disposed)
777
+ return;
778
+ const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
344
779
  ws.binaryType = "arraybuffer";
345
780
  ws.onopen = () => {
781
+ if (state.ws !== ws) {
782
+ ws.close(WS_NORMAL_CLOSURE);
783
+ return;
784
+ }
785
+ const wasReconnecting = state.reconnectAttempts > 0;
346
786
  state.isConnected = true;
347
- state.reconnectAttempts = 0;
348
787
  flushPendingMessages();
788
+ if (wasReconnecting) {
789
+ emitConnection({
790
+ reconnect: {
791
+ attempts: state.reconnectAttempts,
792
+ lastResumedAt: Date.now(),
793
+ maxAttempts: maxReconnectAttempts,
794
+ status: "resumed"
795
+ },
796
+ type: "connection"
797
+ });
798
+ state.reconnectResetTimeout = setTimeout(() => {
799
+ if (state.ws === ws && ws.readyState === WS_OPEN) {
800
+ state.reconnectAttempts = 0;
801
+ }
802
+ state.reconnectResetTimeout = null;
803
+ }, reconnectResetAfterMs);
804
+ }
349
805
  listeners.forEach((listener) => listener({
350
806
  scenarioId: state.scenarioId ?? undefined,
351
807
  sessionId: state.sessionId,
@@ -359,6 +815,8 @@ var createVoiceConnection = (path, options = {}) => {
359
815
  }, pingInterval);
360
816
  };
361
817
  ws.onmessage = (event) => {
818
+ if (state.ws !== ws)
819
+ return;
362
820
  const parsed = parseServerMessage(event);
363
821
  if (!parsed) {
364
822
  return;
@@ -367,29 +825,86 @@ var createVoiceConnection = (path, options = {}) => {
367
825
  state.sessionId = parsed.sessionId;
368
826
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
369
827
  }
828
+ if (parsed.type === "call_control_ack") {
829
+ const pending = pendingCallControls.get(parsed.requestId);
830
+ if (pending) {
831
+ clearTimeout(pending.timeout);
832
+ pendingCallControls.delete(parsed.requestId);
833
+ if (parsed.ok) {
834
+ pending.resolve();
835
+ } else {
836
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
837
+ }
838
+ }
839
+ }
370
840
  listeners.forEach((listener) => listener(parsed));
371
841
  };
372
842
  ws.onclose = (event) => {
843
+ if (state.ws !== ws)
844
+ return;
845
+ state.ws = null;
373
846
  state.isConnected = false;
374
847
  clearTimers();
375
- const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
848
+ rejectPendingCallControls("Voice connection interrupted");
849
+ const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && event.code !== WS_INITIALIZATION_FAILED && state.reconnectAttempts < maxReconnectAttempts;
850
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
851
+ const initializationFailure = event.code === WS_INITIALIZATION_FAILED;
852
+ if (authorizationFailure) {
853
+ listeners.forEach((listener) => listener({
854
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
855
+ recoverable: false,
856
+ type: "error"
857
+ }));
858
+ }
859
+ if (initializationFailure) {
860
+ listeners.forEach((listener) => listener({
861
+ message: "Voice provider initialization failed.",
862
+ recoverable: false,
863
+ type: "error"
864
+ }));
865
+ }
376
866
  if (reconnectable) {
377
867
  scheduleReconnect();
868
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
869
+ if (authorizationFailure) {
870
+ emitConnection({
871
+ reconnect: {
872
+ attempts: state.reconnectAttempts,
873
+ lastDisconnectAt: Date.now(),
874
+ maxAttempts: maxReconnectAttempts,
875
+ status: "exhausted"
876
+ },
877
+ type: "connection"
878
+ });
879
+ } else if (!initializationFailure) {
880
+ emitTerminalFailure("Voice connection could not be restored.");
881
+ }
378
882
  }
379
883
  };
380
884
  state.ws = ws;
381
885
  };
382
886
  const sendSerialized = (value) => {
887
+ if (disposed)
888
+ return;
383
889
  if (state.ws?.readyState === WS_OPEN) {
384
890
  state.ws.send(value);
385
891
  return;
386
892
  }
893
+ if (typeof value !== "string") {
894
+ return;
895
+ }
896
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
897
+ state.pendingMessages.shift();
898
+ }
387
899
  state.pendingMessages.push(value);
388
900
  };
389
901
  const send = (message) => {
390
902
  sendSerialized(JSON.stringify(message));
391
903
  };
392
904
  const start = (input = {}) => {
905
+ if (input.sessionId && input.sessionId !== state.sessionId) {
906
+ throw new Error("Voice session switching requires a new connection instance.");
907
+ }
393
908
  if (input.sessionId) {
394
909
  state.sessionId = input.sessionId;
395
910
  }
@@ -397,9 +912,9 @@ var createVoiceConnection = (path, options = {}) => {
397
912
  state.scenarioId = input.scenarioId;
398
913
  }
399
914
  send({
400
- type: "start",
915
+ scenarioId: state.scenarioId ?? undefined,
401
916
  sessionId: state.sessionId,
402
- scenarioId: state.scenarioId ?? undefined
917
+ type: "start"
403
918
  });
404
919
  };
405
920
  const sendAudio = (audio) => {
@@ -409,20 +924,63 @@ var createVoiceConnection = (path, options = {}) => {
409
924
  send({ type: "end_turn" });
410
925
  };
411
926
  const callControl = (message) => {
412
- send({
927
+ if (state.ws?.readyState !== WS_OPEN) {
928
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
929
+ }
930
+ const requestId = crypto.randomUUID();
931
+ const { promise, reject, resolve } = Promise.withResolvers();
932
+ const timeout = setTimeout(() => {
933
+ pendingCallControls.delete(requestId);
934
+ reject(new Error(`${message.action} acknowledgement timed out`));
935
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
936
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
937
+ state.ws.send(JSON.stringify({
413
938
  ...message,
939
+ requestId,
414
940
  type: "call_control"
415
- });
941
+ }));
942
+ return promise;
943
+ };
944
+ const close = (reason = "client-close") => {
945
+ disposed = true;
946
+ reconnectPreparationAbort?.abort();
947
+ reconnectPreparationAbort = null;
948
+ lifecycleGeneration += 1;
949
+ clearTimers();
950
+ rejectPendingCallControls("Voice connection closed");
951
+ state.pendingMessages.length = 0;
952
+ if (state.ws) {
953
+ const ws = state.ws;
954
+ state.ws = null;
955
+ if (ws.readyState === WS_OPEN) {
956
+ ws.send(JSON.stringify({ reason, type: "close" }));
957
+ }
958
+ ws.close(WS_NORMAL_CLOSURE);
959
+ }
960
+ state.isConnected = false;
961
+ listeners.clear();
416
962
  };
417
- const close = () => {
963
+ const disconnect = () => {
964
+ disposed = true;
965
+ reconnectPreparationAbort?.abort();
966
+ reconnectPreparationAbort = null;
967
+ lifecycleGeneration += 1;
418
968
  clearTimers();
969
+ rejectPendingCallControls("Voice connection disconnected");
970
+ state.pendingMessages.length = 0;
419
971
  if (state.ws) {
420
- state.ws.close(WS_NORMAL_CLOSURE);
972
+ const ws = state.ws;
421
973
  state.ws = null;
974
+ ws.close(WS_NORMAL_CLOSURE);
422
975
  }
423
976
  state.isConnected = false;
424
977
  listeners.clear();
425
978
  };
979
+ const simulateDisconnect = () => {
980
+ if (state.ws?.readyState === WS_OPEN) {
981
+ state.ws.close(4000, "absolutejs-voice-reconnect-proof");
982
+ }
983
+ };
426
984
  const subscribe = (callback) => {
427
985
  listeners.add(callback);
428
986
  return () => {
@@ -433,32 +991,64 @@ var createVoiceConnection = (path, options = {}) => {
433
991
  return {
434
992
  callControl,
435
993
  close,
994
+ disconnect,
436
995
  endTurn,
437
- getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
438
- getScenarioId: () => state.scenarioId ?? "",
439
- getSessionId: () => state.sessionId,
440
996
  send,
441
997
  sendAudio,
998
+ simulateDisconnect,
442
999
  start,
443
- subscribe
1000
+ subscribe,
1001
+ getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
1002
+ getScenarioId: () => state.scenarioId ?? "",
1003
+ getSessionId: () => state.sessionId
444
1004
  };
445
1005
  };
446
1006
 
447
1007
  // src/client/store.ts
1008
+ var createInitialReconnectState = () => ({
1009
+ attempts: 0,
1010
+ maxAttempts: 0,
1011
+ status: "idle"
1012
+ });
1013
+ var appendSegmentText = (accumulated, next) => {
1014
+ const nextText = next.trim().replace(/\s+/g, " ");
1015
+ if (!nextText)
1016
+ return accumulated;
1017
+ if (!accumulated)
1018
+ return nextText;
1019
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
1020
+ return accumulated;
1021
+ }
1022
+ if (nextText.includes(accumulated))
1023
+ return nextText;
1024
+ return `${accumulated} ${nextText}`;
1025
+ };
1026
+ var joinPartial = (finalized, interim) => {
1027
+ const interimText = interim.trim().replace(/\s+/g, " ");
1028
+ if (!finalized)
1029
+ return interimText;
1030
+ if (!interimText || finalized.endsWith(interimText))
1031
+ return finalized;
1032
+ return `${finalized} ${interimText}`;
1033
+ };
448
1034
  var createInitialState = () => ({
449
1035
  assistantAudio: [],
1036
+ assistantStreamingText: "",
450
1037
  assistantTexts: [],
451
1038
  call: null,
452
1039
  error: null,
453
1040
  isConnected: false,
454
- scenarioId: null,
455
1041
  partial: "",
1042
+ reconnect: createInitialReconnectState(),
1043
+ scenarioId: null,
456
1044
  sessionId: null,
1045
+ sessionMetadata: null,
457
1046
  status: "idle",
458
1047
  turns: []
459
1048
  });
460
1049
  var createVoiceStreamStore = () => {
461
1050
  let state = createInitialState();
1051
+ let turnFinalText = "";
462
1052
  const subscribers = new Set;
463
1053
  const notify = () => {
464
1054
  subscribers.forEach((subscriber) => subscriber());
@@ -482,9 +1072,16 @@ var createVoiceStreamStore = () => {
482
1072
  case "assistant":
483
1073
  state = {
484
1074
  ...state,
1075
+ assistantStreamingText: "",
485
1076
  assistantTexts: [...state.assistantTexts, action.text]
486
1077
  };
487
1078
  break;
1079
+ case "assistant_delta":
1080
+ state = {
1081
+ ...state,
1082
+ assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
1083
+ };
1084
+ break;
488
1085
  case "complete":
489
1086
  state = {
490
1087
  ...state,
@@ -509,7 +1106,19 @@ var createVoiceStreamStore = () => {
509
1106
  case "connected":
510
1107
  state = {
511
1108
  ...state,
512
- isConnected: true
1109
+ isConnected: true,
1110
+ reconnect: state.reconnect.status === "reconnecting" ? {
1111
+ ...state.reconnect,
1112
+ lastResumedAt: Date.now(),
1113
+ nextAttemptAt: undefined,
1114
+ status: "resumed"
1115
+ } : state.reconnect
1116
+ };
1117
+ break;
1118
+ case "connection":
1119
+ state = {
1120
+ ...state,
1121
+ reconnect: action.reconnect
513
1122
  };
514
1123
  break;
515
1124
  case "disconnected":
@@ -521,20 +1130,44 @@ var createVoiceStreamStore = () => {
521
1130
  case "error":
522
1131
  state = {
523
1132
  ...state,
524
- error: action.message
1133
+ error: action.message,
1134
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
525
1135
  };
526
1136
  break;
527
1137
  case "final":
1138
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
528
1139
  state = {
529
1140
  ...state,
530
- partial: action.transcript.text,
531
- turns: state.turns.map((turn) => turn)
1141
+ partial: turnFinalText
532
1142
  };
533
1143
  break;
534
1144
  case "partial":
535
1145
  state = {
536
1146
  ...state,
537
- partial: action.transcript.text
1147
+ partial: joinPartial(turnFinalText, action.transcript.text)
1148
+ };
1149
+ break;
1150
+ case "replay":
1151
+ turnFinalText = action.partial;
1152
+ state = {
1153
+ ...state,
1154
+ assistantStreamingText: "",
1155
+ assistantTexts: [...action.assistantTexts],
1156
+ call: action.call ?? null,
1157
+ error: null,
1158
+ isConnected: action.status === "active",
1159
+ partial: action.partial,
1160
+ reconnect: state.reconnect.status === "reconnecting" ? {
1161
+ ...state.reconnect,
1162
+ lastResumedAt: Date.now(),
1163
+ nextAttemptAt: undefined,
1164
+ status: "resumed"
1165
+ } : state.reconnect,
1166
+ scenarioId: action.scenarioId ?? state.scenarioId,
1167
+ sessionId: action.sessionId,
1168
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
1169
+ status: action.status,
1170
+ turns: [...action.turns]
538
1171
  };
539
1172
  break;
540
1173
  case "session":
@@ -544,10 +1177,12 @@ var createVoiceStreamStore = () => {
544
1177
  scenarioId: action.scenarioId ?? state.scenarioId,
545
1178
  isConnected: action.status === "active",
546
1179
  sessionId: action.sessionId,
1180
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
547
1181
  status: action.status
548
1182
  };
549
1183
  break;
550
1184
  case "turn":
1185
+ turnFinalText = "";
551
1186
  state = {
552
1187
  ...state,
553
1188
  partial: "",
@@ -574,30 +1209,86 @@ var createVoiceStreamStore = () => {
574
1209
  var createVoiceStream = (path, options = {}) => {
575
1210
  const connection = createVoiceConnection(path, options);
576
1211
  const store = createVoiceStreamStore();
1212
+ const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
1213
+ ...options.browserMedia,
1214
+ getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
1215
+ getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
1216
+ }) : null;
577
1217
  const subscribers = new Set;
1218
+ let disposed = false;
578
1219
  const start = (input) => Promise.resolve().then(() => {
1220
+ if (disposed)
1221
+ return;
579
1222
  if (!input?.sessionId && !input?.scenarioId) {
580
1223
  return;
581
1224
  }
582
1225
  connection.start(input);
1226
+ browserMediaReporter?.start();
583
1227
  });
584
1228
  const notify = () => {
585
1229
  subscribers.forEach((subscriber) => subscriber());
586
1230
  };
1231
+ const reportReconnect = () => {
1232
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
1233
+ return;
1234
+ }
1235
+ const snapshot = store.getSnapshot();
1236
+ const body = JSON.stringify({
1237
+ at: Date.now(),
1238
+ reconnect: snapshot.reconnect,
1239
+ scenarioId: snapshot.scenarioId,
1240
+ sessionId: connection.getSessionId(),
1241
+ turnIds: snapshot.turns.map((turn) => turn.id)
1242
+ });
1243
+ fetch(options.reconnectReportPath, {
1244
+ body,
1245
+ headers: {
1246
+ "Content-Type": "application/json"
1247
+ },
1248
+ keepalive: true,
1249
+ method: "POST"
1250
+ }).catch(() => {});
1251
+ };
587
1252
  const unsubscribeConnection = connection.subscribe((message) => {
588
1253
  const action = serverMessageToAction(message);
589
1254
  if (action) {
590
1255
  store.dispatch(action);
1256
+ if (message.type === "connection") {
1257
+ reportReconnect();
1258
+ }
591
1259
  notify();
592
1260
  }
593
1261
  });
594
1262
  return {
1263
+ start,
1264
+ get assistantAudio() {
1265
+ return store.getSnapshot().assistantAudio;
1266
+ },
1267
+ get assistantTexts() {
1268
+ return store.getSnapshot().assistantTexts;
1269
+ },
1270
+ get assistantStreamingText() {
1271
+ return store.getSnapshot().assistantStreamingText;
1272
+ },
1273
+ get call() {
1274
+ return store.getSnapshot().call;
1275
+ },
595
1276
  callControl(message) {
596
- connection.callControl(message);
1277
+ return connection.callControl(message);
1278
+ },
1279
+ close(reason) {
1280
+ disposed = true;
1281
+ unsubscribeConnection();
1282
+ browserMediaReporter?.close();
1283
+ connection.close(reason);
1284
+ store.dispatch({ type: "disconnected" });
1285
+ notify();
597
1286
  },
598
- close() {
1287
+ disconnect() {
1288
+ disposed = true;
599
1289
  unsubscribeConnection();
600
- connection.close();
1290
+ browserMediaReporter?.close();
1291
+ connection.disconnect();
601
1292
  store.dispatch({ type: "disconnected" });
602
1293
  notify();
603
1294
  },
@@ -616,44 +1307,43 @@ var createVoiceStream = (path, options = {}) => {
616
1307
  get isConnected() {
617
1308
  return store.getSnapshot().isConnected;
618
1309
  },
619
- get scenarioId() {
620
- return store.getSnapshot().scenarioId;
621
- },
622
- start,
623
1310
  get partial() {
624
1311
  return store.getSnapshot().partial;
625
1312
  },
626
- get sessionId() {
627
- return connection.getSessionId();
1313
+ get reconnect() {
1314
+ return store.getSnapshot().reconnect;
628
1315
  },
629
- get status() {
630
- return store.getSnapshot().status;
1316
+ get scenarioId() {
1317
+ return store.getSnapshot().scenarioId;
631
1318
  },
632
- get turns() {
633
- return store.getSnapshot().turns;
1319
+ sendAudio(audio) {
1320
+ connection.sendAudio(audio);
634
1321
  },
635
- get assistantTexts() {
636
- return store.getSnapshot().assistantTexts;
1322
+ get sessionId() {
1323
+ return connection.getSessionId();
637
1324
  },
638
- get assistantAudio() {
639
- return store.getSnapshot().assistantAudio;
1325
+ get sessionMetadata() {
1326
+ return store.getSnapshot().sessionMetadata;
640
1327
  },
641
- get call() {
642
- return store.getSnapshot().call;
1328
+ simulateDisconnect() {
1329
+ connection.simulateDisconnect();
643
1330
  },
644
- sendAudio(audio) {
645
- connection.sendAudio(audio);
1331
+ get status() {
1332
+ return store.getSnapshot().status;
646
1333
  },
647
1334
  subscribe(subscriber) {
648
1335
  subscribers.add(subscriber);
649
1336
  return () => {
650
1337
  subscribers.delete(subscriber);
651
1338
  };
1339
+ },
1340
+ get turns() {
1341
+ return store.getSnapshot().turns;
652
1342
  }
653
1343
  };
654
1344
  };
655
1345
 
656
- // src/audioConditioning.ts
1346
+ // src/core/audioConditioning.ts
657
1347
  var DEFAULT_TARGET_LEVEL = 0.08;
658
1348
  var DEFAULT_MAX_GAIN = 3;
659
1349
  var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
@@ -671,34 +1361,37 @@ var resolveAudioConditioningConfig = (config) => {
671
1361
  };
672
1362
  };
673
1363
 
674
- // src/turnProfiles.ts
1364
+ // src/core/turnProfiles.ts
675
1365
  var TURN_PROFILE_DEFAULTS = {
676
1366
  balanced: {
677
1367
  qualityProfile: "general",
1368
+ minSilenceMs: 400,
678
1369
  silenceMs: 1400,
679
1370
  speechThreshold: 0.012,
680
1371
  transcriptStabilityMs: 1000
681
1372
  },
682
1373
  fast: {
683
1374
  qualityProfile: "general",
1375
+ minSilenceMs: 300,
684
1376
  silenceMs: 700,
685
1377
  speechThreshold: 0.015,
686
1378
  transcriptStabilityMs: 450
687
1379
  },
688
1380
  "long-form": {
689
1381
  qualityProfile: "general",
1382
+ minSilenceMs: 600,
690
1383
  silenceMs: 2200,
691
1384
  speechThreshold: 0.01,
692
1385
  transcriptStabilityMs: 1500
693
1386
  }
694
1387
  };
695
1388
  var QUALITY_PROFILE_DEFAULTS = {
696
- general: {},
697
1389
  "accent-heavy": {
698
1390
  silenceMs: 1200,
699
1391
  speechThreshold: 0.01,
700
1392
  transcriptStabilityMs: 1200
701
1393
  },
1394
+ general: {},
702
1395
  "noisy-room": {
703
1396
  silenceMs: 2000,
704
1397
  speechThreshold: 0.02,
@@ -717,16 +1410,18 @@ var resolveTurnDetectionConfig = (config) => {
717
1410
  const qualityProfile = config?.qualityProfile ?? DEFAULT_QUALITY_PROFILE;
718
1411
  const preset = TURN_PROFILE_DEFAULTS[profile];
719
1412
  const quality = QUALITY_PROFILE_DEFAULTS[qualityProfile];
1413
+ const silenceMs = config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs;
720
1414
  return {
721
1415
  profile,
722
1416
  qualityProfile,
723
- silenceMs: config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs,
1417
+ minSilenceMs: Math.min(silenceMs, config?.minSilenceMs ?? quality.minSilenceMs ?? preset.minSilenceMs),
1418
+ silenceMs,
724
1419
  speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
725
1420
  transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
726
1421
  };
727
1422
  };
728
1423
 
729
- // src/presets.ts
1424
+ // src/core/presets.ts
730
1425
  var PRESET_INPUTS = {
731
1426
  chat: {
732
1427
  audioConditioning: {
@@ -747,8 +1442,8 @@ var PRESET_INPUTS = {
747
1442
  },
748
1443
  sttLifecycle: "continuous",
749
1444
  turnDetection: {
750
- qualityProfile: "short-command",
751
- profile: "balanced"
1445
+ profile: "balanced",
1446
+ qualityProfile: "short-command"
752
1447
  }
753
1448
  },
754
1449
  default: {
@@ -763,8 +1458,8 @@ var PRESET_INPUTS = {
763
1458
  },
764
1459
  sttLifecycle: "continuous",
765
1460
  turnDetection: {
766
- qualityProfile: "general",
767
- profile: "fast"
1461
+ profile: "fast",
1462
+ qualityProfile: "general"
768
1463
  }
769
1464
  },
770
1465
  dictation: {
@@ -786,8 +1481,8 @@ var PRESET_INPUTS = {
786
1481
  },
787
1482
  sttLifecycle: "continuous",
788
1483
  turnDetection: {
789
- qualityProfile: "accent-heavy",
790
- profile: "long-form"
1484
+ profile: "long-form",
1485
+ qualityProfile: "accent-heavy"
791
1486
  }
792
1487
  },
793
1488
  "guided-intake": {
@@ -809,8 +1504,8 @@ var PRESET_INPUTS = {
809
1504
  },
810
1505
  sttLifecycle: "turn-scoped",
811
1506
  turnDetection: {
812
- qualityProfile: "accent-heavy",
813
- profile: "long-form"
1507
+ profile: "long-form",
1508
+ qualityProfile: "accent-heavy"
814
1509
  }
815
1510
  },
816
1511
  "noisy-room": {
@@ -832,8 +1527,8 @@ var PRESET_INPUTS = {
832
1527
  },
833
1528
  sttLifecycle: "continuous",
834
1529
  turnDetection: {
835
- qualityProfile: "noisy-room",
836
1530
  profile: "long-form",
1531
+ qualityProfile: "noisy-room",
837
1532
  silenceMs: 2100,
838
1533
  speechThreshold: 0.02,
839
1534
  transcriptStabilityMs: 1650
@@ -858,8 +1553,8 @@ var PRESET_INPUTS = {
858
1553
  },
859
1554
  sttLifecycle: "continuous",
860
1555
  turnDetection: {
861
- qualityProfile: "noisy-room",
862
1556
  profile: "long-form",
1557
+ qualityProfile: "noisy-room",
863
1558
  silenceMs: 660,
864
1559
  speechThreshold: 0.012,
865
1560
  transcriptStabilityMs: 300
@@ -884,8 +1579,8 @@ var PRESET_INPUTS = {
884
1579
  },
885
1580
  sttLifecycle: "continuous",
886
1581
  turnDetection: {
887
- qualityProfile: "noisy-room",
888
1582
  profile: "long-form",
1583
+ qualityProfile: "noisy-room",
889
1584
  silenceMs: 620,
890
1585
  speechThreshold: 0.012,
891
1586
  transcriptStabilityMs: 280
@@ -910,8 +1605,8 @@ var PRESET_INPUTS = {
910
1605
  },
911
1606
  sttLifecycle: "continuous",
912
1607
  turnDetection: {
913
- qualityProfile: "noisy-room",
914
- profile: "long-form"
1608
+ profile: "long-form",
1609
+ qualityProfile: "noisy-room"
915
1610
  }
916
1611
  }
917
1612
  };
@@ -935,14 +1630,17 @@ var resolveVoiceRuntimePreset = (name = "default") => {
935
1630
  // src/client/controller.ts
936
1631
  var createInitialState2 = (stream) => ({
937
1632
  assistantAudio: [...stream.assistantAudio],
1633
+ assistantStreamingText: stream.assistantStreamingText,
938
1634
  assistantTexts: [...stream.assistantTexts],
939
1635
  call: stream.call,
940
1636
  error: stream.error,
941
1637
  isConnected: stream.isConnected,
942
1638
  isRecording: false,
943
1639
  partial: stream.partial,
1640
+ reconnect: stream.reconnect,
944
1641
  recordingError: null,
945
1642
  sessionId: stream.sessionId,
1643
+ sessionMetadata: stream.sessionMetadata,
946
1644
  scenarioId: stream.scenarioId,
947
1645
  status: stream.status,
948
1646
  turns: [...stream.turns]
@@ -954,6 +1652,9 @@ var createVoiceController = (path, options = {}) => {
954
1652
  ...options.connection
955
1653
  });
956
1654
  let capture = null;
1655
+ let captureGeneration = 0;
1656
+ let disposed = false;
1657
+ let startRecordingPromise = null;
957
1658
  let state = createInitialState2(stream);
958
1659
  const subscribers = new Set;
959
1660
  const notify = () => {
@@ -965,12 +1666,15 @@ var createVoiceController = (path, options = {}) => {
965
1666
  state = {
966
1667
  ...state,
967
1668
  assistantAudio: [...stream.assistantAudio],
1669
+ assistantStreamingText: stream.assistantStreamingText,
968
1670
  assistantTexts: [...stream.assistantTexts],
969
1671
  call: stream.call,
970
1672
  error: stream.error,
971
1673
  isConnected: stream.isConnected,
972
1674
  partial: stream.partial,
1675
+ reconnect: stream.reconnect,
973
1676
  sessionId: stream.sessionId,
1677
+ sessionMetadata: stream.sessionMetadata,
974
1678
  scenarioId: stream.scenarioId,
975
1679
  status: stream.status,
976
1680
  turns: [...stream.turns]
@@ -994,12 +1698,20 @@ var createVoiceController = (path, options = {}) => {
994
1698
  capture = createMicrophoneCapture({
995
1699
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
1700
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
998
- sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
1701
+ onAudio: (audio) => {
1702
+ if (options.capture?.onAudio) {
1703
+ options.capture.onAudio(audio, stream.sendAudio);
1704
+ return;
1705
+ }
1706
+ stream.sendAudio(audio);
1707
+ },
1708
+ sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
1709
+ ...options.capture?.stream ? { stream: options.capture.stream } : {}
999
1710
  });
1000
1711
  return capture;
1001
1712
  };
1002
1713
  const stopRecording = () => {
1714
+ captureGeneration += 1;
1003
1715
  capture?.stop();
1004
1716
  capture = null;
1005
1717
  state = {
@@ -1009,43 +1721,88 @@ var createVoiceController = (path, options = {}) => {
1009
1721
  notify();
1010
1722
  };
1011
1723
  const startRecording = async () => {
1724
+ if (disposed) {
1725
+ throw new Error("Voice controller is closed.");
1726
+ }
1012
1727
  if (state.isRecording) {
1013
1728
  return;
1014
1729
  }
1015
- try {
1016
- state = {
1017
- ...state,
1018
- recordingError: null
1019
- };
1020
- notify();
1021
- await ensureCapture().start();
1022
- state = {
1023
- ...state,
1024
- isRecording: true
1025
- };
1026
- notify();
1027
- } catch (error) {
1028
- capture = null;
1029
- state = {
1030
- ...state,
1031
- isRecording: false,
1032
- recordingError: error instanceof Error ? error.message : String(error)
1033
- };
1034
- notify();
1035
- throw error;
1036
- }
1730
+ if (startRecordingPromise)
1731
+ return startRecordingPromise;
1732
+ const generation = ++captureGeneration;
1733
+ const activeCapture = ensureCapture();
1734
+ const operation = (async () => {
1735
+ try {
1736
+ state = {
1737
+ ...state,
1738
+ recordingError: null
1739
+ };
1740
+ notify();
1741
+ await activeCapture.start();
1742
+ if (disposed || generation !== captureGeneration || capture !== activeCapture) {
1743
+ activeCapture.stop();
1744
+ return;
1745
+ }
1746
+ state = {
1747
+ ...state,
1748
+ isRecording: true
1749
+ };
1750
+ notify();
1751
+ } catch (error) {
1752
+ activeCapture.stop();
1753
+ if (generation !== captureGeneration || capture !== activeCapture) {
1754
+ throw error;
1755
+ }
1756
+ capture = null;
1757
+ state = {
1758
+ ...state,
1759
+ isRecording: false,
1760
+ recordingError: error instanceof Error ? error.message : String(error)
1761
+ };
1762
+ notify();
1763
+ throw error;
1764
+ }
1765
+ })();
1766
+ const pending = operation.finally(() => {
1767
+ if (startRecordingPromise === pending)
1768
+ startRecordingPromise = null;
1769
+ });
1770
+ startRecordingPromise = pending;
1771
+ return startRecordingPromise;
1772
+ };
1773
+ const close = (reason) => {
1774
+ disposed = true;
1775
+ unsubscribeStream();
1776
+ stopRecording();
1777
+ stream.close(reason);
1037
1778
  };
1038
- const close = () => {
1779
+ const disconnect = () => {
1780
+ disposed = true;
1039
1781
  unsubscribeStream();
1040
1782
  stopRecording();
1041
- stream.close();
1783
+ stream.disconnect();
1042
1784
  };
1043
1785
  return {
1044
- bindHTMX(bindingOptions) {
1045
- return bindVoiceHTMX(stream, bindingOptions);
1786
+ close,
1787
+ disconnect,
1788
+ startRecording,
1789
+ stopRecording,
1790
+ get assistantAudio() {
1791
+ return state.assistantAudio;
1792
+ },
1793
+ get assistantTexts() {
1794
+ return state.assistantTexts;
1795
+ },
1796
+ get assistantStreamingText() {
1797
+ return state.assistantStreamingText;
1798
+ },
1799
+ bindHTMX(bindingOptions) {
1800
+ return bindVoiceHTMX(stream, bindingOptions);
1801
+ },
1802
+ get call() {
1803
+ return state.call;
1046
1804
  },
1047
1805
  callControl: (message) => stream.callControl(message),
1048
- close,
1049
1806
  endTurn: () => stream.endTurn(),
1050
1807
  get error() {
1051
1808
  return state.error;
@@ -1061,21 +1818,26 @@ var createVoiceController = (path, options = {}) => {
1061
1818
  get partial() {
1062
1819
  return state.partial;
1063
1820
  },
1821
+ get reconnect() {
1822
+ return state.reconnect;
1823
+ },
1064
1824
  get recordingError() {
1065
1825
  return state.recordingError;
1066
1826
  },
1827
+ get scenarioId() {
1828
+ return state.scenarioId;
1829
+ },
1067
1830
  sendAudio: (audio) => stream.sendAudio(audio),
1068
1831
  get sessionId() {
1069
1832
  return state.sessionId;
1070
1833
  },
1071
- get scenarioId() {
1072
- return state.scenarioId;
1834
+ get sessionMetadata() {
1835
+ return state.sessionMetadata;
1073
1836
  },
1074
- startRecording,
1837
+ simulateDisconnect: () => stream.simulateDisconnect(),
1075
1838
  get status() {
1076
1839
  return state.status;
1077
1840
  },
1078
- stopRecording,
1079
1841
  subscribe: (subscriber) => {
1080
1842
  subscribers.add(subscriber);
1081
1843
  return () => {
@@ -1091,15 +1853,778 @@ var createVoiceController = (path, options = {}) => {
1091
1853
  },
1092
1854
  get turns() {
1093
1855
  return state.turns;
1856
+ }
1857
+ };
1858
+ };
1859
+
1860
+ // src/client/timeStretch.ts
1861
+ var HOP_MS = 10;
1862
+ var SEEK_MS = 5;
1863
+ var ENERGY_EPSILON = 0.000001;
1864
+ var HALF = 0.5;
1865
+ var MS_PER_SECOND = 1000;
1866
+ var makeHann = (length) => {
1867
+ const weights = new Float32Array(length);
1868
+ for (let index = 0;index < length; index += 1) {
1869
+ weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
1870
+ }
1871
+ return weights;
1872
+ };
1873
+ var correlationScore = (base, start, ref, length) => {
1874
+ let dot = 0;
1875
+ let energy = 0;
1876
+ for (let index = 0;index < length; index += 1) {
1877
+ const sample = base[start + index] ?? 0;
1878
+ dot += sample * (ref[index] ?? 0);
1879
+ energy += sample * sample;
1880
+ }
1881
+ return dot / Math.sqrt(energy + ENERGY_EPSILON);
1882
+ };
1883
+ var overlapAddGrain = (src, off, tail, weights, hop) => {
1884
+ const out = new Float32Array(hop);
1885
+ const nextTail = new Float32Array(hop);
1886
+ for (let index = 0;index < hop; index += 1) {
1887
+ out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
1888
+ nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
1889
+ }
1890
+ return { nextTail, out };
1891
+ };
1892
+ var createTimeStretcher = () => {
1893
+ let sampleRate = 0;
1894
+ let channelCount = 0;
1895
+ let hop = 0;
1896
+ let frameLen = 0;
1897
+ let seek = 0;
1898
+ let weights = new Float32Array(0);
1899
+ let buffers = [];
1900
+ let inputStart = 0;
1901
+ let analysisPos = 0;
1902
+ let olaTail = [];
1903
+ let naturalRef = null;
1904
+ const init = (rate, channels) => {
1905
+ sampleRate = rate;
1906
+ channelCount = channels;
1907
+ hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
1908
+ frameLen = hop * 2;
1909
+ seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
1910
+ weights = makeHann(frameLen);
1911
+ buffers = Array.from({ length: channels }, () => new Float32Array(0));
1912
+ olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
1913
+ inputStart = 0;
1914
+ analysisPos = seek;
1915
+ naturalRef = null;
1916
+ };
1917
+ const reset = () => {
1918
+ buffers = buffers.map(() => new Float32Array(0));
1919
+ olaTail = olaTail.map(() => new Float32Array(hop));
1920
+ inputStart = 0;
1921
+ analysisPos = seek;
1922
+ naturalRef = null;
1923
+ };
1924
+ const append = (input) => {
1925
+ for (let channel = 0;channel < channelCount; channel += 1) {
1926
+ const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
1927
+ const existing = buffers[channel] ?? new Float32Array(0);
1928
+ const merged = new Float32Array(existing.length + incoming.length);
1929
+ merged.set(existing, 0);
1930
+ merged.set(incoming, existing.length);
1931
+ buffers[channel] = merged;
1932
+ }
1933
+ };
1934
+ const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
1935
+ const compact = () => {
1936
+ const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
1937
+ if (keepFrom <= inputStart)
1938
+ return;
1939
+ const drop = keepFrom - inputStart;
1940
+ for (let channel = 0;channel < channelCount; channel += 1) {
1941
+ buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
1942
+ }
1943
+ inputStart = keepFrom;
1944
+ };
1945
+ const bestOffset = (center) => {
1946
+ if (!naturalRef)
1947
+ return 0;
1948
+ const [base] = buffers;
1949
+ if (!base)
1950
+ return 0;
1951
+ let bestDelta = 0;
1952
+ let bestScore = -Infinity;
1953
+ for (let delta = -seek;delta <= seek; delta += 1) {
1954
+ const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
1955
+ if (score <= bestScore)
1956
+ continue;
1957
+ bestScore = score;
1958
+ bestDelta = delta;
1959
+ }
1960
+ return bestDelta;
1961
+ };
1962
+ const process = (input, speed, rate) => {
1963
+ const channels = Math.max(1, input.length);
1964
+ if (sampleRate !== rate || channelCount !== channels)
1965
+ init(rate, channels);
1966
+ append(input);
1967
+ const analysisHop = hop * speed;
1968
+ const segments = Array.from({ length: channelCount }, () => []);
1969
+ const emitGrain = (pos) => {
1970
+ const off = pos - inputStart;
1971
+ for (let channel = 0;channel < channelCount; channel += 1) {
1972
+ const src = buffers[channel];
1973
+ const tail = olaTail[channel];
1974
+ if (!src || !tail)
1975
+ continue;
1976
+ const grain = overlapAddGrain(src, off, tail, weights, hop);
1977
+ olaTail[channel] = grain.nextTail;
1978
+ segments[channel]?.push(grain.out);
1979
+ }
1980
+ };
1981
+ const captureRef = (pos) => {
1982
+ const ref = new Float32Array(frameLen);
1983
+ const refOff = pos + hop - inputStart;
1984
+ const [base] = buffers;
1985
+ if (base)
1986
+ ref.set(base.subarray(refOff, refOff + frameLen));
1987
+ naturalRef = ref;
1988
+ };
1989
+ const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
1990
+ while (canEmit()) {
1991
+ const center = Math.round(analysisPos);
1992
+ const pos = center + bestOffset(center);
1993
+ emitGrain(pos);
1994
+ captureRef(pos);
1995
+ analysisPos += analysisHop;
1996
+ }
1997
+ compact();
1998
+ return segments.map((channelSegments) => {
1999
+ const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
2000
+ const merged = new Float32Array(total);
2001
+ let offset = 0;
2002
+ for (const seg of channelSegments) {
2003
+ merged.set(seg, offset);
2004
+ offset += seg.length;
2005
+ }
2006
+ return merged;
2007
+ });
2008
+ };
2009
+ return { process, reset };
2010
+ };
2011
+
2012
+ // src/client/audioPlayer.ts
2013
+ var activeAudioPlayers = new Set;
2014
+ var DEFAULT_LOOKAHEAD_MS = 15;
2015
+ var DEFAULT_VOLUME = 1;
2016
+ var DEFAULT_PLAYBACK_RATE = 1;
2017
+ var MIN_PLAYBACK_RATE = 0.5;
2018
+ var MAX_PLAYBACK_RATE = 2;
2019
+ var STRETCH_BYPASS_EPSILON = 0.01;
2020
+ var ANALYSER_FFT_SIZE = 256;
2021
+ var PCM_BYTE_MIDPOINT = 128;
2022
+ var GAP_EPSILON_S = 0.03;
2023
+ var createInitialState3 = () => ({
2024
+ activeSourceCount: 0,
2025
+ error: null,
2026
+ isActive: false,
2027
+ isPlaying: false,
2028
+ lastInterruptLatencyMs: undefined,
2029
+ lastPlaybackStopLatencyMs: undefined,
2030
+ processedChunkCount: 0,
2031
+ queuedChunkCount: 0
2032
+ });
2033
+ var getAudioContextCtor = () => {
2034
+ if (typeof window === "undefined") {
2035
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
2036
+ }
2037
+ return window.AudioContext ?? window.webkitAudioContext;
2038
+ };
2039
+ var clampVolume = (volume) => {
2040
+ if (typeof volume !== "number" || !Number.isFinite(volume)) {
2041
+ return DEFAULT_VOLUME;
2042
+ }
2043
+ return Math.min(1, Math.max(0, volume));
2044
+ };
2045
+ var clampPlaybackRate = (rate) => {
2046
+ if (typeof rate !== "number" || !Number.isFinite(rate)) {
2047
+ return DEFAULT_PLAYBACK_RATE;
2048
+ }
2049
+ return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
2050
+ };
2051
+ var decodePCM16LEChunk = (audioContext, chunk) => {
2052
+ const { format } = chunk;
2053
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
2054
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
2055
+ }
2056
+ const bytes = chunk.chunk;
2057
+ const channels = Math.max(1, format.channels);
2058
+ const sampleCount = Math.floor(bytes.byteLength / 2);
2059
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
2060
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
2061
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2062
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
2063
+ const channelData = audioBuffer.getChannelData(channelIndex);
2064
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
2065
+ const sampleIndex = frameIndex * channels + channelIndex;
2066
+ const sampleOffset = sampleIndex * 2;
2067
+ if (sampleOffset + 1 >= bytes.byteLength) {
2068
+ channelData[frameIndex] = 0;
2069
+ continue;
2070
+ }
2071
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
2072
+ }
2073
+ }
2074
+ return audioBuffer;
2075
+ };
2076
+ var createVoiceAudioPlayer = (source, options = {}) => {
2077
+ const subscribers = new Set;
2078
+ const sourceNodes = new Set;
2079
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
2080
+ let state = createInitialState3();
2081
+ let audioContext = null;
2082
+ let outputNode = null;
2083
+ let analyserNode = null;
2084
+ let analyserBuffer = null;
2085
+ let volume = clampVolume(options.volume);
2086
+ let playbackRate = clampPlaybackRate(options.playbackRate);
2087
+ let stretcher = null;
2088
+ let queueEndTime = 0;
2089
+ let syncPromise = Promise.resolve();
2090
+ let interruptStartedAt = null;
2091
+ let interruptPromise = null;
2092
+ let resolveInterruptPromise = null;
2093
+ let interruptFallbackTimer = null;
2094
+ let concurrencyCounted = false;
2095
+ let observedPeakConcurrency = 0;
2096
+ let scheduledDurationMs = 0;
2097
+ let gapCount = 0;
2098
+ let totalGapMs = 0;
2099
+ let maxGapMs = 0;
2100
+ let errorCount = 0;
2101
+ let hasScheduledAnyChunk = false;
2102
+ const sampleConcurrency = () => {
2103
+ if (activeAudioPlayers.size > observedPeakConcurrency) {
2104
+ observedPeakConcurrency = activeAudioPlayers.size;
2105
+ }
2106
+ };
2107
+ const markPlayerActive = () => {
2108
+ if (concurrencyCounted) {
2109
+ return;
2110
+ }
2111
+ concurrencyCounted = true;
2112
+ activeAudioPlayers.add(sampleConcurrency);
2113
+ for (const sample of activeAudioPlayers) {
2114
+ sample();
2115
+ }
2116
+ };
2117
+ const markPlayerInactive = () => {
2118
+ if (!concurrencyCounted) {
2119
+ return;
2120
+ }
2121
+ concurrencyCounted = false;
2122
+ activeAudioPlayers.delete(sampleConcurrency);
2123
+ };
2124
+ const notify = () => {
2125
+ for (const subscriber of subscribers) {
2126
+ subscriber();
2127
+ }
2128
+ };
2129
+ const setState = (next) => {
2130
+ state = {
2131
+ ...state,
2132
+ ...next
2133
+ };
2134
+ notify();
2135
+ };
2136
+ const clearError = () => {
2137
+ if (state.error !== null) {
2138
+ setState({ error: null });
2139
+ }
2140
+ };
2141
+ const clearInterruptTimer = () => {
2142
+ if (interruptFallbackTimer !== null) {
2143
+ clearTimeout(interruptFallbackTimer);
2144
+ interruptFallbackTimer = null;
2145
+ }
2146
+ };
2147
+ const resolveInterrupt = (latencyMs) => {
2148
+ clearInterruptTimer();
2149
+ interruptStartedAt = null;
2150
+ stretcher?.reset();
2151
+ setState({
2152
+ activeSourceCount: sourceNodes.size,
2153
+ isPlaying: false,
2154
+ lastInterruptLatencyMs: latencyMs,
2155
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
2156
+ });
2157
+ resolveInterruptPromise?.();
2158
+ resolveInterruptPromise = null;
2159
+ interruptPromise = null;
2160
+ };
2161
+ const estimateOutputStopLatencyMs = (context) => {
2162
+ if (!context) {
2163
+ return 0;
2164
+ }
2165
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
2166
+ };
2167
+ const applyOutputGain = (context) => {
2168
+ if (!outputNode) {
2169
+ return;
2170
+ }
2171
+ const gainValue = volume;
2172
+ if (outputNode.gain.setValueAtTime) {
2173
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
2174
+ return;
2175
+ }
2176
+ outputNode.gain.value = gainValue;
2177
+ };
2178
+ const muteOutputGain = (context) => {
2179
+ if (!outputNode) {
2180
+ return;
2181
+ }
2182
+ const gainValue = 0;
2183
+ if (outputNode.gain.setValueAtTime) {
2184
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
2185
+ return;
2186
+ }
2187
+ outputNode.gain.value = gainValue;
2188
+ };
2189
+ const maybeResolveInterrupt = () => {
2190
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
2191
+ return;
2192
+ }
2193
+ resolveInterrupt(Date.now() - interruptStartedAt);
2194
+ };
2195
+ const ensureAudioContext = async () => {
2196
+ if (audioContext) {
2197
+ return audioContext;
2198
+ }
2199
+ if (options.createAudioContext) {
2200
+ audioContext = options.createAudioContext();
2201
+ } else {
2202
+ const AudioContextCtor = getAudioContextCtor();
2203
+ if (!AudioContextCtor) {
2204
+ throw new Error("Assistant audio playback requires AudioContext support.");
2205
+ }
2206
+ audioContext = new AudioContextCtor;
2207
+ }
2208
+ if (audioContext.createGain) {
2209
+ outputNode = audioContext.createGain();
2210
+ outputNode.connect?.(audioContext.destination);
2211
+ if (audioContext.createAnalyser) {
2212
+ analyserNode = audioContext.createAnalyser();
2213
+ analyserNode.fftSize = ANALYSER_FFT_SIZE;
2214
+ analyserBuffer = new Uint8Array(analyserNode.fftSize);
2215
+ outputNode.connect?.(analyserNode);
2216
+ }
2217
+ }
2218
+ queueEndTime = audioContext.currentTime;
2219
+ return audioContext;
2220
+ };
2221
+ const scheduleBuffer = (context, buffer, rate) => {
2222
+ const node = context.createBufferSource();
2223
+ node.buffer = buffer;
2224
+ if (node.playbackRate) {
2225
+ node.playbackRate.value = rate;
2226
+ }
2227
+ node.connect(outputNode ?? context.destination);
2228
+ node.onended = () => {
2229
+ sourceNodes.delete(node);
2230
+ node.disconnect?.();
2231
+ setState({
2232
+ activeSourceCount: sourceNodes.size,
2233
+ isPlaying: sourceNodes.size > 0 && state.isActive
2234
+ });
2235
+ maybeResolveInterrupt();
2236
+ };
2237
+ const earliestStart = context.currentTime + lookaheadSeconds;
2238
+ const startAt = Math.max(earliestStart, queueEndTime);
2239
+ if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
2240
+ const gapMs = (earliestStart - queueEndTime) * 1000;
2241
+ gapCount += 1;
2242
+ totalGapMs += gapMs;
2243
+ maxGapMs = Math.max(maxGapMs, gapMs);
2244
+ }
2245
+ hasScheduledAnyChunk = true;
2246
+ scheduledDurationMs += buffer.duration / rate * 1000;
2247
+ sampleConcurrency();
2248
+ queueEndTime = startAt + buffer.duration / rate;
2249
+ sourceNodes.add(node);
2250
+ setState({
2251
+ activeSourceCount: sourceNodes.size,
2252
+ isPlaying: true
2253
+ });
2254
+ node.start(startAt);
2255
+ };
2256
+ const scheduleChunk = async (chunk) => {
2257
+ const context = await ensureAudioContext();
2258
+ const buffer = decodePCM16LEChunk(context, chunk);
2259
+ if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
2260
+ stretcher?.reset();
2261
+ scheduleBuffer(context, buffer, playbackRate);
2262
+ return;
2263
+ }
2264
+ const channels = Math.max(1, chunk.format.channels);
2265
+ const input = [];
2266
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
2267
+ input.push(buffer.getChannelData(channelIndex));
2268
+ }
2269
+ stretcher ??= createTimeStretcher();
2270
+ const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
2271
+ const outLength = stretched[0]?.length ?? 0;
2272
+ if (outLength === 0) {
2273
+ return;
2274
+ }
2275
+ const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
2276
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
2277
+ const channelOut = stretched[channelIndex];
2278
+ if (!channelOut)
2279
+ continue;
2280
+ outBuffer.getChannelData(channelIndex).set(channelOut);
2281
+ }
2282
+ scheduleBuffer(context, outBuffer, 1);
2283
+ };
2284
+ const stopQueuedPlayback = (options2) => {
2285
+ for (const node of [...sourceNodes]) {
2286
+ node.stop?.();
2287
+ }
2288
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
2289
+ if (options2?.forceClear) {
2290
+ for (const node of sourceNodes) {
2291
+ node.disconnect?.();
2292
+ }
2293
+ sourceNodes.clear();
2294
+ maybeResolveInterrupt();
2295
+ }
2296
+ };
2297
+ const sync = async () => {
2298
+ if (!state.isActive) {
2299
+ return;
2300
+ }
2301
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
2302
+ if (nextChunks.length === 0) {
2303
+ return;
2304
+ }
2305
+ try {
2306
+ clearError();
2307
+ for (const chunk of nextChunks) {
2308
+ await scheduleChunk(chunk);
2309
+ }
2310
+ setState({
2311
+ processedChunkCount: source.assistantAudio.length,
2312
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
2313
+ });
2314
+ } catch (error) {
2315
+ errorCount += 1;
2316
+ setState({
2317
+ error: error instanceof Error ? error.message : String(error)
2318
+ });
2319
+ }
2320
+ };
2321
+ const queueSync = () => {
2322
+ syncPromise = syncPromise.then(() => sync(), () => sync());
2323
+ return syncPromise;
2324
+ };
2325
+ const unsubscribeSource = source.subscribe(() => {
2326
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
2327
+ player.start();
2328
+ return;
2329
+ }
2330
+ if (state.isActive) {
2331
+ queueSync();
2332
+ }
2333
+ });
2334
+ const player = {
2335
+ get activeSourceCount() {
2336
+ return state.activeSourceCount;
1094
2337
  },
1095
- get assistantTexts() {
1096
- return state.assistantTexts;
2338
+ close: async () => {
2339
+ markPlayerInactive();
2340
+ unsubscribeSource();
2341
+ stopQueuedPlayback({ forceClear: true });
2342
+ clearInterruptTimer();
2343
+ resolveInterruptPromise?.();
2344
+ resolveInterruptPromise = null;
2345
+ interruptPromise = null;
2346
+ interruptStartedAt = null;
2347
+ if (audioContext && audioContext.state !== "closed") {
2348
+ await audioContext.close();
2349
+ }
2350
+ audioContext = null;
2351
+ outputNode?.disconnect?.();
2352
+ outputNode = null;
2353
+ analyserNode?.disconnect?.();
2354
+ analyserNode = null;
2355
+ analyserBuffer = null;
2356
+ queueEndTime = 0;
2357
+ setState({
2358
+ activeSourceCount: 0,
2359
+ isActive: false,
2360
+ isPlaying: false
2361
+ });
1097
2362
  },
1098
- get assistantAudio() {
1099
- return state.assistantAudio;
2363
+ get error() {
2364
+ return state.error;
1100
2365
  },
1101
- get call() {
1102
- return state.call;
2366
+ getOutputLevel: () => {
2367
+ if (!analyserNode || !analyserBuffer) {
2368
+ return 0;
2369
+ }
2370
+ analyserNode.getByteTimeDomainData(analyserBuffer);
2371
+ let sumSquares = 0;
2372
+ for (const sample of analyserBuffer) {
2373
+ const centered = (sample - PCM_BYTE_MIDPOINT) / PCM_BYTE_MIDPOINT;
2374
+ sumSquares += centered * centered;
2375
+ }
2376
+ return Math.sqrt(sumSquares / analyserBuffer.length);
2377
+ },
2378
+ getSnapshot: () => state,
2379
+ getIntegritySummary: () => {
2380
+ const chunksReceived = source.assistantAudio.length;
2381
+ const chunksScheduled = state.processedChunkCount;
2382
+ return {
2383
+ chunksReceived,
2384
+ chunksScheduled,
2385
+ errorCount,
2386
+ gapCount,
2387
+ maxConcurrentPlayers: observedPeakConcurrency,
2388
+ maxGapMs: Math.round(maxGapMs),
2389
+ ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
2390
+ scheduledDurationMs: Math.round(scheduledDurationMs),
2391
+ totalGapMs: Math.round(totalGapMs)
2392
+ };
2393
+ },
2394
+ interrupt: async () => {
2395
+ const startedAt = Date.now();
2396
+ const context = await ensureAudioContext();
2397
+ interruptStartedAt = startedAt;
2398
+ muteOutputGain(context);
2399
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
2400
+ setState({
2401
+ isActive: false,
2402
+ isPlaying: sourceNodes.size > 0,
2403
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
2404
+ });
2405
+ if (sourceNodes.size === 0) {
2406
+ resolveInterrupt(playbackStopLatencyMs);
2407
+ return;
2408
+ }
2409
+ if (!interruptPromise) {
2410
+ interruptPromise = new Promise((resolve) => {
2411
+ resolveInterruptPromise = resolve;
2412
+ });
2413
+ }
2414
+ clearInterruptTimer();
2415
+ interruptFallbackTimer = setTimeout(() => {
2416
+ for (const node of sourceNodes) {
2417
+ node.disconnect?.();
2418
+ }
2419
+ sourceNodes.clear();
2420
+ resolveInterrupt(Date.now() - startedAt);
2421
+ }, 250);
2422
+ stopQueuedPlayback();
2423
+ await interruptPromise;
2424
+ },
2425
+ get isActive() {
2426
+ return state.isActive;
2427
+ },
2428
+ get isPlaying() {
2429
+ return state.isPlaying;
2430
+ },
2431
+ get lastInterruptLatencyMs() {
2432
+ return state.lastInterruptLatencyMs;
2433
+ },
2434
+ get lastPlaybackStopLatencyMs() {
2435
+ return state.lastPlaybackStopLatencyMs;
2436
+ },
2437
+ pause: async () => {
2438
+ if (!audioContext) {
2439
+ setState({
2440
+ activeSourceCount: 0,
2441
+ isActive: false,
2442
+ isPlaying: false
2443
+ });
2444
+ return;
2445
+ }
2446
+ await audioContext.suspend();
2447
+ markPlayerInactive();
2448
+ setState({
2449
+ activeSourceCount: sourceNodes.size,
2450
+ isActive: false,
2451
+ isPlaying: false
2452
+ });
2453
+ },
2454
+ get playbackRate() {
2455
+ return playbackRate;
2456
+ },
2457
+ get processedChunkCount() {
2458
+ return state.processedChunkCount;
2459
+ },
2460
+ get queuedChunkCount() {
2461
+ return state.queuedChunkCount;
2462
+ },
2463
+ setPlaybackRate: (nextRate) => {
2464
+ playbackRate = clampPlaybackRate(nextRate);
2465
+ },
2466
+ setVolume: (nextVolume) => {
2467
+ volume = clampVolume(nextVolume);
2468
+ applyOutputGain(audioContext);
2469
+ },
2470
+ start: async () => {
2471
+ try {
2472
+ clearError();
2473
+ const context = await ensureAudioContext();
2474
+ applyOutputGain(context);
2475
+ if (context.state === "suspended") {
2476
+ await context.resume();
2477
+ }
2478
+ markPlayerActive();
2479
+ setState({
2480
+ activeSourceCount: sourceNodes.size,
2481
+ isActive: true,
2482
+ isPlaying: context.state === "running"
2483
+ });
2484
+ await queueSync();
2485
+ } catch (error) {
2486
+ errorCount += 1;
2487
+ markPlayerInactive();
2488
+ setState({
2489
+ error: error instanceof Error ? error.message : String(error),
2490
+ isActive: false,
2491
+ isPlaying: false
2492
+ });
2493
+ throw error;
2494
+ }
2495
+ },
2496
+ subscribe: (subscriber) => {
2497
+ subscribers.add(subscriber);
2498
+ return () => {
2499
+ subscribers.delete(subscriber);
2500
+ };
2501
+ },
2502
+ get volume() {
2503
+ return volume;
2504
+ }
2505
+ };
2506
+ return player;
2507
+ };
2508
+
2509
+ // src/client/bargeInMonitor.ts
2510
+ var DEFAULT_THRESHOLD_MS = 250;
2511
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
2512
+ var summarize = (events, thresholdMs) => {
2513
+ const stopped = events.filter((event) => event.status === "stopped");
2514
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
2515
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
2516
+ const passed = stopped.length - failed;
2517
+ return {
2518
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
2519
+ events: [...events],
2520
+ failed,
2521
+ lastEvent: events.at(-1),
2522
+ passed,
2523
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
2524
+ thresholdMs,
2525
+ total: stopped.length
2526
+ };
2527
+ };
2528
+ var createVoiceBargeInMonitor = (options = {}) => {
2529
+ const listeners = new Set;
2530
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
2531
+ const fetchImpl = options.fetch ?? globalThis.fetch;
2532
+ const events = [];
2533
+ const emit = () => {
2534
+ for (const listener of listeners) {
2535
+ listener();
2536
+ }
2537
+ };
2538
+ const postEvent = (event) => {
2539
+ if (!options.path || typeof fetchImpl !== "function") {
2540
+ return;
2541
+ }
2542
+ fetchImpl(options.path, {
2543
+ body: JSON.stringify(event),
2544
+ headers: {
2545
+ "Content-Type": "application/json"
2546
+ },
2547
+ method: "POST"
2548
+ }).catch(() => {});
2549
+ };
2550
+ const record = (status, input) => {
2551
+ const event = {
2552
+ at: Date.now(),
2553
+ id: createEventId(),
2554
+ latencyMs: input.latencyMs,
2555
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
2556
+ reason: input.reason,
2557
+ sessionId: input.sessionId,
2558
+ status,
2559
+ thresholdMs
2560
+ };
2561
+ events.push(event);
2562
+ postEvent(event);
2563
+ emit();
2564
+ return event;
2565
+ };
2566
+ return {
2567
+ getSnapshot: () => summarize(events, thresholdMs),
2568
+ recordRequested: (input) => record("requested", input),
2569
+ recordSkipped: (input) => record("skipped", input),
2570
+ recordStopped: (input) => record("stopped", input),
2571
+ subscribe: (subscriber) => {
2572
+ listeners.add(subscriber);
2573
+ return () => {
2574
+ listeners.delete(subscriber);
2575
+ };
2576
+ }
2577
+ };
2578
+ };
2579
+
2580
+ // src/client/duplex.ts
2581
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
2582
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
2583
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
2584
+ let lastPartial = controller.partial;
2585
+ const interruptIfPlaying = (reason) => {
2586
+ if (!player.isPlaying || options.enabled === false) {
2587
+ options.monitor?.recordSkipped({
2588
+ reason,
2589
+ sessionId: controller.sessionId
2590
+ });
2591
+ return;
2592
+ }
2593
+ options.monitor?.recordRequested({
2594
+ reason,
2595
+ sessionId: controller.sessionId
2596
+ });
2597
+ player.interrupt().then(() => {
2598
+ options.monitor?.recordStopped({
2599
+ latencyMs: player.lastInterruptLatencyMs,
2600
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
2601
+ reason,
2602
+ sessionId: controller.sessionId
2603
+ });
2604
+ });
2605
+ };
2606
+ const unsubscribe = controller.subscribe(() => {
2607
+ if (options.interruptOnPartial === false) {
2608
+ lastPartial = controller.partial;
2609
+ return;
2610
+ }
2611
+ if (!lastPartial && controller.partial) {
2612
+ interruptIfPlaying("partial-transcript");
2613
+ }
2614
+ lastPartial = controller.partial;
2615
+ });
2616
+ return {
2617
+ close: () => {
2618
+ unsubscribe();
2619
+ },
2620
+ handleLevel: (level) => {
2621
+ if (shouldInterruptForLevel(level, options)) {
2622
+ interruptIfPlaying("input-level");
2623
+ }
2624
+ },
2625
+ sendAudio: (audio) => {
2626
+ interruptIfPlaying("manual-audio");
2627
+ controller.sendAudio(audio);
1103
2628
  }
1104
2629
  };
1105
2630
  };
@@ -1126,8 +2651,7 @@ var DEFAULT_GUIDED_PROMPTS = [
1126
2651
  "Now describe what you are trying to do or test.",
1127
2652
  "Finish with any detail that feels blocked, risky, or unclear."
1128
2653
  ];
1129
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1130
- var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
2654
+ var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
1131
2655
  var readErrorField = (value, key) => {
1132
2656
  const candidate = value[key];
1133
2657
  if (typeof candidate === "string" && candidate.trim()) {
@@ -1160,6 +2684,17 @@ var formatErrorMessage = (error) => {
1160
2684
  }
1161
2685
  return "Unexpected error";
1162
2686
  };
2687
+ var formatReconnectState = (reconnect) => {
2688
+ const pieces = [reconnect.status];
2689
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
2690
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
2691
+ }
2692
+ if (reconnect.nextAttemptAt) {
2693
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
2694
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
2695
+ }
2696
+ return pieces.join(" · ");
2697
+ };
1163
2698
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1164
2699
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1165
2700
  const next = levels.slice(-(count - 1));
@@ -1216,6 +2751,20 @@ var parsePromptList = (value) => {
1216
2751
  } catch {}
1217
2752
  return DEFAULT_GUIDED_PROMPTS;
1218
2753
  };
2754
+ var parseOptionalNumber = (value) => {
2755
+ if (!value) {
2756
+ return;
2757
+ }
2758
+ const parsed = Number(value);
2759
+ return Number.isFinite(parsed) ? parsed : undefined;
2760
+ };
2761
+ var resolveElement2 = (root, selector, ctor) => {
2762
+ if (!selector) {
2763
+ return null;
2764
+ }
2765
+ const value = document.querySelector(selector);
2766
+ return value instanceof ctor ? value : null;
2767
+ };
1219
2768
  var requireElement = (root, selector, ctor, name) => {
1220
2769
  const value = selector ? document.querySelector(selector) : null;
1221
2770
  if (value instanceof ctor) {
@@ -1266,11 +2815,20 @@ var initVoiceHTMXRoot = (root) => {
1266
2815
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
2816
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
2817
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
2818
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
2819
+ const bargeInPath = root.dataset.voiceBargeInPath;
2820
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
2821
+ path: bargeInPath,
2822
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
2823
+ }) : null;
2824
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
2825
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
2826
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
2827
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
2828
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1272
2829
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1273
2830
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
2831
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1274
2832
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1275
2833
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1276
2834
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1279,35 +2837,70 @@ var initVoiceHTMXRoot = (root) => {
1279
2837
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
2838
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
2839
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
2840
+ let activeMode = null;
2841
+ let hasStartedModes = {
2842
+ general: false,
2843
+ guided: false
2844
+ };
2845
+ let isCapturing = false;
2846
+ let micError = null;
2847
+ let waveLevels = createInitialVoiceWaveLevels();
2848
+ let guidedBargeInBinding = null;
2849
+ let generalBargeInBinding = null;
1282
2850
  const guidedVoice = createVoiceController(guidedPath, {
1283
2851
  capture: {
2852
+ onAudio: (audio, sendAudio) => {
2853
+ if (guidedBargeInBinding) {
2854
+ guidedBargeInBinding.sendAudio(audio);
2855
+ return;
2856
+ }
2857
+ sendAudio(audio);
2858
+ },
1284
2859
  onLevel: (level) => {
2860
+ guidedBargeInBinding?.handleLevel(level);
1285
2861
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
2862
  renderWave();
1287
2863
  }
1288
2864
  },
2865
+ connection: {
2866
+ reconnectReportPath
2867
+ },
1289
2868
  preset: "guided-intake"
1290
2869
  });
1291
2870
  const generalVoice = createVoiceController(generalPath, {
1292
2871
  capture: {
2872
+ onAudio: (audio, sendAudio) => {
2873
+ if (generalBargeInBinding) {
2874
+ generalBargeInBinding.sendAudio(audio);
2875
+ return;
2876
+ }
2877
+ sendAudio(audio);
2878
+ },
1293
2879
  onLevel: (level) => {
2880
+ generalBargeInBinding?.handleLevel(level);
1294
2881
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
2882
  renderWave();
1296
2883
  }
1297
2884
  },
2885
+ connection: {
2886
+ reconnectReportPath
2887
+ },
1298
2888
  preset: "dictation"
1299
2889
  });
1300
2890
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
2891
  const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
1302
- let activeMode = null;
1303
- let hasStartedModes = {
1304
- general: false,
1305
- guided: false
1306
- };
1307
- let isCapturing = false;
1308
- let micError = null;
1309
- let waveLevels = createInitialVoiceWaveLevels();
2892
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
2893
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
2894
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
2895
+ interruptThreshold: bargeInSpeechThreshold,
2896
+ monitor: bargeInMonitor ?? undefined
2897
+ });
2898
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
2899
+ interruptThreshold: bargeInSpeechThreshold,
2900
+ monitor: bargeInMonitor ?? undefined
2901
+ });
1310
2902
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
2903
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1311
2904
  const renderWave = () => {
1312
2905
  const path = createVoiceWavePath(waveLevels);
1313
2906
  voiceWaveGlow.setAttribute("d", path);
@@ -1319,9 +2912,12 @@ var initVoiceHTMXRoot = (root) => {
1319
2912
  const render = () => {
1320
2913
  const voice = currentVoice();
1321
2914
  const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
1322
- const status = voice.status;
2915
+ const { status } = voice;
1323
2916
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1324
2917
  errorStatus.textContent = micError || voice.error || "None";
2918
+ if (reconnectStatus) {
2919
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2920
+ }
1325
2921
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1326
2922
  promptStatus.textContent = resolvePromptMessage({
1327
2923
  guidedPrompts,
@@ -1385,8 +2981,18 @@ var initVoiceHTMXRoot = (root) => {
1385
2981
  render();
1386
2982
  }
1387
2983
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
2984
+ guidedVoice.subscribe(() => {
2985
+ if (guidedVoice.assistantAudio.length > 0) {
2986
+ guidedAudioPlayer.start().catch(() => {});
2987
+ }
2988
+ render();
2989
+ });
2990
+ generalVoice.subscribe(() => {
2991
+ if (generalVoice.assistantAudio.length > 0) {
2992
+ generalAudioPlayer.start().catch(() => {});
2993
+ }
2994
+ render();
2995
+ });
1390
2996
  startGuidedButton.addEventListener("click", () => {
1391
2997
  startMode("guided");
1392
2998
  });
@@ -1396,9 +3002,16 @@ var initVoiceHTMXRoot = (root) => {
1396
3002
  stopButton.addEventListener("click", () => {
1397
3003
  stopMic();
1398
3004
  });
3005
+ root.addEventListener("absolute-voice-simulate-disconnect", () => {
3006
+ currentVoice().simulateDisconnect();
3007
+ });
1399
3008
  window.addEventListener("beforeunload", () => {
1400
3009
  guidedVoice.stopRecording();
1401
3010
  generalVoice.stopRecording();
3011
+ guidedBargeInBinding?.close();
3012
+ generalBargeInBinding?.close();
3013
+ guidedAudioPlayer.close();
3014
+ generalAudioPlayer.close();
1402
3015
  stopGuidedBinding();
1403
3016
  stopGeneralBinding();
1404
3017
  guidedVoice.close();