@absolutejs/voice 0.0.22-beta.6 → 0.0.22-beta.600

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 (495) hide show
  1. package/LICENSE +88 -0
  2. package/README.md +4518 -951
  3. package/dist/angular/index.d.ts +36 -2
  4. package/dist/angular/index.js +4610 -320
  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 +12 -0
  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 +11 -0
  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 +7 -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 +12 -0
  35. package/dist/client/actions.d.ts +150 -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 +9 -1
  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 +1383 -80
  58. package/dist/client/htmxDashboardRenderers.d.ts +71 -0
  59. package/dist/client/index.d.ts +107 -7
  60. package/dist/client/index.js +11063 -103
  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 +40 -0
  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 +21 -0
  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 +21 -0
  96. package/dist/client/routingStatusWidget.d.ts +32 -0
  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 +21 -0
  111. package/dist/{agent.d.ts → core/agent.d.ts} +109 -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} +21 -14
  120. package/dist/core/assistantExperiment.d.ts +42 -0
  121. package/dist/core/assistantHealth.d.ts +81 -0
  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/core/diagnosticsRoutes.d.ts +44 -0
  163. package/dist/core/dncRegistry.d.ts +38 -0
  164. package/dist/core/dtmfCollector.d.ts +37 -0
  165. package/dist/core/evalRoutes.d.ts +219 -0
  166. package/dist/{fileStore.d.ts → core/fileStore.d.ts} +34 -20
  167. package/dist/core/guardrails.d.ts +128 -0
  168. package/dist/core/handoff.d.ts +54 -0
  169. package/dist/core/handoffHealth.d.ts +94 -0
  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/core/modelAdapters.d.ts +159 -0
  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/core/opsConsoleRoutes.d.ts +80 -0
  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/core/opsWebhook.d.ts +126 -0
  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/core/providerAdapters.d.ts +48 -0
  234. package/dist/core/providerCapabilities.d.ts +92 -0
  235. package/dist/core/providerDecisionTraces.d.ts +130 -0
  236. package/dist/core/providerHealth.d.ts +89 -0
  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/core/qualityRoutes.d.ts +76 -0
  244. package/dist/{queue.d.ts → core/queue.d.ts} +90 -29
  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/core/resilienceRoutes.d.ts +146 -0
  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 +37 -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/core/sessionReplay.d.ts +187 -0
  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 +1504 -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/core/workflowContract.d.ts +91 -0
  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 +1775 -0
  309. package/dist/embed/voice-widget.js +10 -0
  310. package/dist/index.d.ts +376 -46
  311. package/dist/index.js +47984 -4451
  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 +6 -0
  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 +6 -0
  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 -2
  343. package/dist/react/index.js +13488 -252
  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 +7 -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 +8 -0
  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 +8 -0
  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 +7 -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 +8 -0
  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 +9 -0
  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 +10 -0
  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 +10 -0
  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 +8 -0
  400. package/dist/svelte/index.d.ts +37 -2
  401. package/dist/svelte/index.js +7013 -222
  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 -11
  415. package/dist/testing/index.js +10720 -2970
  416. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  417. package/dist/testing/providerSimulator.d.ts +44 -0
  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 +34 -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 +55 -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 -2
  447. package/dist/vue/index.js +13420 -958
  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 +9 -0
  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 +8 -0
  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 +11 -5
  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 +9 -0
  473. package/package.json +155 -256
  474. package/dist/modelAdapters.d.ts +0 -39
  475. package/dist/turnProfiles.d.ts +0 -6
  476. package/dist/types.d.ts +0 -886
  477. package/fixtures/README.md +0 -57
  478. package/fixtures/manifest.json +0 -199
  479. package/fixtures/pcm/dialogue-three-clean.pcm +0 -0
  480. package/fixtures/pcm/dialogue-three-mixed.pcm +0 -0
  481. package/fixtures/pcm/dialogue-two-clean.pcm +0 -0
  482. package/fixtures/pcm/dialogue-two-noisy.pcm +0 -0
  483. package/fixtures/pcm/multiturn-three-mixed.pcm +0 -0
  484. package/fixtures/pcm/multiturn-two-clean.pcm +0 -0
  485. package/fixtures/pcm/quietly-alone-clean.pcm +0 -0
  486. package/fixtures/pcm/rainstorms-noisy.pcm +0 -0
  487. package/fixtures/pcm/stella-bulgaria-bulgarian20.pcm +0 -0
  488. package/fixtures/pcm/stella-ghana-english507.pcm +0 -0
  489. package/fixtures/pcm/stella-india-english37.pcm +0 -0
  490. package/fixtures/pcm/stella-jamaica-jamaican-creole-english1.pcm +0 -0
  491. package/fixtures/pcm/stella-liberia-liberian-pidgin-english2.pcm +0 -0
  492. package/fixtures/pcm/stella-pakistan-english519.pcm +0 -0
  493. package/fixtures/pcm/stella-sierra-leone-krio5.pcm +0 -0
  494. package/fixtures/pcm/stella-singapore-english655.pcm +0 -0
  495. 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,31 @@ 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
+ };
212
+ case "call_lifecycle":
213
+ return {
214
+ event: message.event,
215
+ sessionId: message.sessionId,
216
+ type: "call_lifecycle"
217
+ };
191
218
  case "error":
192
219
  return {
193
220
  message: normalizeErrorMessage(message.message),
@@ -203,9 +230,22 @@ var serverMessageToAction = (message) => {
203
230
  transcript: message.transcript,
204
231
  type: "partial"
205
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
+ };
206
245
  case "session":
207
246
  return {
208
247
  sessionId: message.sessionId,
248
+ sessionMetadata: message.sessionMetadata,
209
249
  scenarioId: message.scenarioId,
210
250
  status: message.status,
211
251
  type: "session"
@@ -220,26 +260,257 @@ var serverMessageToAction = (message) => {
220
260
  }
221
261
  };
222
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
+
223
490
  // src/client/connection.ts
224
491
  var WS_OPEN = 1;
225
492
  var WS_CLOSED = 3;
226
493
  var WS_NORMAL_CLOSURE = 1000;
227
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
494
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
228
495
  var DEFAULT_PING_INTERVAL = 30000;
229
- 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));
230
499
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
231
500
  var noop = () => {};
232
501
  var noopUnsubscribe = () => noop;
233
502
  var NOOP_CONNECTION = {
234
- start: () => {},
503
+ callControl: noop,
235
504
  close: noop,
236
505
  endTurn: noop,
506
+ send: noop,
507
+ sendAudio: noop,
508
+ simulateDisconnect: noop,
509
+ subscribe: noopUnsubscribe,
237
510
  getReadyState: () => WS_CLOSED,
238
511
  getScenarioId: () => "",
239
512
  getSessionId: () => "",
240
- send: noop,
241
- sendAudio: noop,
242
- subscribe: noopUnsubscribe
513
+ start: () => {}
243
514
  };
244
515
  var createSessionId = () => crypto.randomUUID();
245
516
  var buildWsUrl = (path, sessionId, scenarioId) => {
@@ -260,11 +531,14 @@ var isVoiceServerMessage = (value) => {
260
531
  switch (value.type) {
261
532
  case "audio":
262
533
  case "assistant":
534
+ case "call_lifecycle":
263
535
  case "complete":
536
+ case "connection":
264
537
  case "error":
265
538
  case "final":
266
539
  case "partial":
267
540
  case "pong":
541
+ case "replay":
268
542
  case "session":
269
543
  case "turn":
270
544
  return true;
@@ -290,7 +564,9 @@ var createVoiceConnection = (path, options = {}) => {
290
564
  const listeners = new Set;
291
565
  const shouldReconnect = options.reconnect !== false;
292
566
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
567
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
293
568
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
569
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
294
570
  const state = {
295
571
  isConnected: false,
296
572
  pendingMessages: [],
@@ -301,6 +577,9 @@ var createVoiceConnection = (path, options = {}) => {
301
577
  sessionId: options.sessionId ?? createSessionId(),
302
578
  ws: null
303
579
  };
580
+ const emitConnection = (reconnect) => {
581
+ listeners.forEach((listener) => listener(reconnect));
582
+ };
304
583
  const clearTimers = () => {
305
584
  if (state.pingInterval) {
306
585
  clearInterval(state.pingInterval);
@@ -324,20 +603,52 @@ var createVoiceConnection = (path, options = {}) => {
324
603
  };
325
604
  const scheduleReconnect = () => {
326
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
+ });
327
618
  state.reconnectTimeout = setTimeout(() => {
328
619
  if (state.reconnectAttempts > maxReconnectAttempts) {
620
+ emitConnection({
621
+ reconnect: {
622
+ attempts: state.reconnectAttempts,
623
+ maxAttempts: maxReconnectAttempts,
624
+ status: "exhausted"
625
+ },
626
+ type: "connection"
627
+ });
329
628
  return;
330
629
  }
331
630
  connect();
332
- }, RECONNECT_DELAY_MS);
631
+ }, delayMs);
333
632
  };
334
633
  const connect = () => {
335
634
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
336
635
  ws.binaryType = "arraybuffer";
337
636
  ws.onopen = () => {
637
+ const wasReconnecting = state.reconnectAttempts > 0;
338
638
  state.isConnected = true;
339
- state.reconnectAttempts = 0;
340
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
+ }
341
652
  listeners.forEach((listener) => listener({
342
653
  scenarioId: state.scenarioId ?? undefined,
343
654
  sessionId: state.sessionId,
@@ -367,6 +678,16 @@ var createVoiceConnection = (path, options = {}) => {
367
678
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
368
679
  if (reconnectable) {
369
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
+ });
370
691
  }
371
692
  };
372
693
  state.ws = ws;
@@ -389,9 +710,9 @@ var createVoiceConnection = (path, options = {}) => {
389
710
  state.scenarioId = input.scenarioId;
390
711
  }
391
712
  send({
392
- type: "start",
713
+ scenarioId: state.scenarioId ?? undefined,
393
714
  sessionId: state.sessionId,
394
- scenarioId: state.scenarioId ?? undefined
715
+ type: "start"
395
716
  });
396
717
  };
397
718
  const sendAudio = (audio) => {
@@ -400,6 +721,12 @@ var createVoiceConnection = (path, options = {}) => {
400
721
  const endTurn = () => {
401
722
  send({ type: "end_turn" });
402
723
  };
724
+ const callControl = (message) => {
725
+ send({
726
+ ...message,
727
+ type: "call_control"
728
+ });
729
+ };
403
730
  const close = () => {
404
731
  clearTimers();
405
732
  if (state.ws) {
@@ -409,6 +736,11 @@ var createVoiceConnection = (path, options = {}) => {
409
736
  state.isConnected = false;
410
737
  listeners.clear();
411
738
  };
739
+ const simulateDisconnect = () => {
740
+ if (state.ws?.readyState === WS_OPEN) {
741
+ state.ws.close(4000, "absolutejs-voice-reconnect-proof");
742
+ }
743
+ };
412
744
  const subscribe = (callback) => {
413
745
  listeners.add(callback);
414
746
  return () => {
@@ -417,32 +749,65 @@ var createVoiceConnection = (path, options = {}) => {
417
749
  };
418
750
  connect();
419
751
  return {
420
- start,
752
+ callControl,
421
753
  close,
422
754
  endTurn,
423
- getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
424
- getScenarioId: () => state.scenarioId ?? "",
425
- getSessionId: () => state.sessionId,
426
755
  send,
427
756
  sendAudio,
428
- subscribe
757
+ simulateDisconnect,
758
+ start,
759
+ subscribe,
760
+ getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
761
+ getScenarioId: () => state.scenarioId ?? "",
762
+ getSessionId: () => state.sessionId
429
763
  };
430
764
  };
431
765
 
432
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
+ };
433
793
  var createInitialState = () => ({
434
794
  assistantAudio: [],
795
+ assistantStreamingText: "",
435
796
  assistantTexts: [],
797
+ call: null,
436
798
  error: null,
437
799
  isConnected: false,
438
- scenarioId: null,
439
800
  partial: "",
801
+ reconnect: createInitialReconnectState(),
802
+ scenarioId: null,
440
803
  sessionId: null,
804
+ sessionMetadata: null,
441
805
  status: "idle",
442
806
  turns: []
443
807
  });
444
808
  var createVoiceStreamStore = () => {
445
809
  let state = createInitialState();
810
+ let turnFinalText = "";
446
811
  const subscribers = new Set;
447
812
  const notify = () => {
448
813
  subscribers.forEach((subscriber) => subscriber());
@@ -466,9 +831,16 @@ var createVoiceStreamStore = () => {
466
831
  case "assistant":
467
832
  state = {
468
833
  ...state,
834
+ assistantStreamingText: "",
469
835
  assistantTexts: [...state.assistantTexts, action.text]
470
836
  };
471
837
  break;
838
+ case "assistant_delta":
839
+ state = {
840
+ ...state,
841
+ assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
842
+ };
843
+ break;
472
844
  case "complete":
473
845
  state = {
474
846
  ...state,
@@ -476,10 +848,36 @@ var createVoiceStreamStore = () => {
476
848
  status: "completed"
477
849
  };
478
850
  break;
851
+ case "call_lifecycle":
852
+ state = {
853
+ ...state,
854
+ call: {
855
+ ...state.call,
856
+ disposition: action.event.type === "end" ? action.event.disposition : state.call?.disposition,
857
+ endedAt: action.event.type === "end" ? action.event.at : state.call?.endedAt,
858
+ events: [...state.call?.events ?? [], action.event],
859
+ lastEventAt: action.event.at,
860
+ startedAt: state.call?.startedAt ?? action.event.at
861
+ },
862
+ sessionId: action.sessionId
863
+ };
864
+ break;
479
865
  case "connected":
480
866
  state = {
481
867
  ...state,
482
- 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
483
881
  };
484
882
  break;
485
883
  case "disconnected":
@@ -495,16 +893,39 @@ var createVoiceStreamStore = () => {
495
893
  };
496
894
  break;
497
895
  case "final":
896
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
498
897
  state = {
499
898
  ...state,
500
- partial: action.transcript.text,
501
- turns: state.turns.map((turn) => turn)
899
+ partial: turnFinalText
502
900
  };
503
901
  break;
504
902
  case "partial":
505
903
  state = {
506
904
  ...state,
507
- 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]
508
929
  };
509
930
  break;
510
931
  case "session":
@@ -514,10 +935,12 @@ var createVoiceStreamStore = () => {
514
935
  scenarioId: action.scenarioId ?? state.scenarioId,
515
936
  isConnected: action.status === "active",
516
937
  sessionId: action.sessionId,
938
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
517
939
  status: action.status
518
940
  };
519
941
  break;
520
942
  case "turn":
943
+ turnFinalText = "";
521
944
  state = {
522
945
  ...state,
523
946
  partial: "",
@@ -544,26 +967,73 @@ var createVoiceStreamStore = () => {
544
967
  var createVoiceStream = (path, options = {}) => {
545
968
  const connection = createVoiceConnection(path, options);
546
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;
547
975
  const subscribers = new Set;
548
976
  const start = (input) => Promise.resolve().then(() => {
549
977
  if (!input?.sessionId && !input?.scenarioId) {
550
978
  return;
551
979
  }
552
980
  connection.start(input);
981
+ browserMediaReporter?.start();
553
982
  });
554
983
  const notify = () => {
555
984
  subscribers.forEach((subscriber) => subscriber());
556
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
+ };
557
1007
  const unsubscribeConnection = connection.subscribe((message) => {
558
1008
  const action = serverMessageToAction(message);
559
1009
  if (action) {
560
1010
  store.dispatch(action);
1011
+ if (message.type === "connection") {
1012
+ reportReconnect();
1013
+ }
561
1014
  notify();
562
1015
  }
563
1016
  });
564
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
+ },
1031
+ callControl(message) {
1032
+ connection.callControl(message);
1033
+ },
565
1034
  close() {
566
1035
  unsubscribeConnection();
1036
+ browserMediaReporter?.close();
567
1037
  connection.close();
568
1038
  store.dispatch({ type: "disconnected" });
569
1039
  notify();
@@ -583,41 +1053,43 @@ var createVoiceStream = (path, options = {}) => {
583
1053
  get isConnected() {
584
1054
  return store.getSnapshot().isConnected;
585
1055
  },
1056
+ get partial() {
1057
+ return store.getSnapshot().partial;
1058
+ },
1059
+ get reconnect() {
1060
+ return store.getSnapshot().reconnect;
1061
+ },
586
1062
  get scenarioId() {
587
1063
  return store.getSnapshot().scenarioId;
588
1064
  },
589
- start,
590
- get partial() {
591
- return store.getSnapshot().partial;
1065
+ sendAudio(audio) {
1066
+ connection.sendAudio(audio);
592
1067
  },
593
1068
  get sessionId() {
594
1069
  return connection.getSessionId();
595
1070
  },
596
- get status() {
597
- return store.getSnapshot().status;
598
- },
599
- get turns() {
600
- return store.getSnapshot().turns;
601
- },
602
- get assistantTexts() {
603
- return store.getSnapshot().assistantTexts;
1071
+ get sessionMetadata() {
1072
+ return store.getSnapshot().sessionMetadata;
604
1073
  },
605
- get assistantAudio() {
606
- return store.getSnapshot().assistantAudio;
1074
+ simulateDisconnect() {
1075
+ connection.simulateDisconnect();
607
1076
  },
608
- sendAudio(audio) {
609
- connection.sendAudio(audio);
1077
+ get status() {
1078
+ return store.getSnapshot().status;
610
1079
  },
611
1080
  subscribe(subscriber) {
612
1081
  subscribers.add(subscriber);
613
1082
  return () => {
614
1083
  subscribers.delete(subscriber);
615
1084
  };
1085
+ },
1086
+ get turns() {
1087
+ return store.getSnapshot().turns;
616
1088
  }
617
1089
  };
618
1090
  };
619
1091
 
620
- // src/audioConditioning.ts
1092
+ // src/core/audioConditioning.ts
621
1093
  var DEFAULT_TARGET_LEVEL = 0.08;
622
1094
  var DEFAULT_MAX_GAIN = 3;
623
1095
  var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
@@ -635,34 +1107,37 @@ var resolveAudioConditioningConfig = (config) => {
635
1107
  };
636
1108
  };
637
1109
 
638
- // src/turnProfiles.ts
1110
+ // src/core/turnProfiles.ts
639
1111
  var TURN_PROFILE_DEFAULTS = {
640
1112
  balanced: {
641
1113
  qualityProfile: "general",
1114
+ minSilenceMs: 400,
642
1115
  silenceMs: 1400,
643
1116
  speechThreshold: 0.012,
644
1117
  transcriptStabilityMs: 1000
645
1118
  },
646
1119
  fast: {
647
1120
  qualityProfile: "general",
1121
+ minSilenceMs: 300,
648
1122
  silenceMs: 700,
649
1123
  speechThreshold: 0.015,
650
1124
  transcriptStabilityMs: 450
651
1125
  },
652
1126
  "long-form": {
653
1127
  qualityProfile: "general",
1128
+ minSilenceMs: 600,
654
1129
  silenceMs: 2200,
655
1130
  speechThreshold: 0.01,
656
1131
  transcriptStabilityMs: 1500
657
1132
  }
658
1133
  };
659
1134
  var QUALITY_PROFILE_DEFAULTS = {
660
- general: {},
661
1135
  "accent-heavy": {
662
1136
  silenceMs: 1200,
663
1137
  speechThreshold: 0.01,
664
1138
  transcriptStabilityMs: 1200
665
1139
  },
1140
+ general: {},
666
1141
  "noisy-room": {
667
1142
  silenceMs: 2000,
668
1143
  speechThreshold: 0.02,
@@ -681,16 +1156,18 @@ var resolveTurnDetectionConfig = (config) => {
681
1156
  const qualityProfile = config?.qualityProfile ?? DEFAULT_QUALITY_PROFILE;
682
1157
  const preset = TURN_PROFILE_DEFAULTS[profile];
683
1158
  const quality = QUALITY_PROFILE_DEFAULTS[qualityProfile];
1159
+ const silenceMs = config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs;
684
1160
  return {
685
1161
  profile,
686
1162
  qualityProfile,
687
- silenceMs: config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs,
1163
+ minSilenceMs: Math.min(silenceMs, config?.minSilenceMs ?? quality.minSilenceMs ?? preset.minSilenceMs),
1164
+ silenceMs,
688
1165
  speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
689
1166
  transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
690
1167
  };
691
1168
  };
692
1169
 
693
- // src/presets.ts
1170
+ // src/core/presets.ts
694
1171
  var PRESET_INPUTS = {
695
1172
  chat: {
696
1173
  audioConditioning: {
@@ -711,8 +1188,8 @@ var PRESET_INPUTS = {
711
1188
  },
712
1189
  sttLifecycle: "continuous",
713
1190
  turnDetection: {
714
- qualityProfile: "short-command",
715
- profile: "balanced"
1191
+ profile: "balanced",
1192
+ qualityProfile: "short-command"
716
1193
  }
717
1194
  },
718
1195
  default: {
@@ -727,8 +1204,8 @@ var PRESET_INPUTS = {
727
1204
  },
728
1205
  sttLifecycle: "continuous",
729
1206
  turnDetection: {
730
- qualityProfile: "general",
731
- profile: "fast"
1207
+ profile: "fast",
1208
+ qualityProfile: "general"
732
1209
  }
733
1210
  },
734
1211
  dictation: {
@@ -750,8 +1227,8 @@ var PRESET_INPUTS = {
750
1227
  },
751
1228
  sttLifecycle: "continuous",
752
1229
  turnDetection: {
753
- qualityProfile: "accent-heavy",
754
- profile: "long-form"
1230
+ profile: "long-form",
1231
+ qualityProfile: "accent-heavy"
755
1232
  }
756
1233
  },
757
1234
  "guided-intake": {
@@ -773,8 +1250,8 @@ var PRESET_INPUTS = {
773
1250
  },
774
1251
  sttLifecycle: "turn-scoped",
775
1252
  turnDetection: {
776
- qualityProfile: "accent-heavy",
777
- profile: "long-form"
1253
+ profile: "long-form",
1254
+ qualityProfile: "accent-heavy"
778
1255
  }
779
1256
  },
780
1257
  "noisy-room": {
@@ -796,8 +1273,8 @@ var PRESET_INPUTS = {
796
1273
  },
797
1274
  sttLifecycle: "continuous",
798
1275
  turnDetection: {
799
- qualityProfile: "noisy-room",
800
1276
  profile: "long-form",
1277
+ qualityProfile: "noisy-room",
801
1278
  silenceMs: 2100,
802
1279
  speechThreshold: 0.02,
803
1280
  transcriptStabilityMs: 1650
@@ -822,8 +1299,8 @@ var PRESET_INPUTS = {
822
1299
  },
823
1300
  sttLifecycle: "continuous",
824
1301
  turnDetection: {
825
- qualityProfile: "noisy-room",
826
1302
  profile: "long-form",
1303
+ qualityProfile: "noisy-room",
827
1304
  silenceMs: 660,
828
1305
  speechThreshold: 0.012,
829
1306
  transcriptStabilityMs: 300
@@ -848,8 +1325,8 @@ var PRESET_INPUTS = {
848
1325
  },
849
1326
  sttLifecycle: "continuous",
850
1327
  turnDetection: {
851
- qualityProfile: "noisy-room",
852
1328
  profile: "long-form",
1329
+ qualityProfile: "noisy-room",
853
1330
  silenceMs: 620,
854
1331
  speechThreshold: 0.012,
855
1332
  transcriptStabilityMs: 280
@@ -874,8 +1351,8 @@ var PRESET_INPUTS = {
874
1351
  },
875
1352
  sttLifecycle: "continuous",
876
1353
  turnDetection: {
877
- qualityProfile: "noisy-room",
878
- profile: "long-form"
1354
+ profile: "long-form",
1355
+ qualityProfile: "noisy-room"
879
1356
  }
880
1357
  }
881
1358
  };
@@ -899,13 +1376,17 @@ var resolveVoiceRuntimePreset = (name = "default") => {
899
1376
  // src/client/controller.ts
900
1377
  var createInitialState2 = (stream) => ({
901
1378
  assistantAudio: [...stream.assistantAudio],
1379
+ assistantStreamingText: stream.assistantStreamingText,
902
1380
  assistantTexts: [...stream.assistantTexts],
1381
+ call: stream.call,
903
1382
  error: stream.error,
904
1383
  isConnected: stream.isConnected,
905
1384
  isRecording: false,
906
1385
  partial: stream.partial,
1386
+ reconnect: stream.reconnect,
907
1387
  recordingError: null,
908
1388
  sessionId: stream.sessionId,
1389
+ sessionMetadata: stream.sessionMetadata,
909
1390
  scenarioId: stream.scenarioId,
910
1391
  status: stream.status,
911
1392
  turns: [...stream.turns]
@@ -928,11 +1409,15 @@ var createVoiceController = (path, options = {}) => {
928
1409
  state = {
929
1410
  ...state,
930
1411
  assistantAudio: [...stream.assistantAudio],
1412
+ assistantStreamingText: stream.assistantStreamingText,
931
1413
  assistantTexts: [...stream.assistantTexts],
1414
+ call: stream.call,
932
1415
  error: stream.error,
933
1416
  isConnected: stream.isConnected,
934
1417
  partial: stream.partial,
1418
+ reconnect: stream.reconnect,
935
1419
  sessionId: stream.sessionId,
1420
+ sessionMetadata: stream.sessionMetadata,
936
1421
  scenarioId: stream.scenarioId,
937
1422
  status: stream.status,
938
1423
  turns: [...stream.turns]
@@ -956,8 +1441,15 @@ var createVoiceController = (path, options = {}) => {
956
1441
  capture = createMicrophoneCapture({
957
1442
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
958
1443
  onLevel: options.capture?.onLevel,
959
- onAudio: (audio) => stream.sendAudio(audio),
960
- sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
1444
+ onAudio: (audio) => {
1445
+ if (options.capture?.onAudio) {
1446
+ options.capture.onAudio(audio, stream.sendAudio);
1447
+ return;
1448
+ }
1449
+ stream.sendAudio(audio);
1450
+ },
1451
+ sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
1452
+ ...options.capture?.stream ? { stream: options.capture.stream } : {}
961
1453
  });
962
1454
  return capture;
963
1455
  };
@@ -1003,10 +1495,25 @@ var createVoiceController = (path, options = {}) => {
1003
1495
  stream.close();
1004
1496
  };
1005
1497
  return {
1498
+ close,
1499
+ startRecording,
1500
+ stopRecording,
1501
+ get assistantAudio() {
1502
+ return state.assistantAudio;
1503
+ },
1504
+ get assistantTexts() {
1505
+ return state.assistantTexts;
1506
+ },
1507
+ get assistantStreamingText() {
1508
+ return state.assistantStreamingText;
1509
+ },
1006
1510
  bindHTMX(bindingOptions) {
1007
1511
  return bindVoiceHTMX(stream, bindingOptions);
1008
1512
  },
1009
- close,
1513
+ get call() {
1514
+ return state.call;
1515
+ },
1516
+ callControl: (message) => stream.callControl(message),
1010
1517
  endTurn: () => stream.endTurn(),
1011
1518
  get error() {
1012
1519
  return state.error;
@@ -1022,21 +1529,26 @@ var createVoiceController = (path, options = {}) => {
1022
1529
  get partial() {
1023
1530
  return state.partial;
1024
1531
  },
1532
+ get reconnect() {
1533
+ return state.reconnect;
1534
+ },
1025
1535
  get recordingError() {
1026
1536
  return state.recordingError;
1027
1537
  },
1538
+ get scenarioId() {
1539
+ return state.scenarioId;
1540
+ },
1028
1541
  sendAudio: (audio) => stream.sendAudio(audio),
1029
1542
  get sessionId() {
1030
1543
  return state.sessionId;
1031
1544
  },
1032
- get scenarioId() {
1033
- return state.scenarioId;
1545
+ get sessionMetadata() {
1546
+ return state.sessionMetadata;
1034
1547
  },
1035
- startRecording,
1548
+ simulateDisconnect: () => stream.simulateDisconnect(),
1036
1549
  get status() {
1037
1550
  return state.status;
1038
1551
  },
1039
- stopRecording,
1040
1552
  subscribe: (subscriber) => {
1041
1553
  subscribers.add(subscriber);
1042
1554
  return () => {
@@ -1052,12 +1564,715 @@ var createVoiceController = (path, options = {}) => {
1052
1564
  },
1053
1565
  get turns() {
1054
1566
  return state.turns;
1567
+ }
1568
+ };
1569
+ };
1570
+
1571
+ // src/client/timeStretch.ts
1572
+ var HOP_MS = 10;
1573
+ var SEEK_MS = 5;
1574
+ var ENERGY_EPSILON = 0.000001;
1575
+ var HALF = 0.5;
1576
+ var MS_PER_SECOND = 1000;
1577
+ var makeHann = (length) => {
1578
+ const weights = new Float32Array(length);
1579
+ for (let index = 0;index < length; index += 1) {
1580
+ weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
1581
+ }
1582
+ return weights;
1583
+ };
1584
+ var correlationScore = (base, start, ref, length) => {
1585
+ let dot = 0;
1586
+ let energy = 0;
1587
+ for (let index = 0;index < length; index += 1) {
1588
+ const sample = base[start + index] ?? 0;
1589
+ dot += sample * (ref[index] ?? 0);
1590
+ energy += sample * sample;
1591
+ }
1592
+ return dot / Math.sqrt(energy + ENERGY_EPSILON);
1593
+ };
1594
+ var overlapAddGrain = (src, off, tail, weights, hop) => {
1595
+ const out = new Float32Array(hop);
1596
+ const nextTail = new Float32Array(hop);
1597
+ for (let index = 0;index < hop; index += 1) {
1598
+ out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
1599
+ nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
1600
+ }
1601
+ return { nextTail, out };
1602
+ };
1603
+ var createTimeStretcher = () => {
1604
+ let sampleRate = 0;
1605
+ let channelCount = 0;
1606
+ let hop = 0;
1607
+ let frameLen = 0;
1608
+ let seek = 0;
1609
+ let weights = new Float32Array(0);
1610
+ let buffers = [];
1611
+ let inputStart = 0;
1612
+ let analysisPos = 0;
1613
+ let olaTail = [];
1614
+ let naturalRef = null;
1615
+ const init = (rate, channels) => {
1616
+ sampleRate = rate;
1617
+ channelCount = channels;
1618
+ hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
1619
+ frameLen = hop * 2;
1620
+ seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
1621
+ weights = makeHann(frameLen);
1622
+ buffers = Array.from({ length: channels }, () => new Float32Array(0));
1623
+ olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
1624
+ inputStart = 0;
1625
+ analysisPos = seek;
1626
+ naturalRef = null;
1627
+ };
1628
+ const reset = () => {
1629
+ buffers = buffers.map(() => new Float32Array(0));
1630
+ olaTail = olaTail.map(() => new Float32Array(hop));
1631
+ inputStart = 0;
1632
+ analysisPos = seek;
1633
+ naturalRef = null;
1634
+ };
1635
+ const append = (input) => {
1636
+ for (let channel = 0;channel < channelCount; channel += 1) {
1637
+ const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
1638
+ const existing = buffers[channel] ?? new Float32Array(0);
1639
+ const merged = new Float32Array(existing.length + incoming.length);
1640
+ merged.set(existing, 0);
1641
+ merged.set(incoming, existing.length);
1642
+ buffers[channel] = merged;
1643
+ }
1644
+ };
1645
+ const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
1646
+ const compact = () => {
1647
+ const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
1648
+ if (keepFrom <= inputStart)
1649
+ return;
1650
+ const drop = keepFrom - inputStart;
1651
+ for (let channel = 0;channel < channelCount; channel += 1) {
1652
+ buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
1653
+ }
1654
+ inputStart = keepFrom;
1655
+ };
1656
+ const bestOffset = (center) => {
1657
+ if (!naturalRef)
1658
+ return 0;
1659
+ const [base] = buffers;
1660
+ if (!base)
1661
+ return 0;
1662
+ let bestDelta = 0;
1663
+ let bestScore = -Infinity;
1664
+ for (let delta = -seek;delta <= seek; delta += 1) {
1665
+ const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
1666
+ if (score <= bestScore)
1667
+ continue;
1668
+ bestScore = score;
1669
+ bestDelta = delta;
1670
+ }
1671
+ return bestDelta;
1672
+ };
1673
+ const process = (input, speed, rate) => {
1674
+ const channels = Math.max(1, input.length);
1675
+ if (sampleRate !== rate || channelCount !== channels)
1676
+ init(rate, channels);
1677
+ append(input);
1678
+ const analysisHop = hop * speed;
1679
+ const segments = Array.from({ length: channelCount }, () => []);
1680
+ const emitGrain = (pos) => {
1681
+ const off = pos - inputStart;
1682
+ for (let channel = 0;channel < channelCount; channel += 1) {
1683
+ const src = buffers[channel];
1684
+ const tail = olaTail[channel];
1685
+ if (!src || !tail)
1686
+ continue;
1687
+ const grain = overlapAddGrain(src, off, tail, weights, hop);
1688
+ olaTail[channel] = grain.nextTail;
1689
+ segments[channel]?.push(grain.out);
1690
+ }
1691
+ };
1692
+ const captureRef = (pos) => {
1693
+ const ref = new Float32Array(frameLen);
1694
+ const refOff = pos + hop - inputStart;
1695
+ const [base] = buffers;
1696
+ if (base)
1697
+ ref.set(base.subarray(refOff, refOff + frameLen));
1698
+ naturalRef = ref;
1699
+ };
1700
+ const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
1701
+ while (canEmit()) {
1702
+ const center = Math.round(analysisPos);
1703
+ const pos = center + bestOffset(center);
1704
+ emitGrain(pos);
1705
+ captureRef(pos);
1706
+ analysisPos += analysisHop;
1707
+ }
1708
+ compact();
1709
+ return segments.map((channelSegments) => {
1710
+ const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
1711
+ const merged = new Float32Array(total);
1712
+ let offset = 0;
1713
+ for (const seg of channelSegments) {
1714
+ merged.set(seg, offset);
1715
+ offset += seg.length;
1716
+ }
1717
+ return merged;
1718
+ });
1719
+ };
1720
+ return { process, reset };
1721
+ };
1722
+
1723
+ // src/client/audioPlayer.ts
1724
+ var DEFAULT_LOOKAHEAD_MS = 15;
1725
+ var DEFAULT_VOLUME = 1;
1726
+ var DEFAULT_PLAYBACK_RATE = 1;
1727
+ var MIN_PLAYBACK_RATE = 0.5;
1728
+ var MAX_PLAYBACK_RATE = 2;
1729
+ var STRETCH_BYPASS_EPSILON = 0.01;
1730
+ var ANALYSER_FFT_SIZE = 256;
1731
+ var PCM_BYTE_MIDPOINT = 128;
1732
+ var createInitialState3 = () => ({
1733
+ activeSourceCount: 0,
1734
+ error: null,
1735
+ isActive: false,
1736
+ isPlaying: false,
1737
+ lastInterruptLatencyMs: undefined,
1738
+ lastPlaybackStopLatencyMs: undefined,
1739
+ processedChunkCount: 0,
1740
+ queuedChunkCount: 0
1741
+ });
1742
+ var getAudioContextCtor = () => {
1743
+ if (typeof window === "undefined") {
1744
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1745
+ }
1746
+ return window.AudioContext ?? window.webkitAudioContext;
1747
+ };
1748
+ var clampVolume = (volume) => {
1749
+ if (typeof volume !== "number" || !Number.isFinite(volume)) {
1750
+ return DEFAULT_VOLUME;
1751
+ }
1752
+ return Math.min(1, Math.max(0, volume));
1753
+ };
1754
+ var clampPlaybackRate = (rate) => {
1755
+ if (typeof rate !== "number" || !Number.isFinite(rate)) {
1756
+ return DEFAULT_PLAYBACK_RATE;
1757
+ }
1758
+ return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
1759
+ };
1760
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1761
+ const { format } = chunk;
1762
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1763
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1764
+ }
1765
+ const bytes = chunk.chunk;
1766
+ const channels = Math.max(1, format.channels);
1767
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1768
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1769
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1770
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1771
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1772
+ const channelData = audioBuffer.getChannelData(channelIndex);
1773
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1774
+ const sampleIndex = frameIndex * channels + channelIndex;
1775
+ const sampleOffset = sampleIndex * 2;
1776
+ if (sampleOffset + 1 >= bytes.byteLength) {
1777
+ channelData[frameIndex] = 0;
1778
+ continue;
1779
+ }
1780
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1781
+ }
1782
+ }
1783
+ return audioBuffer;
1784
+ };
1785
+ var createVoiceAudioPlayer = (source, options = {}) => {
1786
+ const subscribers = new Set;
1787
+ const sourceNodes = new Set;
1788
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1789
+ let state = createInitialState3();
1790
+ let audioContext = null;
1791
+ let outputNode = null;
1792
+ let analyserNode = null;
1793
+ let analyserBuffer = null;
1794
+ let volume = clampVolume(options.volume);
1795
+ let playbackRate = clampPlaybackRate(options.playbackRate);
1796
+ let stretcher = null;
1797
+ let queueEndTime = 0;
1798
+ let syncPromise = Promise.resolve();
1799
+ let interruptStartedAt = null;
1800
+ let interruptPromise = null;
1801
+ let resolveInterruptPromise = null;
1802
+ let interruptFallbackTimer = null;
1803
+ const notify = () => {
1804
+ for (const subscriber of subscribers) {
1805
+ subscriber();
1806
+ }
1807
+ };
1808
+ const setState = (next) => {
1809
+ state = {
1810
+ ...state,
1811
+ ...next
1812
+ };
1813
+ notify();
1814
+ };
1815
+ const clearError = () => {
1816
+ if (state.error !== null) {
1817
+ setState({ error: null });
1818
+ }
1819
+ };
1820
+ const clearInterruptTimer = () => {
1821
+ if (interruptFallbackTimer !== null) {
1822
+ clearTimeout(interruptFallbackTimer);
1823
+ interruptFallbackTimer = null;
1824
+ }
1825
+ };
1826
+ const resolveInterrupt = (latencyMs) => {
1827
+ clearInterruptTimer();
1828
+ interruptStartedAt = null;
1829
+ stretcher?.reset();
1830
+ setState({
1831
+ activeSourceCount: sourceNodes.size,
1832
+ isPlaying: false,
1833
+ lastInterruptLatencyMs: latencyMs,
1834
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1835
+ });
1836
+ resolveInterruptPromise?.();
1837
+ resolveInterruptPromise = null;
1838
+ interruptPromise = null;
1839
+ };
1840
+ const estimateOutputStopLatencyMs = (context) => {
1841
+ if (!context) {
1842
+ return 0;
1843
+ }
1844
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1845
+ };
1846
+ const applyOutputGain = (context) => {
1847
+ if (!outputNode) {
1848
+ return;
1849
+ }
1850
+ const gainValue = volume;
1851
+ if (outputNode.gain.setValueAtTime) {
1852
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1853
+ return;
1854
+ }
1855
+ outputNode.gain.value = gainValue;
1856
+ };
1857
+ const muteOutputGain = (context) => {
1858
+ if (!outputNode) {
1859
+ return;
1860
+ }
1861
+ const gainValue = 0;
1862
+ if (outputNode.gain.setValueAtTime) {
1863
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1864
+ return;
1865
+ }
1866
+ outputNode.gain.value = gainValue;
1867
+ };
1868
+ const maybeResolveInterrupt = () => {
1869
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1870
+ return;
1871
+ }
1872
+ resolveInterrupt(Date.now() - interruptStartedAt);
1873
+ };
1874
+ const ensureAudioContext = async () => {
1875
+ if (audioContext) {
1876
+ return audioContext;
1877
+ }
1878
+ if (options.createAudioContext) {
1879
+ audioContext = options.createAudioContext();
1880
+ } else {
1881
+ const AudioContextCtor = getAudioContextCtor();
1882
+ if (!AudioContextCtor) {
1883
+ throw new Error("Assistant audio playback requires AudioContext support.");
1884
+ }
1885
+ audioContext = new AudioContextCtor;
1886
+ }
1887
+ if (audioContext.createGain) {
1888
+ outputNode = audioContext.createGain();
1889
+ outputNode.connect?.(audioContext.destination);
1890
+ if (audioContext.createAnalyser) {
1891
+ analyserNode = audioContext.createAnalyser();
1892
+ analyserNode.fftSize = ANALYSER_FFT_SIZE;
1893
+ analyserBuffer = new Uint8Array(analyserNode.fftSize);
1894
+ outputNode.connect?.(analyserNode);
1895
+ }
1896
+ }
1897
+ queueEndTime = audioContext.currentTime;
1898
+ return audioContext;
1899
+ };
1900
+ const scheduleBuffer = (context, buffer, rate) => {
1901
+ const node = context.createBufferSource();
1902
+ node.buffer = buffer;
1903
+ if (node.playbackRate) {
1904
+ node.playbackRate.value = rate;
1905
+ }
1906
+ node.connect(outputNode ?? context.destination);
1907
+ node.onended = () => {
1908
+ sourceNodes.delete(node);
1909
+ node.disconnect?.();
1910
+ setState({
1911
+ activeSourceCount: sourceNodes.size,
1912
+ isPlaying: sourceNodes.size > 0 && state.isActive
1913
+ });
1914
+ maybeResolveInterrupt();
1915
+ };
1916
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1917
+ queueEndTime = startAt + buffer.duration / rate;
1918
+ sourceNodes.add(node);
1919
+ setState({
1920
+ activeSourceCount: sourceNodes.size,
1921
+ isPlaying: true
1922
+ });
1923
+ node.start(startAt);
1924
+ };
1925
+ const scheduleChunk = async (chunk) => {
1926
+ const context = await ensureAudioContext();
1927
+ const buffer = decodePCM16LEChunk(context, chunk);
1928
+ if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
1929
+ stretcher?.reset();
1930
+ scheduleBuffer(context, buffer, playbackRate);
1931
+ return;
1932
+ }
1933
+ const channels = Math.max(1, chunk.format.channels);
1934
+ const input = [];
1935
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1936
+ input.push(buffer.getChannelData(channelIndex));
1937
+ }
1938
+ stretcher ??= createTimeStretcher();
1939
+ const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
1940
+ const outLength = stretched[0]?.length ?? 0;
1941
+ if (outLength === 0) {
1942
+ return;
1943
+ }
1944
+ const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
1945
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1946
+ const channelOut = stretched[channelIndex];
1947
+ if (!channelOut)
1948
+ continue;
1949
+ outBuffer.getChannelData(channelIndex).set(channelOut);
1950
+ }
1951
+ scheduleBuffer(context, outBuffer, 1);
1952
+ };
1953
+ const stopQueuedPlayback = (options2) => {
1954
+ for (const node of [...sourceNodes]) {
1955
+ node.stop?.();
1956
+ }
1957
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1958
+ if (options2?.forceClear) {
1959
+ for (const node of sourceNodes) {
1960
+ node.disconnect?.();
1961
+ }
1962
+ sourceNodes.clear();
1963
+ maybeResolveInterrupt();
1964
+ }
1965
+ };
1966
+ const sync = async () => {
1967
+ if (!state.isActive) {
1968
+ return;
1969
+ }
1970
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1971
+ if (nextChunks.length === 0) {
1972
+ return;
1973
+ }
1974
+ try {
1975
+ clearError();
1976
+ for (const chunk of nextChunks) {
1977
+ await scheduleChunk(chunk);
1978
+ }
1979
+ setState({
1980
+ processedChunkCount: source.assistantAudio.length,
1981
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1982
+ });
1983
+ } catch (error) {
1984
+ setState({
1985
+ error: error instanceof Error ? error.message : String(error)
1986
+ });
1987
+ }
1988
+ };
1989
+ const queueSync = () => {
1990
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1991
+ return syncPromise;
1992
+ };
1993
+ const unsubscribeSource = source.subscribe(() => {
1994
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1995
+ player.start();
1996
+ return;
1997
+ }
1998
+ if (state.isActive) {
1999
+ queueSync();
2000
+ }
2001
+ });
2002
+ const player = {
2003
+ get activeSourceCount() {
2004
+ return state.activeSourceCount;
1055
2005
  },
1056
- get assistantTexts() {
1057
- return state.assistantTexts;
2006
+ close: async () => {
2007
+ unsubscribeSource();
2008
+ stopQueuedPlayback({ forceClear: true });
2009
+ clearInterruptTimer();
2010
+ resolveInterruptPromise?.();
2011
+ resolveInterruptPromise = null;
2012
+ interruptPromise = null;
2013
+ interruptStartedAt = null;
2014
+ if (audioContext && audioContext.state !== "closed") {
2015
+ await audioContext.close();
2016
+ }
2017
+ audioContext = null;
2018
+ outputNode?.disconnect?.();
2019
+ outputNode = null;
2020
+ analyserNode?.disconnect?.();
2021
+ analyserNode = null;
2022
+ analyserBuffer = null;
2023
+ queueEndTime = 0;
2024
+ setState({
2025
+ activeSourceCount: 0,
2026
+ isActive: false,
2027
+ isPlaying: false
2028
+ });
1058
2029
  },
1059
- get assistantAudio() {
1060
- return state.assistantAudio;
2030
+ get error() {
2031
+ return state.error;
2032
+ },
2033
+ getOutputLevel: () => {
2034
+ if (!analyserNode || !analyserBuffer) {
2035
+ return 0;
2036
+ }
2037
+ analyserNode.getByteTimeDomainData(analyserBuffer);
2038
+ let sumSquares = 0;
2039
+ for (const sample of analyserBuffer) {
2040
+ const centered = (sample - PCM_BYTE_MIDPOINT) / PCM_BYTE_MIDPOINT;
2041
+ sumSquares += centered * centered;
2042
+ }
2043
+ return Math.sqrt(sumSquares / analyserBuffer.length);
2044
+ },
2045
+ getSnapshot: () => state,
2046
+ interrupt: async () => {
2047
+ const startedAt = Date.now();
2048
+ const context = await ensureAudioContext();
2049
+ interruptStartedAt = startedAt;
2050
+ muteOutputGain(context);
2051
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
2052
+ setState({
2053
+ isActive: false,
2054
+ isPlaying: sourceNodes.size > 0,
2055
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
2056
+ });
2057
+ if (sourceNodes.size === 0) {
2058
+ resolveInterrupt(playbackStopLatencyMs);
2059
+ return;
2060
+ }
2061
+ if (!interruptPromise) {
2062
+ interruptPromise = new Promise((resolve) => {
2063
+ resolveInterruptPromise = resolve;
2064
+ });
2065
+ }
2066
+ clearInterruptTimer();
2067
+ interruptFallbackTimer = setTimeout(() => {
2068
+ for (const node of sourceNodes) {
2069
+ node.disconnect?.();
2070
+ }
2071
+ sourceNodes.clear();
2072
+ resolveInterrupt(Date.now() - startedAt);
2073
+ }, 250);
2074
+ stopQueuedPlayback();
2075
+ await interruptPromise;
2076
+ },
2077
+ get isActive() {
2078
+ return state.isActive;
2079
+ },
2080
+ get isPlaying() {
2081
+ return state.isPlaying;
2082
+ },
2083
+ get lastInterruptLatencyMs() {
2084
+ return state.lastInterruptLatencyMs;
2085
+ },
2086
+ get lastPlaybackStopLatencyMs() {
2087
+ return state.lastPlaybackStopLatencyMs;
2088
+ },
2089
+ pause: async () => {
2090
+ if (!audioContext) {
2091
+ setState({
2092
+ activeSourceCount: 0,
2093
+ isActive: false,
2094
+ isPlaying: false
2095
+ });
2096
+ return;
2097
+ }
2098
+ await audioContext.suspend();
2099
+ setState({
2100
+ activeSourceCount: sourceNodes.size,
2101
+ isActive: false,
2102
+ isPlaying: false
2103
+ });
2104
+ },
2105
+ get playbackRate() {
2106
+ return playbackRate;
2107
+ },
2108
+ get processedChunkCount() {
2109
+ return state.processedChunkCount;
2110
+ },
2111
+ get queuedChunkCount() {
2112
+ return state.queuedChunkCount;
2113
+ },
2114
+ setPlaybackRate: (nextRate) => {
2115
+ playbackRate = clampPlaybackRate(nextRate);
2116
+ },
2117
+ setVolume: (nextVolume) => {
2118
+ volume = clampVolume(nextVolume);
2119
+ applyOutputGain(audioContext);
2120
+ },
2121
+ start: async () => {
2122
+ try {
2123
+ clearError();
2124
+ const context = await ensureAudioContext();
2125
+ applyOutputGain(context);
2126
+ if (context.state === "suspended") {
2127
+ await context.resume();
2128
+ }
2129
+ setState({
2130
+ activeSourceCount: sourceNodes.size,
2131
+ isActive: true,
2132
+ isPlaying: context.state === "running"
2133
+ });
2134
+ await queueSync();
2135
+ } catch (error) {
2136
+ setState({
2137
+ error: error instanceof Error ? error.message : String(error),
2138
+ isActive: false,
2139
+ isPlaying: false
2140
+ });
2141
+ throw error;
2142
+ }
2143
+ },
2144
+ subscribe: (subscriber) => {
2145
+ subscribers.add(subscriber);
2146
+ return () => {
2147
+ subscribers.delete(subscriber);
2148
+ };
2149
+ },
2150
+ get volume() {
2151
+ return volume;
2152
+ }
2153
+ };
2154
+ return player;
2155
+ };
2156
+
2157
+ // src/client/bargeInMonitor.ts
2158
+ var DEFAULT_THRESHOLD_MS = 250;
2159
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
2160
+ var summarize = (events, thresholdMs) => {
2161
+ const stopped = events.filter((event) => event.status === "stopped");
2162
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
2163
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
2164
+ const passed = stopped.length - failed;
2165
+ return {
2166
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
2167
+ events: [...events],
2168
+ failed,
2169
+ lastEvent: events.at(-1),
2170
+ passed,
2171
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
2172
+ thresholdMs,
2173
+ total: stopped.length
2174
+ };
2175
+ };
2176
+ var createVoiceBargeInMonitor = (options = {}) => {
2177
+ const listeners = new Set;
2178
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
2179
+ const fetchImpl = options.fetch ?? globalThis.fetch;
2180
+ const events = [];
2181
+ const emit = () => {
2182
+ for (const listener of listeners) {
2183
+ listener();
2184
+ }
2185
+ };
2186
+ const postEvent = (event) => {
2187
+ if (!options.path || typeof fetchImpl !== "function") {
2188
+ return;
2189
+ }
2190
+ fetchImpl(options.path, {
2191
+ body: JSON.stringify(event),
2192
+ headers: {
2193
+ "Content-Type": "application/json"
2194
+ },
2195
+ method: "POST"
2196
+ }).catch(() => {});
2197
+ };
2198
+ const record = (status, input) => {
2199
+ const event = {
2200
+ at: Date.now(),
2201
+ id: createEventId(),
2202
+ latencyMs: input.latencyMs,
2203
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
2204
+ reason: input.reason,
2205
+ sessionId: input.sessionId,
2206
+ status,
2207
+ thresholdMs
2208
+ };
2209
+ events.push(event);
2210
+ postEvent(event);
2211
+ emit();
2212
+ return event;
2213
+ };
2214
+ return {
2215
+ getSnapshot: () => summarize(events, thresholdMs),
2216
+ recordRequested: (input) => record("requested", input),
2217
+ recordSkipped: (input) => record("skipped", input),
2218
+ recordStopped: (input) => record("stopped", input),
2219
+ subscribe: (subscriber) => {
2220
+ listeners.add(subscriber);
2221
+ return () => {
2222
+ listeners.delete(subscriber);
2223
+ };
2224
+ }
2225
+ };
2226
+ };
2227
+
2228
+ // src/client/duplex.ts
2229
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
2230
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
2231
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
2232
+ let lastPartial = controller.partial;
2233
+ const interruptIfPlaying = (reason) => {
2234
+ if (!player.isPlaying || options.enabled === false) {
2235
+ options.monitor?.recordSkipped({
2236
+ reason,
2237
+ sessionId: controller.sessionId
2238
+ });
2239
+ return;
2240
+ }
2241
+ options.monitor?.recordRequested({
2242
+ reason,
2243
+ sessionId: controller.sessionId
2244
+ });
2245
+ player.interrupt().then(() => {
2246
+ options.monitor?.recordStopped({
2247
+ latencyMs: player.lastInterruptLatencyMs,
2248
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
2249
+ reason,
2250
+ sessionId: controller.sessionId
2251
+ });
2252
+ });
2253
+ };
2254
+ const unsubscribe = controller.subscribe(() => {
2255
+ if (options.interruptOnPartial === false) {
2256
+ lastPartial = controller.partial;
2257
+ return;
2258
+ }
2259
+ if (!lastPartial && controller.partial) {
2260
+ interruptIfPlaying("partial-transcript");
2261
+ }
2262
+ lastPartial = controller.partial;
2263
+ });
2264
+ return {
2265
+ close: () => {
2266
+ unsubscribe();
2267
+ },
2268
+ handleLevel: (level) => {
2269
+ if (shouldInterruptForLevel(level, options)) {
2270
+ interruptIfPlaying("input-level");
2271
+ }
2272
+ },
2273
+ sendAudio: (audio) => {
2274
+ interruptIfPlaying("manual-audio");
2275
+ controller.sendAudio(audio);
1061
2276
  }
1062
2277
  };
1063
2278
  };
@@ -1084,8 +2299,7 @@ var DEFAULT_GUIDED_PROMPTS = [
1084
2299
  "Now describe what you are trying to do or test.",
1085
2300
  "Finish with any detail that feels blocked, risky, or unclear."
1086
2301
  ];
1087
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1088
- var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
2302
+ var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
1089
2303
  var readErrorField = (value, key) => {
1090
2304
  const candidate = value[key];
1091
2305
  if (typeof candidate === "string" && candidate.trim()) {
@@ -1118,6 +2332,17 @@ var formatErrorMessage = (error) => {
1118
2332
  }
1119
2333
  return "Unexpected error";
1120
2334
  };
2335
+ var formatReconnectState = (reconnect) => {
2336
+ const pieces = [reconnect.status];
2337
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
2338
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
2339
+ }
2340
+ if (reconnect.nextAttemptAt) {
2341
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
2342
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
2343
+ }
2344
+ return pieces.join(" · ");
2345
+ };
1121
2346
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1122
2347
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1123
2348
  const next = levels.slice(-(count - 1));
@@ -1174,6 +2399,20 @@ var parsePromptList = (value) => {
1174
2399
  } catch {}
1175
2400
  return DEFAULT_GUIDED_PROMPTS;
1176
2401
  };
2402
+ var parseOptionalNumber = (value) => {
2403
+ if (!value) {
2404
+ return;
2405
+ }
2406
+ const parsed = Number(value);
2407
+ return Number.isFinite(parsed) ? parsed : undefined;
2408
+ };
2409
+ var resolveElement2 = (root, selector, ctor) => {
2410
+ if (!selector) {
2411
+ return null;
2412
+ }
2413
+ const value = document.querySelector(selector);
2414
+ return value instanceof ctor ? value : null;
2415
+ };
1177
2416
  var requireElement = (root, selector, ctor, name) => {
1178
2417
  const value = selector ? document.querySelector(selector) : null;
1179
2418
  if (value instanceof ctor) {
@@ -1224,11 +2463,20 @@ var initVoiceHTMXRoot = (root) => {
1224
2463
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1225
2464
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1226
2465
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
2466
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
2467
+ const bargeInPath = root.dataset.voiceBargeInPath;
2468
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
2469
+ path: bargeInPath,
2470
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
2471
+ }) : null;
2472
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
2473
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1227
2474
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1228
2475
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1229
2476
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1230
2477
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1231
2478
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
2479
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1232
2480
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1233
2481
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1234
2482
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1237,35 +2485,70 @@ var initVoiceHTMXRoot = (root) => {
1237
2485
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1238
2486
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1239
2487
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
2488
+ let activeMode = null;
2489
+ let hasStartedModes = {
2490
+ general: false,
2491
+ guided: false
2492
+ };
2493
+ let isCapturing = false;
2494
+ let micError = null;
2495
+ let waveLevels = createInitialVoiceWaveLevels();
2496
+ let guidedBargeInBinding = null;
2497
+ let generalBargeInBinding = null;
1240
2498
  const guidedVoice = createVoiceController(guidedPath, {
1241
2499
  capture: {
2500
+ onAudio: (audio, sendAudio) => {
2501
+ if (guidedBargeInBinding) {
2502
+ guidedBargeInBinding.sendAudio(audio);
2503
+ return;
2504
+ }
2505
+ sendAudio(audio);
2506
+ },
1242
2507
  onLevel: (level) => {
2508
+ guidedBargeInBinding?.handleLevel(level);
1243
2509
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1244
2510
  renderWave();
1245
2511
  }
1246
2512
  },
2513
+ connection: {
2514
+ reconnectReportPath
2515
+ },
1247
2516
  preset: "guided-intake"
1248
2517
  });
1249
2518
  const generalVoice = createVoiceController(generalPath, {
1250
2519
  capture: {
2520
+ onAudio: (audio, sendAudio) => {
2521
+ if (generalBargeInBinding) {
2522
+ generalBargeInBinding.sendAudio(audio);
2523
+ return;
2524
+ }
2525
+ sendAudio(audio);
2526
+ },
1251
2527
  onLevel: (level) => {
2528
+ generalBargeInBinding?.handleLevel(level);
1252
2529
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1253
2530
  renderWave();
1254
2531
  }
1255
2532
  },
2533
+ connection: {
2534
+ reconnectReportPath
2535
+ },
1256
2536
  preset: "dictation"
1257
2537
  });
1258
2538
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1259
2539
  const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
1260
- let activeMode = null;
1261
- let hasStartedModes = {
1262
- general: false,
1263
- guided: false
1264
- };
1265
- let isCapturing = false;
1266
- let micError = null;
1267
- let waveLevels = createInitialVoiceWaveLevels();
2540
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
2541
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
2542
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
2543
+ interruptThreshold: bargeInSpeechThreshold,
2544
+ monitor: bargeInMonitor ?? undefined
2545
+ });
2546
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
2547
+ interruptThreshold: bargeInSpeechThreshold,
2548
+ monitor: bargeInMonitor ?? undefined
2549
+ });
1268
2550
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
2551
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1269
2552
  const renderWave = () => {
1270
2553
  const path = createVoiceWavePath(waveLevels);
1271
2554
  voiceWaveGlow.setAttribute("d", path);
@@ -1277,9 +2560,12 @@ var initVoiceHTMXRoot = (root) => {
1277
2560
  const render = () => {
1278
2561
  const voice = currentVoice();
1279
2562
  const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
1280
- const status = voice.status;
2563
+ const { status } = voice;
1281
2564
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1282
2565
  errorStatus.textContent = micError || voice.error || "None";
2566
+ if (reconnectStatus) {
2567
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2568
+ }
1283
2569
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1284
2570
  promptStatus.textContent = resolvePromptMessage({
1285
2571
  guidedPrompts,
@@ -1343,8 +2629,18 @@ var initVoiceHTMXRoot = (root) => {
1343
2629
  render();
1344
2630
  }
1345
2631
  };
1346
- guidedVoice.subscribe(render);
1347
- generalVoice.subscribe(render);
2632
+ guidedVoice.subscribe(() => {
2633
+ if (guidedVoice.assistantAudio.length > 0) {
2634
+ guidedAudioPlayer.start().catch(() => {});
2635
+ }
2636
+ render();
2637
+ });
2638
+ generalVoice.subscribe(() => {
2639
+ if (generalVoice.assistantAudio.length > 0) {
2640
+ generalAudioPlayer.start().catch(() => {});
2641
+ }
2642
+ render();
2643
+ });
1348
2644
  startGuidedButton.addEventListener("click", () => {
1349
2645
  startMode("guided");
1350
2646
  });
@@ -1354,9 +2650,16 @@ var initVoiceHTMXRoot = (root) => {
1354
2650
  stopButton.addEventListener("click", () => {
1355
2651
  stopMic();
1356
2652
  });
2653
+ root.addEventListener("absolute-voice-simulate-disconnect", () => {
2654
+ currentVoice().simulateDisconnect();
2655
+ });
1357
2656
  window.addEventListener("beforeunload", () => {
1358
2657
  guidedVoice.stopRecording();
1359
2658
  generalVoice.stopRecording();
2659
+ guidedBargeInBinding?.close();
2660
+ generalBargeInBinding?.close();
2661
+ guidedAudioPlayer.close();
2662
+ generalAudioPlayer.close();
1360
2663
  stopGuidedBinding();
1361
2664
  stopGeneralBinding();
1362
2665
  guidedVoice.close();