@absolutejs/voice 0.0.22-beta.65 → 0.0.22-beta.651

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