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

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