@absolutejs/voice 0.0.22-beta.58 → 0.0.22-beta.580

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