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