@absolutejs/voice 0.0.22-beta.59 → 0.0.22-beta.590

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 (501) hide show
  1. package/LICENSE +88 -0
  2. package/README.md +4543 -1181
  3. package/dist/angular/index.d.ts +36 -6
  4. package/dist/angular/index.js +4349 -482
  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 +12 -0
  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 +5 -1
  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 +128 -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 +8 -3
  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 +1350 -83
  58. package/dist/client/htmxDashboardRenderers.d.ts +71 -0
  59. package/dist/client/index.d.ts +107 -19
  60. package/dist/client/index.js +10889 -397
  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 +21 -0
  82. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  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 +33 -0
  86. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  87. package/dist/client/providerStatus.d.ts +5 -3
  88. package/dist/client/providerStatusWidget.d.ts +32 -0
  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} +90 -6
  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} +14 -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 +24 -0
  131. package/dist/core/bargeInRoutes.d.ts +56 -0
  132. package/dist/core/bookingFlow.d.ts +43 -0
  133. package/dist/core/browserCallProfiles.d.ts +120 -0
  134. package/dist/core/browserMediaRoutes.d.ts +62 -0
  135. package/dist/core/cachedTTS.d.ts +54 -0
  136. package/dist/core/calendarAdapter.d.ts +47 -0
  137. package/dist/core/calendarSlots.d.ts +35 -0
  138. package/dist/core/callDebugger.d.ts +66 -0
  139. package/dist/core/callDisposition.d.ts +38 -0
  140. package/dist/core/callQuota.d.ts +54 -0
  141. package/dist/core/callScorecard.d.ts +53 -0
  142. package/dist/core/callerCRMLinker.d.ts +29 -0
  143. package/dist/core/callerMemory.d.ts +37 -0
  144. package/dist/core/callingWindow.d.ts +26 -0
  145. package/dist/core/campaign.d.ts +795 -0
  146. package/dist/core/campaignControls.d.ts +37 -0
  147. package/dist/core/campaignDialers.d.ts +111 -0
  148. package/dist/core/campaignTemplate.d.ts +16 -0
  149. package/dist/core/competitiveCoverage.d.ts +141 -0
  150. package/dist/core/conversationSimulator.d.ts +73 -0
  151. package/dist/{correction.d.ts → core/correction.d.ts} +5 -4
  152. package/dist/core/costAccounting.d.ts +76 -0
  153. package/dist/core/costPredictor.d.ts +74 -0
  154. package/dist/core/crmCallLogger.d.ts +37 -0
  155. package/dist/core/crmContract.d.ts +70 -0
  156. package/dist/core/dataControl.d.ts +180 -0
  157. package/dist/core/debugTiming.d.ts +11 -0
  158. package/dist/core/defineVoiceAssistant.d.ts +68 -0
  159. package/dist/core/deliveryRuntime.d.ts +159 -0
  160. package/dist/core/deliverySinkRoutes.d.ts +117 -0
  161. package/dist/core/demoReadyRoutes.d.ts +98 -0
  162. package/dist/{diagnosticsRoutes.d.ts → core/diagnosticsRoutes.d.ts} +2 -2
  163. package/dist/core/dncRegistry.d.ts +38 -0
  164. package/dist/core/dtmfCollector.d.ts +37 -0
  165. package/dist/{evalRoutes.d.ts → core/evalRoutes.d.ts} +26 -20
  166. package/dist/{fileStore.d.ts → core/fileStore.d.ts} +34 -20
  167. package/dist/core/guardrails.d.ts +128 -0
  168. package/dist/{handoff.d.ts → core/handoff.d.ts} +10 -10
  169. package/dist/{handoffHealth.d.ts → core/handoffHealth.d.ts} +9 -9
  170. package/dist/core/hardenedFetch.d.ts +3 -0
  171. package/dist/core/holdAudio.d.ts +23 -0
  172. package/dist/{htmx.d.ts → core/htmx.d.ts} +2 -2
  173. package/dist/core/htmxDashboardRoutes.d.ts +250 -0
  174. package/dist/core/iceServers.d.ts +34 -0
  175. package/dist/core/incidentBundle.d.ts +119 -0
  176. package/dist/core/incidentTimeline.d.ts +260 -0
  177. package/dist/core/ivrPlan.d.ts +40 -0
  178. package/dist/core/latencySlo.d.ts +56 -0
  179. package/dist/core/liveCoach.d.ts +43 -0
  180. package/dist/core/liveLatency.d.ts +78 -0
  181. package/dist/core/liveOps.d.ts +190 -0
  182. package/dist/core/llmJudge.d.ts +45 -0
  183. package/dist/{logger.d.ts → core/logger.d.ts} +1 -2
  184. package/dist/core/mcpToolset.d.ts +58 -0
  185. package/dist/core/mediaPipelineRoutes.d.ts +171 -0
  186. package/dist/core/mediaPipelineSurfaces.d.ts +48 -0
  187. package/dist/{memoryStore.d.ts → core/memoryStore.d.ts} +1 -1
  188. package/dist/core/midCallSummary.d.ts +27 -0
  189. package/dist/{modelAdapters.d.ts → core/modelAdapters.d.ts} +52 -7
  190. package/dist/core/monitor.d.ts +148 -0
  191. package/dist/core/multilingualProof.d.ts +77 -0
  192. package/dist/core/noShowPredictor.d.ts +46 -0
  193. package/dist/core/oauth2TokenSource.d.ts +21 -0
  194. package/dist/core/observabilityExport.d.ts +501 -0
  195. package/dist/core/openaiTTS.d.ts +18 -0
  196. package/dist/core/operationalStatus.d.ts +87 -0
  197. package/dist/core/operationsRecord.d.ts +371 -0
  198. package/dist/{ops.d.ts → core/ops.d.ts} +70 -70
  199. package/dist/core/opsActionAuditRoutes.d.ts +99 -0
  200. package/dist/{opsConsoleRoutes.d.ts → core/opsConsoleRoutes.d.ts} +11 -8
  201. package/dist/{opsPresets.d.ts → core/opsPresets.d.ts} +2 -2
  202. package/dist/core/opsRecovery.d.ts +137 -0
  203. package/dist/{opsRuntime.d.ts → core/opsRuntime.d.ts} +6 -6
  204. package/dist/{opsSinks.d.ts → core/opsSinks.d.ts} +19 -19
  205. package/dist/core/opsStatus.d.ts +76 -0
  206. package/dist/core/opsStatusRoutes.d.ts +33 -0
  207. package/dist/{opsWebhook.d.ts → core/opsWebhook.d.ts} +15 -15
  208. package/dist/core/otelExporter.d.ts +83 -0
  209. package/dist/core/outcomeContract.d.ts +146 -0
  210. package/dist/{outcomeRecipes.d.ts → core/outcomeRecipes.d.ts} +4 -4
  211. package/dist/core/pathway.d.ts +94 -0
  212. package/dist/core/pathwayCompiler.d.ts +31 -0
  213. package/dist/core/pathwayGenerator.d.ts +27 -0
  214. package/dist/core/pathwayRuntime.d.ts +57 -0
  215. package/dist/core/pathwaySlotCollector.d.ts +29 -0
  216. package/dist/core/pathwayVisualizer.d.ts +8 -0
  217. package/dist/core/phoneAgent.d.ts +139 -0
  218. package/dist/core/phoneAgentProductionSmoke.d.ts +115 -0
  219. package/dist/core/phoneProvisioning.d.ts +29 -0
  220. package/dist/core/platformCoverage.d.ts +91 -0
  221. package/dist/{plugin.d.ts → core/plugin.d.ts} +2 -2
  222. package/dist/core/postCallAnalysis.d.ts +98 -0
  223. package/dist/core/postCallSurvey.d.ts +41 -0
  224. package/dist/{postgresStore.d.ts → core/postgresStore.d.ts} +20 -9
  225. package/dist/{presets.d.ts → core/presets.d.ts} +3 -3
  226. package/dist/core/productionReadiness.d.ts +757 -0
  227. package/dist/core/profileSwitchRecommendation.d.ts +350 -0
  228. package/dist/core/promptInjectionGuard.d.ts +30 -0
  229. package/dist/core/proofAssertions.d.ts +32 -0
  230. package/dist/core/proofPack.d.ts +211 -0
  231. package/dist/core/proofRunner.d.ts +79 -0
  232. package/dist/core/proofTrends.d.ts +966 -0
  233. package/dist/{providerAdapters.d.ts → core/providerAdapters.d.ts} +5 -5
  234. package/dist/core/providerCapabilities.d.ts +92 -0
  235. package/dist/core/providerDecisionTraces.d.ts +130 -0
  236. package/dist/{providerHealth.d.ts → core/providerHealth.d.ts} +15 -5
  237. package/dist/core/providerOrchestration.d.ts +109 -0
  238. package/dist/core/providerRouterTraces.d.ts +35 -0
  239. package/dist/core/providerRoutingContract.d.ts +71 -0
  240. package/dist/core/providerSlo.d.ts +142 -0
  241. package/dist/core/providerStackRecommendations.d.ts +188 -0
  242. package/dist/core/qualityDriftDetector.d.ts +44 -0
  243. package/dist/{qualityRoutes.d.ts → core/qualityRoutes.d.ts} +7 -7
  244. package/dist/{queue.d.ts → core/queue.d.ts} +48 -39
  245. package/dist/core/ragTool.d.ts +52 -0
  246. package/dist/core/readinessProfiles.d.ts +45 -0
  247. package/dist/core/realtimeChannel.d.ts +136 -0
  248. package/dist/core/realtimeProviderContracts.d.ts +133 -0
  249. package/dist/core/reconnectContract.d.ts +177 -0
  250. package/dist/core/recordingRedaction.d.ts +47 -0
  251. package/dist/core/recordingStore.d.ts +60 -0
  252. package/dist/core/redaction.d.ts +13 -0
  253. package/dist/core/reminderScheduler.d.ts +43 -0
  254. package/dist/{resilienceRoutes.d.ts → core/resilienceRoutes.d.ts} +34 -5
  255. package/dist/core/retention.d.ts +37 -0
  256. package/dist/core/retryPolicy.d.ts +38 -0
  257. package/dist/core/routeAuth.d.ts +58 -0
  258. package/dist/{routing.d.ts → core/routing.d.ts} +2 -2
  259. package/dist/{runtimeOps.d.ts → core/runtimeOps.d.ts} +3 -3
  260. package/dist/{s3Store.d.ts → core/s3Store.d.ts} +12 -3
  261. package/dist/core/scorecardCalibration.d.ts +31 -0
  262. package/dist/core/scribe.d.ts +50 -0
  263. package/dist/core/semanticTurn.d.ts +27 -0
  264. package/dist/{session.d.ts → core/session.d.ts} +1 -1
  265. package/dist/core/sessionObservability.d.ts +145 -0
  266. package/dist/{sessionReplay.d.ts → core/sessionReplay.d.ts} +31 -19
  267. package/dist/core/sessionSnapshot.d.ts +109 -0
  268. package/dist/core/simulationSuite.d.ts +144 -0
  269. package/dist/core/sloCalibration.d.ts +185 -0
  270. package/dist/{sqliteStore.d.ts → core/sqliteStore.d.ts} +20 -9
  271. package/dist/{store.d.ts → core/store.d.ts} +1 -1
  272. package/dist/core/supervisorPermissions.d.ts +33 -0
  273. package/dist/core/supervisorPresence.d.ts +49 -0
  274. package/dist/core/telephonyMediaRoutes.d.ts +72 -0
  275. package/dist/core/telephonyOutcome.d.ts +269 -0
  276. package/dist/core/toolContract.d.ts +161 -0
  277. package/dist/core/toolRuntime.d.ts +50 -0
  278. package/dist/{trace.d.ts → core/trace.d.ts} +61 -22
  279. package/dist/core/traceDeliveryRoutes.d.ts +86 -0
  280. package/dist/core/traceTimeline.d.ts +97 -0
  281. package/dist/core/transcriptAnnotator.d.ts +41 -0
  282. package/dist/{turnDetection.d.ts → core/turnDetection.d.ts} +2 -1
  283. package/dist/core/turnLatency.d.ts +95 -0
  284. package/dist/core/turnProfiles.d.ts +3 -0
  285. package/dist/core/turnQuality.d.ts +94 -0
  286. package/dist/core/types.d.ts +1495 -0
  287. package/dist/core/vapiAdapter.d.ts +160 -0
  288. package/dist/core/variableAnalytics.d.ts +47 -0
  289. package/dist/core/voiceConfiguration.d.ts +8 -0
  290. package/dist/core/voiceMonitoring.d.ts +444 -0
  291. package/dist/core/webhookFanout.d.ts +48 -0
  292. package/dist/core/webhookVerification.d.ts +27 -0
  293. package/dist/core/whisperChannel.d.ts +50 -0
  294. package/dist/{workflowContract.d.ts → core/workflowContract.d.ts} +21 -21
  295. package/dist/core/writeBehindStore.d.ts +41 -0
  296. package/dist/core/zeroDataRetention.d.ts +31 -0
  297. package/dist/drizzle/assistantMemory.d.ts +100 -0
  298. package/dist/drizzle/eval.d.ts +55 -0
  299. package/dist/drizzle/handoff.d.ts +56 -0
  300. package/dist/drizzle/incidentBundle.d.ts +55 -0
  301. package/dist/drizzle/index.d.ts +991 -0
  302. package/dist/drizzle/index.js +3059 -0
  303. package/dist/drizzle/observabilityExport.d.ts +55 -0
  304. package/dist/drizzle/proofTrends.d.ts +114 -0
  305. package/dist/drizzle/runtimeStorage.d.ts +1183 -0
  306. package/dist/drizzle/shared.d.ts +69 -0
  307. package/dist/embed/index.d.ts +38 -0
  308. package/dist/embed/index.js +1781 -0
  309. package/dist/embed/voice-widget.js +10 -0
  310. package/dist/index.d.ts +376 -74
  311. package/dist/index.js +46979 -7684
  312. package/dist/internal/evidence.d.ts +10 -0
  313. package/dist/internal/html.d.ts +6 -0
  314. package/dist/internal/status.d.ts +7 -0
  315. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  316. package/dist/react/VoiceCallDebuggerLaunch.d.ts +6 -0
  317. package/dist/react/VoiceCallPlayer.d.ts +11 -0
  318. package/dist/react/VoiceCostDashboard.d.ts +10 -0
  319. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  320. package/dist/react/VoiceLiveAgentConsole.d.ts +11 -0
  321. package/dist/react/VoiceLiveCallViewer.d.ts +9 -0
  322. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  323. package/dist/react/VoiceOpsStatus.d.ts +1 -1
  324. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  325. package/dist/react/VoiceProfileComparison.d.ts +6 -0
  326. package/dist/react/VoiceProfileSwitchRecommendation.d.ts +6 -0
  327. package/dist/react/VoiceProofTrends.d.ts +6 -0
  328. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  329. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  330. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  331. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  332. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  333. package/dist/react/VoiceReconnectProfileEvidence.d.ts +6 -0
  334. package/dist/react/VoiceReplayTimeline.d.ts +6 -0
  335. package/dist/react/VoiceRoutingStatus.d.ts +1 -1
  336. package/dist/react/VoiceSessionObservability.d.ts +6 -0
  337. package/dist/react/VoiceSessionSnapshot.d.ts +6 -0
  338. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  339. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  340. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  341. package/dist/react/VoiceWidget.d.ts +13 -0
  342. package/dist/react/index.d.ts +80 -8
  343. package/dist/react/index.js +12927 -384
  344. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  345. package/dist/react/useVoiceCallDebugger.d.ts +8 -0
  346. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  347. package/dist/react/useVoiceController.d.ts +5 -1
  348. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  349. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  350. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  351. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  352. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  353. package/dist/react/useVoiceProfileComparison.d.ts +8 -0
  354. package/dist/react/useVoiceProfileSwitchRecommendation.d.ts +8 -0
  355. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  356. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  357. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  358. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  359. package/dist/react/useVoiceProviderStatus.d.ts +1 -1
  360. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  361. package/dist/react/useVoiceReconnectProfileEvidence.d.ts +8 -0
  362. package/dist/react/useVoiceRoutingStatus.d.ts +1 -1
  363. package/dist/react/useVoiceSessionObservability.d.ts +8 -0
  364. package/dist/react/useVoiceSessionSnapshot.d.ts +9 -0
  365. package/dist/react/useVoiceStream.d.ts +5 -1
  366. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  367. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  368. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  369. package/dist/react/useVoiceWorkflowStatus.d.ts +1 -1
  370. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  371. package/dist/svelte/createVoiceCallDebugger.d.ts +10 -0
  372. package/dist/svelte/createVoiceCallPlayer.d.ts +33 -0
  373. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  374. package/dist/svelte/createVoiceCostDashboard.d.ts +13 -0
  375. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  376. package/dist/svelte/createVoiceLiveAgentConsole.d.ts +23 -0
  377. package/dist/svelte/createVoiceLiveCallViewer.d.ts +26 -0
  378. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  379. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  380. package/dist/svelte/createVoiceOpsStatus.d.ts +4 -4
  381. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  382. package/dist/svelte/createVoiceProfileComparison.d.ts +7 -0
  383. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  384. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  385. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  386. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  387. package/dist/svelte/createVoiceProviderStatus.d.ts +5 -3
  388. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  389. package/dist/svelte/createVoiceReconnectProfileEvidence.d.ts +7 -0
  390. package/dist/svelte/createVoiceReplayTimeline.d.ts +13 -0
  391. package/dist/svelte/createVoiceRoutingStatus.d.ts +2 -2
  392. package/dist/svelte/createVoiceSessionObservability.d.ts +10 -0
  393. package/dist/svelte/createVoiceSessionSnapshot.d.ts +11 -0
  394. package/dist/svelte/createVoiceStream.d.ts +1 -1
  395. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  396. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  397. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  398. package/dist/svelte/createVoiceWidget.d.ts +19 -0
  399. package/dist/svelte/createVoiceWorkflowStatus.d.ts +2 -2
  400. package/dist/svelte/index.d.ts +37 -7
  401. package/dist/svelte/index.js +7393 -1089
  402. package/dist/telephony/contract.d.ts +61 -0
  403. package/dist/telephony/matrix.d.ts +97 -0
  404. package/dist/telephony/plivo.d.ts +303 -0
  405. package/dist/telephony/response.d.ts +1 -1
  406. package/dist/telephony/security.d.ts +182 -0
  407. package/dist/telephony/telnyx.d.ts +291 -0
  408. package/dist/telephony/twilio.d.ts +152 -15
  409. package/dist/testing/accuracy.d.ts +1 -1
  410. package/dist/testing/benchmark.d.ts +15 -15
  411. package/dist/testing/corrected.d.ts +9 -9
  412. package/dist/testing/duplex.d.ts +5 -5
  413. package/dist/testing/fixtures.d.ts +4 -4
  414. package/dist/testing/index.d.ts +13 -13
  415. package/dist/testing/index.js +7035 -926
  416. package/dist/testing/ioProviderSimulator.d.ts +5 -5
  417. package/dist/testing/providerSimulator.d.ts +5 -5
  418. package/dist/testing/review.d.ts +8 -8
  419. package/dist/testing/sessionBenchmark.d.ts +13 -13
  420. package/dist/testing/stt.d.ts +3 -3
  421. package/dist/testing/telephony.d.ts +27 -2
  422. package/dist/testing/tts.d.ts +2 -2
  423. package/dist/vue/VoiceCallDebuggerLaunch.d.ts +72 -0
  424. package/dist/vue/VoiceCallPlayer.d.ts +40 -0
  425. package/dist/vue/VoiceCostDashboard.d.ts +57 -0
  426. package/dist/vue/VoiceDeliveryRuntime.d.ts +34 -0
  427. package/dist/vue/VoiceLiveAgentConsole.d.ts +50 -0
  428. package/dist/vue/VoiceLiveCallViewer.d.ts +35 -0
  429. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  430. package/dist/vue/VoiceOpsStatus.d.ts +4 -0
  431. package/dist/vue/VoicePlatformCoverage.d.ts +27 -0
  432. package/dist/vue/VoiceProofTrends.d.ts +25 -0
  433. package/dist/vue/VoiceProviderCapabilities.d.ts +55 -0
  434. package/dist/vue/VoiceProviderContracts.d.ts +25 -0
  435. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  436. package/dist/vue/VoiceProviderStatus.d.ts +55 -0
  437. package/dist/vue/VoiceReadinessFailures.d.ts +25 -0
  438. package/dist/vue/VoiceReconnectProfileEvidence.d.ts +25 -0
  439. package/dist/vue/VoiceReplayTimeline.d.ts +17 -0
  440. package/dist/vue/VoiceRoutingStatus.d.ts +4 -0
  441. package/dist/vue/VoiceSessionObservability.d.ts +27 -0
  442. package/dist/vue/VoiceSessionSnapshot.d.ts +72 -0
  443. package/dist/vue/VoiceTurnLatency.d.ts +73 -0
  444. package/dist/vue/VoiceTurnQuality.d.ts +55 -0
  445. package/dist/vue/VoiceWidget.d.ts +77 -0
  446. package/dist/vue/index.d.ts +49 -8
  447. package/dist/vue/index.js +12231 -493
  448. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  449. package/dist/vue/useVoiceCallDebugger.d.ts +10 -0
  450. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  451. package/dist/vue/useVoiceController.d.ts +10 -7
  452. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  453. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  454. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  455. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  456. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  457. package/dist/vue/useVoiceProfileComparison.d.ts +9 -0
  458. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  459. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  460. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  461. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  462. package/dist/vue/useVoiceProviderStatus.d.ts +3 -3
  463. package/dist/vue/useVoiceReadinessFailures.d.ts +959 -0
  464. package/dist/vue/useVoiceReconnectProfileEvidence.d.ts +9 -0
  465. package/dist/vue/useVoiceRoutingStatus.d.ts +2 -2
  466. package/dist/vue/useVoiceSessionObservability.d.ts +9 -0
  467. package/dist/vue/useVoiceSessionSnapshot.d.ts +10 -0
  468. package/dist/vue/useVoiceStream.d.ts +10 -6
  469. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  470. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  471. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  472. package/dist/vue/useVoiceWorkflowStatus.d.ts +3 -3
  473. package/package.json +155 -256
  474. package/dist/angular/voice-app-kit-status.service.d.ts +0 -12
  475. package/dist/angular/voice-ops-status.component.d.ts +0 -15
  476. package/dist/appKit.d.ts +0 -92
  477. package/dist/client/appKitStatus.d.ts +0 -19
  478. package/dist/react/useVoiceAppKitStatus.d.ts +0 -8
  479. package/dist/svelte/createVoiceAppKitStatus.d.ts +0 -8
  480. package/dist/turnProfiles.d.ts +0 -6
  481. package/dist/types.d.ts +0 -968
  482. package/dist/vue/useVoiceAppKitStatus.d.ts +0 -9
  483. package/fixtures/README.md +0 -57
  484. package/fixtures/manifest.json +0 -199
  485. package/fixtures/pcm/dialogue-three-clean.pcm +0 -0
  486. package/fixtures/pcm/dialogue-three-mixed.pcm +0 -0
  487. package/fixtures/pcm/dialogue-two-clean.pcm +0 -0
  488. package/fixtures/pcm/dialogue-two-noisy.pcm +0 -0
  489. package/fixtures/pcm/multiturn-three-mixed.pcm +0 -0
  490. package/fixtures/pcm/multiturn-two-clean.pcm +0 -0
  491. package/fixtures/pcm/quietly-alone-clean.pcm +0 -0
  492. package/fixtures/pcm/rainstorms-noisy.pcm +0 -0
  493. package/fixtures/pcm/stella-bulgaria-bulgarian20.pcm +0 -0
  494. package/fixtures/pcm/stella-ghana-english507.pcm +0 -0
  495. package/fixtures/pcm/stella-india-english37.pcm +0 -0
  496. package/fixtures/pcm/stella-jamaica-jamaican-creole-english1.pcm +0 -0
  497. package/fixtures/pcm/stella-liberia-liberian-pidgin-english2.pcm +0 -0
  498. package/fixtures/pcm/stella-pakistan-english519.pcm +0 -0
  499. package/fixtures/pcm/stella-sierra-leone-krio5.pcm +0 -0
  500. package/fixtures/pcm/stella-singapore-english655.pcm +0 -0
  501. 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";
@@ -101,19 +104,25 @@ var createMicrophoneCapture = (options) => {
101
104
  let processorNode = null;
102
105
  let mediaStream = null;
103
106
  const start = async () => {
104
- if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
107
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
105
108
  throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
106
109
  }
107
110
  const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
108
111
  if (!AudioContextCtor) {
109
112
  throw new Error("Browser microphone capture requires AudioContext support.");
110
113
  }
111
- mediaStream = await navigator.mediaDevices.getUserMedia({
114
+ mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
112
115
  audio: {
113
- channelCount: options.channelCount ?? 1
116
+ autoGainControl: true,
117
+ channelCount: options.channelCount ?? 1,
118
+ echoCancellation: true,
119
+ noiseSuppression: true
114
120
  }
115
121
  });
116
122
  audioContext = new AudioContextCtor;
123
+ if (audioContext.state === "suspended") {
124
+ await audioContext.resume();
125
+ }
117
126
  sourceNode = audioContext.createMediaStreamSource(mediaStream);
118
127
  processorNode = audioContext.createScriptProcessor(4096, 1, 1);
119
128
  processorNode.onaudioprocess = (event) => {
@@ -181,13 +190,25 @@ var serverMessageToAction = (message) => {
181
190
  case "assistant":
182
191
  return {
183
192
  text: message.text,
193
+ turnId: message.turnId,
184
194
  type: "assistant"
185
195
  };
196
+ case "assistant_delta":
197
+ return {
198
+ delta: message.delta,
199
+ turnId: message.turnId,
200
+ type: "assistant_delta"
201
+ };
186
202
  case "complete":
187
203
  return {
188
204
  sessionId: message.sessionId,
189
205
  type: "complete"
190
206
  };
207
+ case "connection":
208
+ return {
209
+ reconnect: message.reconnect,
210
+ type: "connection"
211
+ };
191
212
  case "call_lifecycle":
192
213
  return {
193
214
  event: message.event,
@@ -209,9 +230,22 @@ var serverMessageToAction = (message) => {
209
230
  transcript: message.transcript,
210
231
  type: "partial"
211
232
  };
233
+ case "replay":
234
+ return {
235
+ assistantTexts: message.assistantTexts,
236
+ call: message.call,
237
+ partial: message.partial,
238
+ scenarioId: message.scenarioId,
239
+ sessionId: message.sessionId,
240
+ sessionMetadata: message.sessionMetadata,
241
+ status: message.status,
242
+ turns: message.turns,
243
+ type: "replay"
244
+ };
212
245
  case "session":
213
246
  return {
214
247
  sessionId: message.sessionId,
248
+ sessionMetadata: message.sessionMetadata,
215
249
  scenarioId: message.scenarioId,
216
250
  status: message.status,
217
251
  type: "session"
@@ -226,13 +260,242 @@ var serverMessageToAction = (message) => {
226
260
  }
227
261
  };
228
262
 
263
+ // node_modules/@absolutejs/media/dist/index.js
264
+ var TAU = Math.PI * 2;
265
+ var pushIssue = (issues, severity, code, message) => {
266
+ issues.push({ code, message, severity });
267
+ };
268
+ var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
269
+ var max = (values) => values.length === 0 ? undefined : Math.max(...values);
270
+ var numericStat = (stat, key) => {
271
+ const value = stat[key];
272
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
273
+ };
274
+ var booleanStat = (stat, key) => {
275
+ const value = stat[key];
276
+ return typeof value === "boolean" ? value : undefined;
277
+ };
278
+ var stringStat = (stat, key) => {
279
+ const value = stat[key];
280
+ return typeof value === "string" ? value : undefined;
281
+ };
282
+ var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
283
+ var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
284
+ var normalizeWebRTCStat = (stat) => {
285
+ const sample = {};
286
+ for (const [key, value] of Object.entries(stat)) {
287
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
288
+ sample[key] = value;
289
+ }
290
+ }
291
+ return sample;
292
+ };
293
+ var buildMediaWebRTCStatsReport = (input = {}) => {
294
+ const stats = input.stats ?? [];
295
+ const issues = [];
296
+ const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
297
+ const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
298
+ const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
299
+ const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
300
+ const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
301
+ const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
302
+ const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
303
+ const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
304
+ const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
305
+ const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
306
+ const packetLossDenominator = inboundPackets + packetsLost;
307
+ const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
308
+ const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
309
+ const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
310
+ const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
311
+ const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
312
+ const jitterBufferDelayMs = max(inbound.map((stat) => {
313
+ const delay = numericStat(stat, "jitterBufferDelay");
314
+ const emitted = numericStat(stat, "jitterBufferEmittedCount");
315
+ return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
316
+ }).filter((value) => value !== undefined));
317
+ const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
318
+ if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
319
+ pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
320
+ }
321
+ if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
322
+ pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
323
+ }
324
+ if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
325
+ pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
326
+ }
327
+ if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
328
+ pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
329
+ }
330
+ if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
331
+ pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
332
+ }
333
+ return {
334
+ activeCandidatePairs,
335
+ audioLevelAverage: average(audioLevels),
336
+ bytesReceived,
337
+ bytesSent,
338
+ checkedAt: Date.now(),
339
+ endedAudioTracks,
340
+ inboundPackets,
341
+ issues,
342
+ jitterBufferDelayMs,
343
+ jitterMs,
344
+ liveAudioTracks,
345
+ outboundPackets,
346
+ packetLossRatio,
347
+ packetsLost,
348
+ roundTripTimeMs,
349
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
350
+ totalStats: stats.length
351
+ };
352
+ };
353
+ var collectMediaWebRTCStats = async (input) => {
354
+ const report = await input.peerConnection.getStats(input.selector ?? null);
355
+ return [...report.values()].map(normalizeWebRTCStat);
356
+ };
357
+ var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
358
+ const stats = input.stats ?? [];
359
+ const previousStats = input.previousStats ?? [];
360
+ const issues = [];
361
+ const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
362
+ const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
363
+ const streams = audioRtp.map((stat) => {
364
+ const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
365
+ const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
366
+ const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
367
+ const previous = previousByKey.get(statKey(stat));
368
+ const currentPackets = numericStat(stat, packetsKey);
369
+ const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
370
+ const currentBytes = numericStat(stat, bytesKey);
371
+ const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
372
+ const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
373
+ return {
374
+ bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
375
+ currentPackets,
376
+ direction,
377
+ id: statKey(stat),
378
+ packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
379
+ previousPackets,
380
+ timeDeltaMs
381
+ };
382
+ });
383
+ const inbound = streams.filter((stream) => stream.direction === "inbound");
384
+ const outbound = streams.filter((stream) => stream.direction === "outbound");
385
+ const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
386
+ const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
387
+ const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
388
+ if (input.requireInboundAudio && inbound.length === 0) {
389
+ pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
390
+ }
391
+ if (input.requireOutboundAudio && outbound.length === 0) {
392
+ pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
393
+ }
394
+ if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
395
+ pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
396
+ }
397
+ if (stalledInboundStreams > 0) {
398
+ pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
399
+ }
400
+ if (stalledOutboundStreams > 0) {
401
+ pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
402
+ }
403
+ return {
404
+ checkedAt: Date.now(),
405
+ inboundAudioStreams: inbound.length,
406
+ issues,
407
+ maxObservedGapMs,
408
+ outboundAudioStreams: outbound.length,
409
+ stalledInboundStreams,
410
+ stalledOutboundStreams,
411
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
412
+ streams,
413
+ totalStats: stats.length
414
+ };
415
+ };
416
+
417
+ // src/client/browserMedia.ts
418
+ var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
419
+ var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
420
+ var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
421
+ var postBrowserMediaReport = async (payload, options) => {
422
+ const requestFetch = options.fetch ?? globalThis.fetch;
423
+ if (!requestFetch) {
424
+ return;
425
+ }
426
+ await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
427
+ body: JSON.stringify(payload),
428
+ headers: {
429
+ "Content-Type": "application/json"
430
+ },
431
+ keepalive: true,
432
+ method: "POST"
433
+ });
434
+ };
435
+ var createVoiceBrowserMediaReporter = (options) => {
436
+ let interval = null;
437
+ let previousStats = [];
438
+ const reportOnce = async () => {
439
+ const peerConnection = await resolvePeerConnection(options);
440
+ if (!peerConnection) {
441
+ return;
442
+ }
443
+ const stats = await collectMediaWebRTCStats({ peerConnection });
444
+ const report = buildMediaWebRTCStatsReport({
445
+ ...options,
446
+ stats
447
+ });
448
+ const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
449
+ ...options.continuity,
450
+ previousStats,
451
+ stats
452
+ });
453
+ const payload = {
454
+ at: Date.now(),
455
+ continuity,
456
+ report,
457
+ scenarioId: options.getScenarioId?.() ?? null,
458
+ sessionId: options.getSessionId?.() ?? null
459
+ };
460
+ previousStats = stats;
461
+ options.onReport?.(payload);
462
+ await postBrowserMediaReport(payload, options);
463
+ return payload;
464
+ };
465
+ const run = () => {
466
+ reportOnce().catch((error) => {
467
+ options.onError?.(error);
468
+ });
469
+ };
470
+ const stop = () => {
471
+ if (interval) {
472
+ clearInterval(interval);
473
+ interval = null;
474
+ }
475
+ };
476
+ return {
477
+ close: stop,
478
+ reportOnce,
479
+ stop,
480
+ start: () => {
481
+ if (interval) {
482
+ return;
483
+ }
484
+ run();
485
+ interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
486
+ }
487
+ };
488
+ };
489
+
229
490
  // src/client/connection.ts
230
491
  var WS_OPEN = 1;
231
492
  var WS_CLOSED = 3;
232
493
  var WS_NORMAL_CLOSURE = 1000;
233
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
494
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
234
495
  var DEFAULT_PING_INTERVAL = 30000;
235
- var RECONNECT_DELAY_MS = 500;
496
+ var RECONNECT_BASE_DELAY_MS = 500;
497
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
498
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
236
499
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
237
500
  var noop = () => {};
238
501
  var noopUnsubscribe = () => noop;
@@ -240,13 +503,14 @@ var NOOP_CONNECTION = {
240
503
  callControl: noop,
241
504
  close: noop,
242
505
  endTurn: noop,
506
+ send: noop,
507
+ sendAudio: noop,
508
+ simulateDisconnect: noop,
509
+ subscribe: noopUnsubscribe,
243
510
  getReadyState: () => WS_CLOSED,
244
511
  getScenarioId: () => "",
245
512
  getSessionId: () => "",
246
- send: noop,
247
- sendAudio: noop,
248
- start: () => {},
249
- subscribe: noopUnsubscribe
513
+ start: () => {}
250
514
  };
251
515
  var createSessionId = () => crypto.randomUUID();
252
516
  var buildWsUrl = (path, sessionId, scenarioId) => {
@@ -269,10 +533,12 @@ var isVoiceServerMessage = (value) => {
269
533
  case "assistant":
270
534
  case "call_lifecycle":
271
535
  case "complete":
536
+ case "connection":
272
537
  case "error":
273
538
  case "final":
274
539
  case "partial":
275
540
  case "pong":
541
+ case "replay":
276
542
  case "session":
277
543
  case "turn":
278
544
  return true;
@@ -298,7 +564,9 @@ var createVoiceConnection = (path, options = {}) => {
298
564
  const listeners = new Set;
299
565
  const shouldReconnect = options.reconnect !== false;
300
566
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
567
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
301
568
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
569
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
302
570
  const state = {
303
571
  isConnected: false,
304
572
  pendingMessages: [],
@@ -309,6 +577,9 @@ var createVoiceConnection = (path, options = {}) => {
309
577
  sessionId: options.sessionId ?? createSessionId(),
310
578
  ws: null
311
579
  };
580
+ const emitConnection = (reconnect) => {
581
+ listeners.forEach((listener) => listener(reconnect));
582
+ };
312
583
  const clearTimers = () => {
313
584
  if (state.pingInterval) {
314
585
  clearInterval(state.pingInterval);
@@ -332,20 +603,52 @@ var createVoiceConnection = (path, options = {}) => {
332
603
  };
333
604
  const scheduleReconnect = () => {
334
605
  state.reconnectAttempts += 1;
606
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
607
+ const nextAttemptAt = Date.now() + delayMs;
608
+ emitConnection({
609
+ reconnect: {
610
+ attempts: state.reconnectAttempts,
611
+ lastDisconnectAt: Date.now(),
612
+ maxAttempts: maxReconnectAttempts,
613
+ nextAttemptAt,
614
+ status: "reconnecting"
615
+ },
616
+ type: "connection"
617
+ });
335
618
  state.reconnectTimeout = setTimeout(() => {
336
619
  if (state.reconnectAttempts > maxReconnectAttempts) {
620
+ emitConnection({
621
+ reconnect: {
622
+ attempts: state.reconnectAttempts,
623
+ maxAttempts: maxReconnectAttempts,
624
+ status: "exhausted"
625
+ },
626
+ type: "connection"
627
+ });
337
628
  return;
338
629
  }
339
630
  connect();
340
- }, RECONNECT_DELAY_MS);
631
+ }, delayMs);
341
632
  };
342
633
  const connect = () => {
343
634
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
344
635
  ws.binaryType = "arraybuffer";
345
636
  ws.onopen = () => {
637
+ const wasReconnecting = state.reconnectAttempts > 0;
346
638
  state.isConnected = true;
347
- state.reconnectAttempts = 0;
348
639
  flushPendingMessages();
640
+ if (wasReconnecting) {
641
+ emitConnection({
642
+ reconnect: {
643
+ attempts: state.reconnectAttempts,
644
+ lastResumedAt: Date.now(),
645
+ maxAttempts: maxReconnectAttempts,
646
+ status: "resumed"
647
+ },
648
+ type: "connection"
649
+ });
650
+ state.reconnectAttempts = 0;
651
+ }
349
652
  listeners.forEach((listener) => listener({
350
653
  scenarioId: state.scenarioId ?? undefined,
351
654
  sessionId: state.sessionId,
@@ -375,6 +678,16 @@ var createVoiceConnection = (path, options = {}) => {
375
678
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
376
679
  if (reconnectable) {
377
680
  scheduleReconnect();
681
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
682
+ emitConnection({
683
+ reconnect: {
684
+ attempts: state.reconnectAttempts,
685
+ lastDisconnectAt: Date.now(),
686
+ maxAttempts: maxReconnectAttempts,
687
+ status: "exhausted"
688
+ },
689
+ type: "connection"
690
+ });
378
691
  }
379
692
  };
380
693
  state.ws = ws;
@@ -397,9 +710,9 @@ var createVoiceConnection = (path, options = {}) => {
397
710
  state.scenarioId = input.scenarioId;
398
711
  }
399
712
  send({
400
- type: "start",
713
+ scenarioId: state.scenarioId ?? undefined,
401
714
  sessionId: state.sessionId,
402
- scenarioId: state.scenarioId ?? undefined
715
+ type: "start"
403
716
  });
404
717
  };
405
718
  const sendAudio = (audio) => {
@@ -423,6 +736,11 @@ var createVoiceConnection = (path, options = {}) => {
423
736
  state.isConnected = false;
424
737
  listeners.clear();
425
738
  };
739
+ const simulateDisconnect = () => {
740
+ if (state.ws?.readyState === WS_OPEN) {
741
+ state.ws.close(4000, "absolutejs-voice-reconnect-proof");
742
+ }
743
+ };
426
744
  const subscribe = (callback) => {
427
745
  listeners.add(callback);
428
746
  return () => {
@@ -434,31 +752,62 @@ var createVoiceConnection = (path, options = {}) => {
434
752
  callControl,
435
753
  close,
436
754
  endTurn,
437
- getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
438
- getScenarioId: () => state.scenarioId ?? "",
439
- getSessionId: () => state.sessionId,
440
755
  send,
441
756
  sendAudio,
757
+ simulateDisconnect,
442
758
  start,
443
- subscribe
759
+ subscribe,
760
+ getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
761
+ getScenarioId: () => state.scenarioId ?? "",
762
+ getSessionId: () => state.sessionId
444
763
  };
445
764
  };
446
765
 
447
766
  // src/client/store.ts
767
+ var createInitialReconnectState = () => ({
768
+ attempts: 0,
769
+ maxAttempts: 0,
770
+ status: "idle"
771
+ });
772
+ var appendSegmentText = (accumulated, next) => {
773
+ const nextText = next.trim().replace(/\s+/g, " ");
774
+ if (!nextText)
775
+ return accumulated;
776
+ if (!accumulated)
777
+ return nextText;
778
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
779
+ return accumulated;
780
+ }
781
+ if (nextText.includes(accumulated))
782
+ return nextText;
783
+ return `${accumulated} ${nextText}`;
784
+ };
785
+ var joinPartial = (finalized, interim) => {
786
+ const interimText = interim.trim().replace(/\s+/g, " ");
787
+ if (!finalized)
788
+ return interimText;
789
+ if (!interimText || finalized.endsWith(interimText))
790
+ return finalized;
791
+ return `${finalized} ${interimText}`;
792
+ };
448
793
  var createInitialState = () => ({
449
794
  assistantAudio: [],
795
+ assistantStreamingText: "",
450
796
  assistantTexts: [],
451
797
  call: null,
452
798
  error: null,
453
799
  isConnected: false,
454
- scenarioId: null,
455
800
  partial: "",
801
+ reconnect: createInitialReconnectState(),
802
+ scenarioId: null,
456
803
  sessionId: null,
804
+ sessionMetadata: null,
457
805
  status: "idle",
458
806
  turns: []
459
807
  });
460
808
  var createVoiceStreamStore = () => {
461
809
  let state = createInitialState();
810
+ let turnFinalText = "";
462
811
  const subscribers = new Set;
463
812
  const notify = () => {
464
813
  subscribers.forEach((subscriber) => subscriber());
@@ -482,9 +831,16 @@ var createVoiceStreamStore = () => {
482
831
  case "assistant":
483
832
  state = {
484
833
  ...state,
834
+ assistantStreamingText: "",
485
835
  assistantTexts: [...state.assistantTexts, action.text]
486
836
  };
487
837
  break;
838
+ case "assistant_delta":
839
+ state = {
840
+ ...state,
841
+ assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
842
+ };
843
+ break;
488
844
  case "complete":
489
845
  state = {
490
846
  ...state,
@@ -509,7 +865,19 @@ var createVoiceStreamStore = () => {
509
865
  case "connected":
510
866
  state = {
511
867
  ...state,
512
- isConnected: true
868
+ isConnected: true,
869
+ reconnect: state.reconnect.status === "reconnecting" ? {
870
+ ...state.reconnect,
871
+ lastResumedAt: Date.now(),
872
+ nextAttemptAt: undefined,
873
+ status: "resumed"
874
+ } : state.reconnect
875
+ };
876
+ break;
877
+ case "connection":
878
+ state = {
879
+ ...state,
880
+ reconnect: action.reconnect
513
881
  };
514
882
  break;
515
883
  case "disconnected":
@@ -525,16 +893,39 @@ var createVoiceStreamStore = () => {
525
893
  };
526
894
  break;
527
895
  case "final":
896
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
528
897
  state = {
529
898
  ...state,
530
- partial: action.transcript.text,
531
- turns: state.turns.map((turn) => turn)
899
+ partial: turnFinalText
532
900
  };
533
901
  break;
534
902
  case "partial":
535
903
  state = {
536
904
  ...state,
537
- partial: action.transcript.text
905
+ partial: joinPartial(turnFinalText, action.transcript.text)
906
+ };
907
+ break;
908
+ case "replay":
909
+ turnFinalText = action.partial;
910
+ state = {
911
+ ...state,
912
+ assistantStreamingText: "",
913
+ assistantTexts: [...action.assistantTexts],
914
+ call: action.call ?? null,
915
+ error: null,
916
+ isConnected: action.status === "active",
917
+ partial: action.partial,
918
+ reconnect: state.reconnect.status === "reconnecting" ? {
919
+ ...state.reconnect,
920
+ lastResumedAt: Date.now(),
921
+ nextAttemptAt: undefined,
922
+ status: "resumed"
923
+ } : state.reconnect,
924
+ scenarioId: action.scenarioId ?? state.scenarioId,
925
+ sessionId: action.sessionId,
926
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
927
+ status: action.status,
928
+ turns: [...action.turns]
538
929
  };
539
930
  break;
540
931
  case "session":
@@ -544,10 +935,12 @@ var createVoiceStreamStore = () => {
544
935
  scenarioId: action.scenarioId ?? state.scenarioId,
545
936
  isConnected: action.status === "active",
546
937
  sessionId: action.sessionId,
938
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
547
939
  status: action.status
548
940
  };
549
941
  break;
550
942
  case "turn":
943
+ turnFinalText = "";
551
944
  state = {
552
945
  ...state,
553
946
  partial: "",
@@ -574,29 +967,73 @@ var createVoiceStreamStore = () => {
574
967
  var createVoiceStream = (path, options = {}) => {
575
968
  const connection = createVoiceConnection(path, options);
576
969
  const store = createVoiceStreamStore();
970
+ const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
971
+ ...options.browserMedia,
972
+ getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
973
+ getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
974
+ }) : null;
577
975
  const subscribers = new Set;
578
976
  const start = (input) => Promise.resolve().then(() => {
579
977
  if (!input?.sessionId && !input?.scenarioId) {
580
978
  return;
581
979
  }
582
980
  connection.start(input);
981
+ browserMediaReporter?.start();
583
982
  });
584
983
  const notify = () => {
585
984
  subscribers.forEach((subscriber) => subscriber());
586
985
  };
986
+ const reportReconnect = () => {
987
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
988
+ return;
989
+ }
990
+ const snapshot = store.getSnapshot();
991
+ const body = JSON.stringify({
992
+ at: Date.now(),
993
+ reconnect: snapshot.reconnect,
994
+ scenarioId: snapshot.scenarioId,
995
+ sessionId: connection.getSessionId(),
996
+ turnIds: snapshot.turns.map((turn) => turn.id)
997
+ });
998
+ fetch(options.reconnectReportPath, {
999
+ body,
1000
+ headers: {
1001
+ "Content-Type": "application/json"
1002
+ },
1003
+ keepalive: true,
1004
+ method: "POST"
1005
+ }).catch(() => {});
1006
+ };
587
1007
  const unsubscribeConnection = connection.subscribe((message) => {
588
1008
  const action = serverMessageToAction(message);
589
1009
  if (action) {
590
1010
  store.dispatch(action);
1011
+ if (message.type === "connection") {
1012
+ reportReconnect();
1013
+ }
591
1014
  notify();
592
1015
  }
593
1016
  });
594
1017
  return {
1018
+ start,
1019
+ get assistantAudio() {
1020
+ return store.getSnapshot().assistantAudio;
1021
+ },
1022
+ get assistantTexts() {
1023
+ return store.getSnapshot().assistantTexts;
1024
+ },
1025
+ get assistantStreamingText() {
1026
+ return store.getSnapshot().assistantStreamingText;
1027
+ },
1028
+ get call() {
1029
+ return store.getSnapshot().call;
1030
+ },
595
1031
  callControl(message) {
596
1032
  connection.callControl(message);
597
1033
  },
598
1034
  close() {
599
1035
  unsubscribeConnection();
1036
+ browserMediaReporter?.close();
600
1037
  connection.close();
601
1038
  store.dispatch({ type: "disconnected" });
602
1039
  notify();
@@ -616,44 +1053,43 @@ var createVoiceStream = (path, options = {}) => {
616
1053
  get isConnected() {
617
1054
  return store.getSnapshot().isConnected;
618
1055
  },
619
- get scenarioId() {
620
- return store.getSnapshot().scenarioId;
621
- },
622
- start,
623
1056
  get partial() {
624
1057
  return store.getSnapshot().partial;
625
1058
  },
626
- get sessionId() {
627
- return connection.getSessionId();
1059
+ get reconnect() {
1060
+ return store.getSnapshot().reconnect;
628
1061
  },
629
- get status() {
630
- return store.getSnapshot().status;
1062
+ get scenarioId() {
1063
+ return store.getSnapshot().scenarioId;
631
1064
  },
632
- get turns() {
633
- return store.getSnapshot().turns;
1065
+ sendAudio(audio) {
1066
+ connection.sendAudio(audio);
634
1067
  },
635
- get assistantTexts() {
636
- return store.getSnapshot().assistantTexts;
1068
+ get sessionId() {
1069
+ return connection.getSessionId();
637
1070
  },
638
- get assistantAudio() {
639
- return store.getSnapshot().assistantAudio;
1071
+ get sessionMetadata() {
1072
+ return store.getSnapshot().sessionMetadata;
640
1073
  },
641
- get call() {
642
- return store.getSnapshot().call;
1074
+ simulateDisconnect() {
1075
+ connection.simulateDisconnect();
643
1076
  },
644
- sendAudio(audio) {
645
- connection.sendAudio(audio);
1077
+ get status() {
1078
+ return store.getSnapshot().status;
646
1079
  },
647
1080
  subscribe(subscriber) {
648
1081
  subscribers.add(subscriber);
649
1082
  return () => {
650
1083
  subscribers.delete(subscriber);
651
1084
  };
1085
+ },
1086
+ get turns() {
1087
+ return store.getSnapshot().turns;
652
1088
  }
653
1089
  };
654
1090
  };
655
1091
 
656
- // src/audioConditioning.ts
1092
+ // src/core/audioConditioning.ts
657
1093
  var DEFAULT_TARGET_LEVEL = 0.08;
658
1094
  var DEFAULT_MAX_GAIN = 3;
659
1095
  var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
@@ -671,34 +1107,43 @@ var resolveAudioConditioningConfig = (config) => {
671
1107
  };
672
1108
  };
673
1109
 
674
- // src/turnProfiles.ts
1110
+ // src/core/turnDetection.ts
1111
+ var DEFAULT_SEMANTIC_VETO_RECHECK_MS = 1200;
1112
+
1113
+ // src/core/turnProfiles.ts
675
1114
  var TURN_PROFILE_DEFAULTS = {
676
1115
  balanced: {
677
1116
  qualityProfile: "general",
1117
+ semanticVetoMaxMs: 0,
1118
+ semanticVetoRecheckMs: DEFAULT_SEMANTIC_VETO_RECHECK_MS,
678
1119
  silenceMs: 1400,
679
1120
  speechThreshold: 0.012,
680
1121
  transcriptStabilityMs: 1000
681
1122
  },
682
1123
  fast: {
683
1124
  qualityProfile: "general",
1125
+ semanticVetoMaxMs: 0,
1126
+ semanticVetoRecheckMs: DEFAULT_SEMANTIC_VETO_RECHECK_MS,
684
1127
  silenceMs: 700,
685
1128
  speechThreshold: 0.015,
686
1129
  transcriptStabilityMs: 450
687
1130
  },
688
1131
  "long-form": {
689
1132
  qualityProfile: "general",
1133
+ semanticVetoMaxMs: 0,
1134
+ semanticVetoRecheckMs: DEFAULT_SEMANTIC_VETO_RECHECK_MS,
690
1135
  silenceMs: 2200,
691
1136
  speechThreshold: 0.01,
692
1137
  transcriptStabilityMs: 1500
693
1138
  }
694
1139
  };
695
1140
  var QUALITY_PROFILE_DEFAULTS = {
696
- general: {},
697
1141
  "accent-heavy": {
698
1142
  silenceMs: 1200,
699
1143
  speechThreshold: 0.01,
700
1144
  transcriptStabilityMs: 1200
701
1145
  },
1146
+ general: {},
702
1147
  "noisy-room": {
703
1148
  silenceMs: 2000,
704
1149
  speechThreshold: 0.02,
@@ -720,13 +1165,15 @@ var resolveTurnDetectionConfig = (config) => {
720
1165
  return {
721
1166
  profile,
722
1167
  qualityProfile,
1168
+ semanticVetoMaxMs: config?.semanticVetoMaxMs ?? preset.semanticVetoMaxMs,
1169
+ semanticVetoRecheckMs: config?.semanticVetoRecheckMs ?? preset.semanticVetoRecheckMs,
723
1170
  silenceMs: config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs,
724
1171
  speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
725
1172
  transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
726
1173
  };
727
1174
  };
728
1175
 
729
- // src/presets.ts
1176
+ // src/core/presets.ts
730
1177
  var PRESET_INPUTS = {
731
1178
  chat: {
732
1179
  audioConditioning: {
@@ -747,8 +1194,8 @@ var PRESET_INPUTS = {
747
1194
  },
748
1195
  sttLifecycle: "continuous",
749
1196
  turnDetection: {
750
- qualityProfile: "short-command",
751
- profile: "balanced"
1197
+ profile: "balanced",
1198
+ qualityProfile: "short-command"
752
1199
  }
753
1200
  },
754
1201
  default: {
@@ -763,8 +1210,8 @@ var PRESET_INPUTS = {
763
1210
  },
764
1211
  sttLifecycle: "continuous",
765
1212
  turnDetection: {
766
- qualityProfile: "general",
767
- profile: "fast"
1213
+ profile: "fast",
1214
+ qualityProfile: "general"
768
1215
  }
769
1216
  },
770
1217
  dictation: {
@@ -786,8 +1233,8 @@ var PRESET_INPUTS = {
786
1233
  },
787
1234
  sttLifecycle: "continuous",
788
1235
  turnDetection: {
789
- qualityProfile: "accent-heavy",
790
- profile: "long-form"
1236
+ profile: "long-form",
1237
+ qualityProfile: "accent-heavy"
791
1238
  }
792
1239
  },
793
1240
  "guided-intake": {
@@ -809,8 +1256,8 @@ var PRESET_INPUTS = {
809
1256
  },
810
1257
  sttLifecycle: "turn-scoped",
811
1258
  turnDetection: {
812
- qualityProfile: "accent-heavy",
813
- profile: "long-form"
1259
+ profile: "long-form",
1260
+ qualityProfile: "accent-heavy"
814
1261
  }
815
1262
  },
816
1263
  "noisy-room": {
@@ -832,8 +1279,8 @@ var PRESET_INPUTS = {
832
1279
  },
833
1280
  sttLifecycle: "continuous",
834
1281
  turnDetection: {
835
- qualityProfile: "noisy-room",
836
1282
  profile: "long-form",
1283
+ qualityProfile: "noisy-room",
837
1284
  silenceMs: 2100,
838
1285
  speechThreshold: 0.02,
839
1286
  transcriptStabilityMs: 1650
@@ -858,8 +1305,8 @@ var PRESET_INPUTS = {
858
1305
  },
859
1306
  sttLifecycle: "continuous",
860
1307
  turnDetection: {
861
- qualityProfile: "noisy-room",
862
1308
  profile: "long-form",
1309
+ qualityProfile: "noisy-room",
863
1310
  silenceMs: 660,
864
1311
  speechThreshold: 0.012,
865
1312
  transcriptStabilityMs: 300
@@ -884,8 +1331,8 @@ var PRESET_INPUTS = {
884
1331
  },
885
1332
  sttLifecycle: "continuous",
886
1333
  turnDetection: {
887
- qualityProfile: "noisy-room",
888
1334
  profile: "long-form",
1335
+ qualityProfile: "noisy-room",
889
1336
  silenceMs: 620,
890
1337
  speechThreshold: 0.012,
891
1338
  transcriptStabilityMs: 280
@@ -910,8 +1357,8 @@ var PRESET_INPUTS = {
910
1357
  },
911
1358
  sttLifecycle: "continuous",
912
1359
  turnDetection: {
913
- qualityProfile: "noisy-room",
914
- profile: "long-form"
1360
+ profile: "long-form",
1361
+ qualityProfile: "noisy-room"
915
1362
  }
916
1363
  }
917
1364
  };
@@ -935,14 +1382,17 @@ var resolveVoiceRuntimePreset = (name = "default") => {
935
1382
  // src/client/controller.ts
936
1383
  var createInitialState2 = (stream) => ({
937
1384
  assistantAudio: [...stream.assistantAudio],
1385
+ assistantStreamingText: stream.assistantStreamingText,
938
1386
  assistantTexts: [...stream.assistantTexts],
939
1387
  call: stream.call,
940
1388
  error: stream.error,
941
1389
  isConnected: stream.isConnected,
942
1390
  isRecording: false,
943
1391
  partial: stream.partial,
1392
+ reconnect: stream.reconnect,
944
1393
  recordingError: null,
945
1394
  sessionId: stream.sessionId,
1395
+ sessionMetadata: stream.sessionMetadata,
946
1396
  scenarioId: stream.scenarioId,
947
1397
  status: stream.status,
948
1398
  turns: [...stream.turns]
@@ -965,12 +1415,15 @@ var createVoiceController = (path, options = {}) => {
965
1415
  state = {
966
1416
  ...state,
967
1417
  assistantAudio: [...stream.assistantAudio],
1418
+ assistantStreamingText: stream.assistantStreamingText,
968
1419
  assistantTexts: [...stream.assistantTexts],
969
1420
  call: stream.call,
970
1421
  error: stream.error,
971
1422
  isConnected: stream.isConnected,
972
1423
  partial: stream.partial,
1424
+ reconnect: stream.reconnect,
973
1425
  sessionId: stream.sessionId,
1426
+ sessionMetadata: stream.sessionMetadata,
974
1427
  scenarioId: stream.scenarioId,
975
1428
  status: stream.status,
976
1429
  turns: [...stream.turns]
@@ -994,8 +1447,15 @@ var createVoiceController = (path, options = {}) => {
994
1447
  capture = createMicrophoneCapture({
995
1448
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
1449
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
998
- sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
1450
+ onAudio: (audio) => {
1451
+ if (options.capture?.onAudio) {
1452
+ options.capture.onAudio(audio, stream.sendAudio);
1453
+ return;
1454
+ }
1455
+ stream.sendAudio(audio);
1456
+ },
1457
+ sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
1458
+ ...options.capture?.stream ? { stream: options.capture.stream } : {}
999
1459
  });
1000
1460
  return capture;
1001
1461
  };
@@ -1041,11 +1501,25 @@ var createVoiceController = (path, options = {}) => {
1041
1501
  stream.close();
1042
1502
  };
1043
1503
  return {
1504
+ close,
1505
+ startRecording,
1506
+ stopRecording,
1507
+ get assistantAudio() {
1508
+ return state.assistantAudio;
1509
+ },
1510
+ get assistantTexts() {
1511
+ return state.assistantTexts;
1512
+ },
1513
+ get assistantStreamingText() {
1514
+ return state.assistantStreamingText;
1515
+ },
1044
1516
  bindHTMX(bindingOptions) {
1045
1517
  return bindVoiceHTMX(stream, bindingOptions);
1046
1518
  },
1519
+ get call() {
1520
+ return state.call;
1521
+ },
1047
1522
  callControl: (message) => stream.callControl(message),
1048
- close,
1049
1523
  endTurn: () => stream.endTurn(),
1050
1524
  get error() {
1051
1525
  return state.error;
@@ -1061,21 +1535,26 @@ var createVoiceController = (path, options = {}) => {
1061
1535
  get partial() {
1062
1536
  return state.partial;
1063
1537
  },
1538
+ get reconnect() {
1539
+ return state.reconnect;
1540
+ },
1064
1541
  get recordingError() {
1065
1542
  return state.recordingError;
1066
1543
  },
1544
+ get scenarioId() {
1545
+ return state.scenarioId;
1546
+ },
1067
1547
  sendAudio: (audio) => stream.sendAudio(audio),
1068
1548
  get sessionId() {
1069
1549
  return state.sessionId;
1070
1550
  },
1071
- get scenarioId() {
1072
- return state.scenarioId;
1551
+ get sessionMetadata() {
1552
+ return state.sessionMetadata;
1073
1553
  },
1074
- startRecording,
1554
+ simulateDisconnect: () => stream.simulateDisconnect(),
1075
1555
  get status() {
1076
1556
  return state.status;
1077
1557
  },
1078
- stopRecording,
1079
1558
  subscribe: (subscriber) => {
1080
1559
  subscribers.add(subscriber);
1081
1560
  return () => {
@@ -1091,15 +1570,715 @@ var createVoiceController = (path, options = {}) => {
1091
1570
  },
1092
1571
  get turns() {
1093
1572
  return state.turns;
1573
+ }
1574
+ };
1575
+ };
1576
+
1577
+ // src/client/timeStretch.ts
1578
+ var HOP_MS = 10;
1579
+ var SEEK_MS = 5;
1580
+ var ENERGY_EPSILON = 0.000001;
1581
+ var HALF = 0.5;
1582
+ var MS_PER_SECOND = 1000;
1583
+ var makeHann = (length) => {
1584
+ const weights = new Float32Array(length);
1585
+ for (let index = 0;index < length; index += 1) {
1586
+ weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
1587
+ }
1588
+ return weights;
1589
+ };
1590
+ var correlationScore = (base, start, ref, length) => {
1591
+ let dot = 0;
1592
+ let energy = 0;
1593
+ for (let index = 0;index < length; index += 1) {
1594
+ const sample = base[start + index] ?? 0;
1595
+ dot += sample * (ref[index] ?? 0);
1596
+ energy += sample * sample;
1597
+ }
1598
+ return dot / Math.sqrt(energy + ENERGY_EPSILON);
1599
+ };
1600
+ var overlapAddGrain = (src, off, tail, weights, hop) => {
1601
+ const out = new Float32Array(hop);
1602
+ const nextTail = new Float32Array(hop);
1603
+ for (let index = 0;index < hop; index += 1) {
1604
+ out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
1605
+ nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
1606
+ }
1607
+ return { nextTail, out };
1608
+ };
1609
+ var createTimeStretcher = () => {
1610
+ let sampleRate = 0;
1611
+ let channelCount = 0;
1612
+ let hop = 0;
1613
+ let frameLen = 0;
1614
+ let seek = 0;
1615
+ let weights = new Float32Array(0);
1616
+ let buffers = [];
1617
+ let inputStart = 0;
1618
+ let analysisPos = 0;
1619
+ let olaTail = [];
1620
+ let naturalRef = null;
1621
+ const init = (rate, channels) => {
1622
+ sampleRate = rate;
1623
+ channelCount = channels;
1624
+ hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
1625
+ frameLen = hop * 2;
1626
+ seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
1627
+ weights = makeHann(frameLen);
1628
+ buffers = Array.from({ length: channels }, () => new Float32Array(0));
1629
+ olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
1630
+ inputStart = 0;
1631
+ analysisPos = seek;
1632
+ naturalRef = null;
1633
+ };
1634
+ const reset = () => {
1635
+ buffers = buffers.map(() => new Float32Array(0));
1636
+ olaTail = olaTail.map(() => new Float32Array(hop));
1637
+ inputStart = 0;
1638
+ analysisPos = seek;
1639
+ naturalRef = null;
1640
+ };
1641
+ const append = (input) => {
1642
+ for (let channel = 0;channel < channelCount; channel += 1) {
1643
+ const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
1644
+ const existing = buffers[channel] ?? new Float32Array(0);
1645
+ const merged = new Float32Array(existing.length + incoming.length);
1646
+ merged.set(existing, 0);
1647
+ merged.set(incoming, existing.length);
1648
+ buffers[channel] = merged;
1649
+ }
1650
+ };
1651
+ const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
1652
+ const compact = () => {
1653
+ const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
1654
+ if (keepFrom <= inputStart)
1655
+ return;
1656
+ const drop = keepFrom - inputStart;
1657
+ for (let channel = 0;channel < channelCount; channel += 1) {
1658
+ buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
1659
+ }
1660
+ inputStart = keepFrom;
1661
+ };
1662
+ const bestOffset = (center) => {
1663
+ if (!naturalRef)
1664
+ return 0;
1665
+ const [base] = buffers;
1666
+ if (!base)
1667
+ return 0;
1668
+ let bestDelta = 0;
1669
+ let bestScore = -Infinity;
1670
+ for (let delta = -seek;delta <= seek; delta += 1) {
1671
+ const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
1672
+ if (score <= bestScore)
1673
+ continue;
1674
+ bestScore = score;
1675
+ bestDelta = delta;
1676
+ }
1677
+ return bestDelta;
1678
+ };
1679
+ const process = (input, speed, rate) => {
1680
+ const channels = Math.max(1, input.length);
1681
+ if (sampleRate !== rate || channelCount !== channels)
1682
+ init(rate, channels);
1683
+ append(input);
1684
+ const analysisHop = hop * speed;
1685
+ const segments = Array.from({ length: channelCount }, () => []);
1686
+ const emitGrain = (pos) => {
1687
+ const off = pos - inputStart;
1688
+ for (let channel = 0;channel < channelCount; channel += 1) {
1689
+ const src = buffers[channel];
1690
+ const tail = olaTail[channel];
1691
+ if (!src || !tail)
1692
+ continue;
1693
+ const grain = overlapAddGrain(src, off, tail, weights, hop);
1694
+ olaTail[channel] = grain.nextTail;
1695
+ segments[channel]?.push(grain.out);
1696
+ }
1697
+ };
1698
+ const captureRef = (pos) => {
1699
+ const ref = new Float32Array(frameLen);
1700
+ const refOff = pos + hop - inputStart;
1701
+ const [base] = buffers;
1702
+ if (base)
1703
+ ref.set(base.subarray(refOff, refOff + frameLen));
1704
+ naturalRef = ref;
1705
+ };
1706
+ const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
1707
+ while (canEmit()) {
1708
+ const center = Math.round(analysisPos);
1709
+ const pos = center + bestOffset(center);
1710
+ emitGrain(pos);
1711
+ captureRef(pos);
1712
+ analysisPos += analysisHop;
1713
+ }
1714
+ compact();
1715
+ return segments.map((channelSegments) => {
1716
+ const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
1717
+ const merged = new Float32Array(total);
1718
+ let offset = 0;
1719
+ for (const seg of channelSegments) {
1720
+ merged.set(seg, offset);
1721
+ offset += seg.length;
1722
+ }
1723
+ return merged;
1724
+ });
1725
+ };
1726
+ return { process, reset };
1727
+ };
1728
+
1729
+ // src/client/audioPlayer.ts
1730
+ var DEFAULT_LOOKAHEAD_MS = 15;
1731
+ var DEFAULT_VOLUME = 1;
1732
+ var DEFAULT_PLAYBACK_RATE = 1;
1733
+ var MIN_PLAYBACK_RATE = 0.5;
1734
+ var MAX_PLAYBACK_RATE = 2;
1735
+ var STRETCH_BYPASS_EPSILON = 0.01;
1736
+ var ANALYSER_FFT_SIZE = 256;
1737
+ var PCM_BYTE_MIDPOINT = 128;
1738
+ var createInitialState3 = () => ({
1739
+ activeSourceCount: 0,
1740
+ error: null,
1741
+ isActive: false,
1742
+ isPlaying: false,
1743
+ lastInterruptLatencyMs: undefined,
1744
+ lastPlaybackStopLatencyMs: undefined,
1745
+ processedChunkCount: 0,
1746
+ queuedChunkCount: 0
1747
+ });
1748
+ var getAudioContextCtor = () => {
1749
+ if (typeof window === "undefined") {
1750
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1751
+ }
1752
+ return window.AudioContext ?? window.webkitAudioContext;
1753
+ };
1754
+ var clampVolume = (volume) => {
1755
+ if (typeof volume !== "number" || !Number.isFinite(volume)) {
1756
+ return DEFAULT_VOLUME;
1757
+ }
1758
+ return Math.min(1, Math.max(0, volume));
1759
+ };
1760
+ var clampPlaybackRate = (rate) => {
1761
+ if (typeof rate !== "number" || !Number.isFinite(rate)) {
1762
+ return DEFAULT_PLAYBACK_RATE;
1763
+ }
1764
+ return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
1765
+ };
1766
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1767
+ const { format } = chunk;
1768
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1769
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1770
+ }
1771
+ const bytes = chunk.chunk;
1772
+ const channels = Math.max(1, format.channels);
1773
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1774
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1775
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1776
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1777
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1778
+ const channelData = audioBuffer.getChannelData(channelIndex);
1779
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1780
+ const sampleIndex = frameIndex * channels + channelIndex;
1781
+ const sampleOffset = sampleIndex * 2;
1782
+ if (sampleOffset + 1 >= bytes.byteLength) {
1783
+ channelData[frameIndex] = 0;
1784
+ continue;
1785
+ }
1786
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1787
+ }
1788
+ }
1789
+ return audioBuffer;
1790
+ };
1791
+ var createVoiceAudioPlayer = (source, options = {}) => {
1792
+ const subscribers = new Set;
1793
+ const sourceNodes = new Set;
1794
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1795
+ let state = createInitialState3();
1796
+ let audioContext = null;
1797
+ let outputNode = null;
1798
+ let analyserNode = null;
1799
+ let analyserBuffer = null;
1800
+ let volume = clampVolume(options.volume);
1801
+ let playbackRate = clampPlaybackRate(options.playbackRate);
1802
+ let stretcher = null;
1803
+ let queueEndTime = 0;
1804
+ let syncPromise = Promise.resolve();
1805
+ let interruptStartedAt = null;
1806
+ let interruptPromise = null;
1807
+ let resolveInterruptPromise = null;
1808
+ let interruptFallbackTimer = null;
1809
+ const notify = () => {
1810
+ for (const subscriber of subscribers) {
1811
+ subscriber();
1812
+ }
1813
+ };
1814
+ const setState = (next) => {
1815
+ state = {
1816
+ ...state,
1817
+ ...next
1818
+ };
1819
+ notify();
1820
+ };
1821
+ const clearError = () => {
1822
+ if (state.error !== null) {
1823
+ setState({ error: null });
1824
+ }
1825
+ };
1826
+ const clearInterruptTimer = () => {
1827
+ if (interruptFallbackTimer !== null) {
1828
+ clearTimeout(interruptFallbackTimer);
1829
+ interruptFallbackTimer = null;
1830
+ }
1831
+ };
1832
+ const resolveInterrupt = (latencyMs) => {
1833
+ clearInterruptTimer();
1834
+ interruptStartedAt = null;
1835
+ stretcher?.reset();
1836
+ setState({
1837
+ activeSourceCount: sourceNodes.size,
1838
+ isPlaying: false,
1839
+ lastInterruptLatencyMs: latencyMs,
1840
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1841
+ });
1842
+ resolveInterruptPromise?.();
1843
+ resolveInterruptPromise = null;
1844
+ interruptPromise = null;
1845
+ };
1846
+ const estimateOutputStopLatencyMs = (context) => {
1847
+ if (!context) {
1848
+ return 0;
1849
+ }
1850
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1851
+ };
1852
+ const applyOutputGain = (context) => {
1853
+ if (!outputNode) {
1854
+ return;
1855
+ }
1856
+ const gainValue = volume;
1857
+ if (outputNode.gain.setValueAtTime) {
1858
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1859
+ return;
1860
+ }
1861
+ outputNode.gain.value = gainValue;
1862
+ };
1863
+ const muteOutputGain = (context) => {
1864
+ if (!outputNode) {
1865
+ return;
1866
+ }
1867
+ const gainValue = 0;
1868
+ if (outputNode.gain.setValueAtTime) {
1869
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1870
+ return;
1871
+ }
1872
+ outputNode.gain.value = gainValue;
1873
+ };
1874
+ const maybeResolveInterrupt = () => {
1875
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1876
+ return;
1877
+ }
1878
+ resolveInterrupt(Date.now() - interruptStartedAt);
1879
+ };
1880
+ const ensureAudioContext = async () => {
1881
+ if (audioContext) {
1882
+ return audioContext;
1883
+ }
1884
+ if (options.createAudioContext) {
1885
+ audioContext = options.createAudioContext();
1886
+ } else {
1887
+ const AudioContextCtor = getAudioContextCtor();
1888
+ if (!AudioContextCtor) {
1889
+ throw new Error("Assistant audio playback requires AudioContext support.");
1890
+ }
1891
+ audioContext = new AudioContextCtor;
1892
+ }
1893
+ if (audioContext.createGain) {
1894
+ outputNode = audioContext.createGain();
1895
+ outputNode.connect?.(audioContext.destination);
1896
+ if (audioContext.createAnalyser) {
1897
+ analyserNode = audioContext.createAnalyser();
1898
+ analyserNode.fftSize = ANALYSER_FFT_SIZE;
1899
+ analyserBuffer = new Uint8Array(analyserNode.fftSize);
1900
+ outputNode.connect?.(analyserNode);
1901
+ }
1902
+ }
1903
+ queueEndTime = audioContext.currentTime;
1904
+ return audioContext;
1905
+ };
1906
+ const scheduleBuffer = (context, buffer, rate) => {
1907
+ const node = context.createBufferSource();
1908
+ node.buffer = buffer;
1909
+ if (node.playbackRate) {
1910
+ node.playbackRate.value = rate;
1911
+ }
1912
+ node.connect(outputNode ?? context.destination);
1913
+ node.onended = () => {
1914
+ sourceNodes.delete(node);
1915
+ node.disconnect?.();
1916
+ setState({
1917
+ activeSourceCount: sourceNodes.size,
1918
+ isPlaying: sourceNodes.size > 0 && state.isActive
1919
+ });
1920
+ maybeResolveInterrupt();
1921
+ };
1922
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1923
+ queueEndTime = startAt + buffer.duration / rate;
1924
+ sourceNodes.add(node);
1925
+ setState({
1926
+ activeSourceCount: sourceNodes.size,
1927
+ isPlaying: true
1928
+ });
1929
+ node.start(startAt);
1930
+ };
1931
+ const scheduleChunk = async (chunk) => {
1932
+ const context = await ensureAudioContext();
1933
+ const buffer = decodePCM16LEChunk(context, chunk);
1934
+ if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
1935
+ stretcher?.reset();
1936
+ scheduleBuffer(context, buffer, playbackRate);
1937
+ return;
1938
+ }
1939
+ const channels = Math.max(1, chunk.format.channels);
1940
+ const input = [];
1941
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1942
+ input.push(buffer.getChannelData(channelIndex));
1943
+ }
1944
+ stretcher ??= createTimeStretcher();
1945
+ const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
1946
+ const outLength = stretched[0]?.length ?? 0;
1947
+ if (outLength === 0) {
1948
+ return;
1949
+ }
1950
+ const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
1951
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1952
+ const channelOut = stretched[channelIndex];
1953
+ if (!channelOut)
1954
+ continue;
1955
+ outBuffer.getChannelData(channelIndex).set(channelOut);
1956
+ }
1957
+ scheduleBuffer(context, outBuffer, 1);
1958
+ };
1959
+ const stopQueuedPlayback = (options2) => {
1960
+ for (const node of [...sourceNodes]) {
1961
+ node.stop?.();
1962
+ }
1963
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1964
+ if (options2?.forceClear) {
1965
+ for (const node of sourceNodes) {
1966
+ node.disconnect?.();
1967
+ }
1968
+ sourceNodes.clear();
1969
+ maybeResolveInterrupt();
1970
+ }
1971
+ };
1972
+ const sync = async () => {
1973
+ if (!state.isActive) {
1974
+ return;
1975
+ }
1976
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1977
+ if (nextChunks.length === 0) {
1978
+ return;
1979
+ }
1980
+ try {
1981
+ clearError();
1982
+ for (const chunk of nextChunks) {
1983
+ await scheduleChunk(chunk);
1984
+ }
1985
+ setState({
1986
+ processedChunkCount: source.assistantAudio.length,
1987
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1988
+ });
1989
+ } catch (error) {
1990
+ setState({
1991
+ error: error instanceof Error ? error.message : String(error)
1992
+ });
1993
+ }
1994
+ };
1995
+ const queueSync = () => {
1996
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1997
+ return syncPromise;
1998
+ };
1999
+ const unsubscribeSource = source.subscribe(() => {
2000
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
2001
+ player.start();
2002
+ return;
2003
+ }
2004
+ if (state.isActive) {
2005
+ queueSync();
2006
+ }
2007
+ });
2008
+ const player = {
2009
+ get activeSourceCount() {
2010
+ return state.activeSourceCount;
1094
2011
  },
1095
- get assistantTexts() {
1096
- return state.assistantTexts;
2012
+ close: async () => {
2013
+ unsubscribeSource();
2014
+ stopQueuedPlayback({ forceClear: true });
2015
+ clearInterruptTimer();
2016
+ resolveInterruptPromise?.();
2017
+ resolveInterruptPromise = null;
2018
+ interruptPromise = null;
2019
+ interruptStartedAt = null;
2020
+ if (audioContext && audioContext.state !== "closed") {
2021
+ await audioContext.close();
2022
+ }
2023
+ audioContext = null;
2024
+ outputNode?.disconnect?.();
2025
+ outputNode = null;
2026
+ analyserNode?.disconnect?.();
2027
+ analyserNode = null;
2028
+ analyserBuffer = null;
2029
+ queueEndTime = 0;
2030
+ setState({
2031
+ activeSourceCount: 0,
2032
+ isActive: false,
2033
+ isPlaying: false
2034
+ });
1097
2035
  },
1098
- get assistantAudio() {
1099
- return state.assistantAudio;
2036
+ get error() {
2037
+ return state.error;
1100
2038
  },
1101
- get call() {
1102
- return state.call;
2039
+ getOutputLevel: () => {
2040
+ if (!analyserNode || !analyserBuffer) {
2041
+ return 0;
2042
+ }
2043
+ analyserNode.getByteTimeDomainData(analyserBuffer);
2044
+ let sumSquares = 0;
2045
+ for (const sample of analyserBuffer) {
2046
+ const centered = (sample - PCM_BYTE_MIDPOINT) / PCM_BYTE_MIDPOINT;
2047
+ sumSquares += centered * centered;
2048
+ }
2049
+ return Math.sqrt(sumSquares / analyserBuffer.length);
2050
+ },
2051
+ getSnapshot: () => state,
2052
+ interrupt: async () => {
2053
+ const startedAt = Date.now();
2054
+ const context = await ensureAudioContext();
2055
+ interruptStartedAt = startedAt;
2056
+ muteOutputGain(context);
2057
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
2058
+ setState({
2059
+ isActive: false,
2060
+ isPlaying: sourceNodes.size > 0,
2061
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
2062
+ });
2063
+ if (sourceNodes.size === 0) {
2064
+ resolveInterrupt(playbackStopLatencyMs);
2065
+ return;
2066
+ }
2067
+ if (!interruptPromise) {
2068
+ interruptPromise = new Promise((resolve) => {
2069
+ resolveInterruptPromise = resolve;
2070
+ });
2071
+ }
2072
+ clearInterruptTimer();
2073
+ interruptFallbackTimer = setTimeout(() => {
2074
+ for (const node of sourceNodes) {
2075
+ node.disconnect?.();
2076
+ }
2077
+ sourceNodes.clear();
2078
+ resolveInterrupt(Date.now() - startedAt);
2079
+ }, 250);
2080
+ stopQueuedPlayback();
2081
+ await interruptPromise;
2082
+ },
2083
+ get isActive() {
2084
+ return state.isActive;
2085
+ },
2086
+ get isPlaying() {
2087
+ return state.isPlaying;
2088
+ },
2089
+ get lastInterruptLatencyMs() {
2090
+ return state.lastInterruptLatencyMs;
2091
+ },
2092
+ get lastPlaybackStopLatencyMs() {
2093
+ return state.lastPlaybackStopLatencyMs;
2094
+ },
2095
+ pause: async () => {
2096
+ if (!audioContext) {
2097
+ setState({
2098
+ activeSourceCount: 0,
2099
+ isActive: false,
2100
+ isPlaying: false
2101
+ });
2102
+ return;
2103
+ }
2104
+ await audioContext.suspend();
2105
+ setState({
2106
+ activeSourceCount: sourceNodes.size,
2107
+ isActive: false,
2108
+ isPlaying: false
2109
+ });
2110
+ },
2111
+ get playbackRate() {
2112
+ return playbackRate;
2113
+ },
2114
+ get processedChunkCount() {
2115
+ return state.processedChunkCount;
2116
+ },
2117
+ get queuedChunkCount() {
2118
+ return state.queuedChunkCount;
2119
+ },
2120
+ setPlaybackRate: (nextRate) => {
2121
+ playbackRate = clampPlaybackRate(nextRate);
2122
+ },
2123
+ setVolume: (nextVolume) => {
2124
+ volume = clampVolume(nextVolume);
2125
+ applyOutputGain(audioContext);
2126
+ },
2127
+ start: async () => {
2128
+ try {
2129
+ clearError();
2130
+ const context = await ensureAudioContext();
2131
+ applyOutputGain(context);
2132
+ if (context.state === "suspended") {
2133
+ await context.resume();
2134
+ }
2135
+ setState({
2136
+ activeSourceCount: sourceNodes.size,
2137
+ isActive: true,
2138
+ isPlaying: context.state === "running"
2139
+ });
2140
+ await queueSync();
2141
+ } catch (error) {
2142
+ setState({
2143
+ error: error instanceof Error ? error.message : String(error),
2144
+ isActive: false,
2145
+ isPlaying: false
2146
+ });
2147
+ throw error;
2148
+ }
2149
+ },
2150
+ subscribe: (subscriber) => {
2151
+ subscribers.add(subscriber);
2152
+ return () => {
2153
+ subscribers.delete(subscriber);
2154
+ };
2155
+ },
2156
+ get volume() {
2157
+ return volume;
2158
+ }
2159
+ };
2160
+ return player;
2161
+ };
2162
+
2163
+ // src/client/bargeInMonitor.ts
2164
+ var DEFAULT_THRESHOLD_MS = 250;
2165
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
2166
+ var summarize = (events, thresholdMs) => {
2167
+ const stopped = events.filter((event) => event.status === "stopped");
2168
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
2169
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
2170
+ const passed = stopped.length - failed;
2171
+ return {
2172
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
2173
+ events: [...events],
2174
+ failed,
2175
+ lastEvent: events.at(-1),
2176
+ passed,
2177
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
2178
+ thresholdMs,
2179
+ total: stopped.length
2180
+ };
2181
+ };
2182
+ var createVoiceBargeInMonitor = (options = {}) => {
2183
+ const listeners = new Set;
2184
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
2185
+ const fetchImpl = options.fetch ?? globalThis.fetch;
2186
+ const events = [];
2187
+ const emit = () => {
2188
+ for (const listener of listeners) {
2189
+ listener();
2190
+ }
2191
+ };
2192
+ const postEvent = (event) => {
2193
+ if (!options.path || typeof fetchImpl !== "function") {
2194
+ return;
2195
+ }
2196
+ fetchImpl(options.path, {
2197
+ body: JSON.stringify(event),
2198
+ headers: {
2199
+ "Content-Type": "application/json"
2200
+ },
2201
+ method: "POST"
2202
+ }).catch(() => {});
2203
+ };
2204
+ const record = (status, input) => {
2205
+ const event = {
2206
+ at: Date.now(),
2207
+ id: createEventId(),
2208
+ latencyMs: input.latencyMs,
2209
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
2210
+ reason: input.reason,
2211
+ sessionId: input.sessionId,
2212
+ status,
2213
+ thresholdMs
2214
+ };
2215
+ events.push(event);
2216
+ postEvent(event);
2217
+ emit();
2218
+ return event;
2219
+ };
2220
+ return {
2221
+ getSnapshot: () => summarize(events, thresholdMs),
2222
+ recordRequested: (input) => record("requested", input),
2223
+ recordSkipped: (input) => record("skipped", input),
2224
+ recordStopped: (input) => record("stopped", input),
2225
+ subscribe: (subscriber) => {
2226
+ listeners.add(subscriber);
2227
+ return () => {
2228
+ listeners.delete(subscriber);
2229
+ };
2230
+ }
2231
+ };
2232
+ };
2233
+
2234
+ // src/client/duplex.ts
2235
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
2236
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
2237
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
2238
+ let lastPartial = controller.partial;
2239
+ const interruptIfPlaying = (reason) => {
2240
+ if (!player.isPlaying || options.enabled === false) {
2241
+ options.monitor?.recordSkipped({
2242
+ reason,
2243
+ sessionId: controller.sessionId
2244
+ });
2245
+ return;
2246
+ }
2247
+ options.monitor?.recordRequested({
2248
+ reason,
2249
+ sessionId: controller.sessionId
2250
+ });
2251
+ player.interrupt().then(() => {
2252
+ options.monitor?.recordStopped({
2253
+ latencyMs: player.lastInterruptLatencyMs,
2254
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
2255
+ reason,
2256
+ sessionId: controller.sessionId
2257
+ });
2258
+ });
2259
+ };
2260
+ const unsubscribe = controller.subscribe(() => {
2261
+ if (options.interruptOnPartial === false) {
2262
+ lastPartial = controller.partial;
2263
+ return;
2264
+ }
2265
+ if (!lastPartial && controller.partial) {
2266
+ interruptIfPlaying("partial-transcript");
2267
+ }
2268
+ lastPartial = controller.partial;
2269
+ });
2270
+ return {
2271
+ close: () => {
2272
+ unsubscribe();
2273
+ },
2274
+ handleLevel: (level) => {
2275
+ if (shouldInterruptForLevel(level, options)) {
2276
+ interruptIfPlaying("input-level");
2277
+ }
2278
+ },
2279
+ sendAudio: (audio) => {
2280
+ interruptIfPlaying("manual-audio");
2281
+ controller.sendAudio(audio);
1103
2282
  }
1104
2283
  };
1105
2284
  };
@@ -1126,8 +2305,7 @@ var DEFAULT_GUIDED_PROMPTS = [
1126
2305
  "Now describe what you are trying to do or test.",
1127
2306
  "Finish with any detail that feels blocked, risky, or unclear."
1128
2307
  ];
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;");
2308
+ var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
1131
2309
  var readErrorField = (value, key) => {
1132
2310
  const candidate = value[key];
1133
2311
  if (typeof candidate === "string" && candidate.trim()) {
@@ -1160,6 +2338,17 @@ var formatErrorMessage = (error) => {
1160
2338
  }
1161
2339
  return "Unexpected error";
1162
2340
  };
2341
+ var formatReconnectState = (reconnect) => {
2342
+ const pieces = [reconnect.status];
2343
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
2344
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
2345
+ }
2346
+ if (reconnect.nextAttemptAt) {
2347
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
2348
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
2349
+ }
2350
+ return pieces.join(" · ");
2351
+ };
1163
2352
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1164
2353
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1165
2354
  const next = levels.slice(-(count - 1));
@@ -1216,6 +2405,20 @@ var parsePromptList = (value) => {
1216
2405
  } catch {}
1217
2406
  return DEFAULT_GUIDED_PROMPTS;
1218
2407
  };
2408
+ var parseOptionalNumber = (value) => {
2409
+ if (!value) {
2410
+ return;
2411
+ }
2412
+ const parsed = Number(value);
2413
+ return Number.isFinite(parsed) ? parsed : undefined;
2414
+ };
2415
+ var resolveElement2 = (root, selector, ctor) => {
2416
+ if (!selector) {
2417
+ return null;
2418
+ }
2419
+ const value = document.querySelector(selector);
2420
+ return value instanceof ctor ? value : null;
2421
+ };
1219
2422
  var requireElement = (root, selector, ctor, name) => {
1220
2423
  const value = selector ? document.querySelector(selector) : null;
1221
2424
  if (value instanceof ctor) {
@@ -1266,11 +2469,20 @@ var initVoiceHTMXRoot = (root) => {
1266
2469
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
2470
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
2471
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
2472
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
2473
+ const bargeInPath = root.dataset.voiceBargeInPath;
2474
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
2475
+ path: bargeInPath,
2476
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
2477
+ }) : null;
2478
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
2479
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
2480
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
2481
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
2482
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1272
2483
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1273
2484
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
2485
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1274
2486
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1275
2487
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1276
2488
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1279,35 +2491,70 @@ var initVoiceHTMXRoot = (root) => {
1279
2491
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
2492
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
2493
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
2494
+ let activeMode = null;
2495
+ let hasStartedModes = {
2496
+ general: false,
2497
+ guided: false
2498
+ };
2499
+ let isCapturing = false;
2500
+ let micError = null;
2501
+ let waveLevels = createInitialVoiceWaveLevels();
2502
+ let guidedBargeInBinding = null;
2503
+ let generalBargeInBinding = null;
1282
2504
  const guidedVoice = createVoiceController(guidedPath, {
1283
2505
  capture: {
2506
+ onAudio: (audio, sendAudio) => {
2507
+ if (guidedBargeInBinding) {
2508
+ guidedBargeInBinding.sendAudio(audio);
2509
+ return;
2510
+ }
2511
+ sendAudio(audio);
2512
+ },
1284
2513
  onLevel: (level) => {
2514
+ guidedBargeInBinding?.handleLevel(level);
1285
2515
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
2516
  renderWave();
1287
2517
  }
1288
2518
  },
2519
+ connection: {
2520
+ reconnectReportPath
2521
+ },
1289
2522
  preset: "guided-intake"
1290
2523
  });
1291
2524
  const generalVoice = createVoiceController(generalPath, {
1292
2525
  capture: {
2526
+ onAudio: (audio, sendAudio) => {
2527
+ if (generalBargeInBinding) {
2528
+ generalBargeInBinding.sendAudio(audio);
2529
+ return;
2530
+ }
2531
+ sendAudio(audio);
2532
+ },
1293
2533
  onLevel: (level) => {
2534
+ generalBargeInBinding?.handleLevel(level);
1294
2535
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
2536
  renderWave();
1296
2537
  }
1297
2538
  },
2539
+ connection: {
2540
+ reconnectReportPath
2541
+ },
1298
2542
  preset: "dictation"
1299
2543
  });
1300
2544
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
2545
  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();
2546
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
2547
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
2548
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
2549
+ interruptThreshold: bargeInSpeechThreshold,
2550
+ monitor: bargeInMonitor ?? undefined
2551
+ });
2552
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
2553
+ interruptThreshold: bargeInSpeechThreshold,
2554
+ monitor: bargeInMonitor ?? undefined
2555
+ });
1310
2556
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
2557
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1311
2558
  const renderWave = () => {
1312
2559
  const path = createVoiceWavePath(waveLevels);
1313
2560
  voiceWaveGlow.setAttribute("d", path);
@@ -1319,9 +2566,12 @@ var initVoiceHTMXRoot = (root) => {
1319
2566
  const render = () => {
1320
2567
  const voice = currentVoice();
1321
2568
  const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
1322
- const status = voice.status;
2569
+ const { status } = voice;
1323
2570
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1324
2571
  errorStatus.textContent = micError || voice.error || "None";
2572
+ if (reconnectStatus) {
2573
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2574
+ }
1325
2575
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1326
2576
  promptStatus.textContent = resolvePromptMessage({
1327
2577
  guidedPrompts,
@@ -1385,8 +2635,18 @@ var initVoiceHTMXRoot = (root) => {
1385
2635
  render();
1386
2636
  }
1387
2637
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
2638
+ guidedVoice.subscribe(() => {
2639
+ if (guidedVoice.assistantAudio.length > 0) {
2640
+ guidedAudioPlayer.start().catch(() => {});
2641
+ }
2642
+ render();
2643
+ });
2644
+ generalVoice.subscribe(() => {
2645
+ if (generalVoice.assistantAudio.length > 0) {
2646
+ generalAudioPlayer.start().catch(() => {});
2647
+ }
2648
+ render();
2649
+ });
1390
2650
  startGuidedButton.addEventListener("click", () => {
1391
2651
  startMode("guided");
1392
2652
  });
@@ -1396,9 +2656,16 @@ var initVoiceHTMXRoot = (root) => {
1396
2656
  stopButton.addEventListener("click", () => {
1397
2657
  stopMic();
1398
2658
  });
2659
+ root.addEventListener("absolute-voice-simulate-disconnect", () => {
2660
+ currentVoice().simulateDisconnect();
2661
+ });
1399
2662
  window.addEventListener("beforeunload", () => {
1400
2663
  guidedVoice.stopRecording();
1401
2664
  generalVoice.stopRecording();
2665
+ guidedBargeInBinding?.close();
2666
+ generalBargeInBinding?.close();
2667
+ guidedAudioPlayer.close();
2668
+ generalAudioPlayer.close();
1402
2669
  stopGuidedBinding();
1403
2670
  stopGeneralBinding();
1404
2671
  guidedVoice.close();