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