@absolutejs/voice 0.0.22-beta.66 → 0.0.22-beta.660
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 +4550 -1180
- package/dist/angular/index.d.ts +36 -7
- package/dist/angular/index.js +4425 -513
- 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 +2 -2
- package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
- package/dist/angular/voice-provider-status.service.d.ts +2 -2
- package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
- package/dist/angular/voice-reconnect-profile-evidence.service.d.ts +12 -0
- package/dist/angular/voice-replay-timeline.service.d.ts +13 -0
- package/dist/angular/voice-routing-status.service.d.ts +1 -1
- package/dist/angular/voice-session-observability.service.d.ts +12 -0
- package/dist/angular/voice-session-snapshot.service.d.ts +13 -0
- package/dist/angular/voice-stream.service.d.ts +6 -2
- package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
- package/dist/angular/voice-turn-latency.service.d.ts +13 -0
- package/dist/angular/voice-turn-quality.service.d.ts +12 -0
- package/dist/angular/voice-widget.service.d.ts +18 -0
- package/dist/angular/voice-workflow-status.service.d.ts +2 -2
- package/dist/client/actions.d.ts +140 -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 +13 -4
- 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 +1760 -147
- package/dist/client/htmxDashboardRenderers.d.ts +71 -0
- package/dist/client/index.d.ts +109 -29
- package/dist/client/index.js +10886 -581
- package/dist/client/liveAgentConsole.d.ts +28 -0
- package/dist/client/liveCallViewer.d.ts +42 -0
- package/dist/client/liveOps.d.ts +22 -0
- package/dist/client/liveOpsWidget.d.ts +23 -0
- package/dist/client/liveTurnLatency.d.ts +41 -0
- package/dist/client/microphone.d.ts +5 -4
- package/dist/client/opsActionCenter.d.ts +56 -0
- package/dist/client/opsActionCenterWidget.d.ts +29 -0
- package/dist/client/opsActionHistory.d.ts +21 -0
- package/dist/client/opsActionHistoryWidget.d.ts +11 -0
- package/dist/client/opsStatus.d.ts +21 -0
- package/dist/client/opsStatusWidget.d.ts +10 -10
- package/dist/client/platformCoverage.d.ts +21 -0
- package/dist/client/platformCoverageWidget.d.ts +38 -0
- package/dist/client/profileComparison.d.ts +21 -0
- package/dist/client/profileComparisonWidget.d.ts +41 -0
- package/dist/client/profileSwitchRecommendation.d.ts +21 -0
- package/dist/client/profileSwitchRecommendationWidget.d.ts +12 -0
- package/dist/client/proofTrends.d.ts +21 -0
- package/dist/client/proofTrendsWidget.d.ts +38 -0
- package/dist/client/providerCapabilities.d.ts +5 -3
- package/dist/client/providerCapabilitiesWidget.d.ts +6 -6
- package/dist/client/providerContracts.d.ts +21 -0
- package/dist/client/providerContractsWidget.d.ts +37 -0
- package/dist/client/providerSimulationControls.d.ts +3 -3
- package/dist/client/providerSimulationControlsWidget.d.ts +5 -5
- package/dist/client/providerStatus.d.ts +5 -3
- package/dist/client/providerStatusWidget.d.ts +6 -6
- package/dist/client/reactiveSource.d.ts +8 -0
- package/dist/client/readinessFailures.d.ts +21 -0
- package/dist/client/readinessFailuresWidget.d.ts +42 -0
- package/dist/client/reconnectProfileEvidence.d.ts +21 -0
- package/dist/client/reconnectProfileEvidenceWidget.d.ts +39 -0
- package/dist/client/replayTimeline.d.ts +26 -0
- package/dist/client/routingStatus.d.ts +5 -3
- package/dist/client/routingStatusWidget.d.ts +10 -6
- package/dist/client/sessionObservability.d.ts +21 -0
- package/dist/client/sessionObservabilityWidget.d.ts +31 -0
- package/dist/client/sessionSnapshot.d.ts +23 -0
- package/dist/client/sessionSnapshotWidget.d.ts +33 -0
- package/dist/client/store.d.ts +1 -1
- package/dist/client/timeStretch.d.ts +5 -0
- package/dist/client/traceTimeline.d.ts +21 -0
- package/dist/client/traceTimelineWidget.d.ts +36 -0
- package/dist/client/turnLatency.d.ts +24 -0
- package/dist/client/turnLatencyWidget.d.ts +33 -0
- package/dist/client/turnQuality.d.ts +21 -0
- package/dist/client/turnQualityWidget.d.ts +32 -0
- package/dist/client/voiceWidgetView.d.ts +48 -0
- package/dist/client/workflowStatus.d.ts +5 -3
- package/dist/{agent.d.ts → core/agent.d.ts} +110 -7
- 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/{assistantHealth.d.ts → core/assistantHealth.d.ts} +9 -9
- package/dist/{assistantMemory.d.ts → core/assistantMemory.d.ts} +10 -10
- package/dist/core/assistantMode.d.ts +22 -0
- package/dist/{audioConditioning.d.ts → core/audioConditioning.d.ts} +2 -2
- package/dist/core/audit.d.ts +131 -0
- package/dist/core/auditDeliveryRoutes.d.ts +85 -0
- package/dist/core/auditExport.d.ts +34 -0
- package/dist/core/auditRoutes.d.ts +66 -0
- package/dist/core/auditSinks.d.ts +151 -0
- package/dist/core/backchannel.d.ts +25 -0
- package/dist/core/bargeInDetector.d.ts +51 -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 +90 -0
- package/dist/core/costPredictor.d.ts +74 -0
- package/dist/core/crmCallLogger.d.ts +37 -0
- package/dist/core/crmContract.d.ts +70 -0
- package/dist/core/dataControl.d.ts +180 -0
- package/dist/core/debugTiming.d.ts +11 -0
- package/dist/core/defineVoiceAssistant.d.ts +68 -0
- package/dist/core/deliveryRuntime.d.ts +159 -0
- package/dist/core/deliverySinkRoutes.d.ts +117 -0
- package/dist/core/demoReadyRoutes.d.ts +98 -0
- package/dist/{diagnosticsRoutes.d.ts → core/diagnosticsRoutes.d.ts} +2 -2
- package/dist/core/dncRegistry.d.ts +38 -0
- package/dist/core/dtmfCollector.d.ts +37 -0
- package/dist/{evalRoutes.d.ts → core/evalRoutes.d.ts} +26 -20
- package/dist/{fileStore.d.ts → core/fileStore.d.ts} +34 -20
- package/dist/core/guardrails.d.ts +128 -0
- package/dist/{handoff.d.ts → core/handoff.d.ts} +10 -10
- package/dist/{handoffHealth.d.ts → core/handoffHealth.d.ts} +9 -9
- package/dist/core/hardenedFetch.d.ts +1 -0
- package/dist/core/holdAudio.d.ts +23 -0
- package/dist/{htmx.d.ts → core/htmx.d.ts} +2 -2
- package/dist/core/htmxDashboardRoutes.d.ts +250 -0
- package/dist/core/iceServers.d.ts +34 -0
- package/dist/core/incidentBundle.d.ts +119 -0
- package/dist/core/incidentTimeline.d.ts +260 -0
- package/dist/core/ivrPlan.d.ts +40 -0
- package/dist/core/latencySlo.d.ts +56 -0
- package/dist/core/liveCoach.d.ts +43 -0
- package/dist/core/liveLatency.d.ts +78 -0
- package/dist/core/liveOps.d.ts +190 -0
- package/dist/core/llmJudge.d.ts +45 -0
- package/dist/{logger.d.ts → core/logger.d.ts} +1 -2
- package/dist/core/mcpToolset.d.ts +58 -0
- package/dist/core/mediaPipelineRoutes.d.ts +171 -0
- package/dist/core/mediaPipelineSurfaces.d.ts +48 -0
- package/dist/{memoryStore.d.ts → core/memoryStore.d.ts} +1 -1
- package/dist/core/midCallSummary.d.ts +27 -0
- package/dist/{modelAdapters.d.ts → core/modelAdapters.d.ts} +64 -7
- package/dist/core/monitor.d.ts +148 -0
- package/dist/core/multilingualProof.d.ts +77 -0
- package/dist/core/noShowPredictor.d.ts +46 -0
- package/dist/core/numberNormalizer.d.ts +1 -0
- package/dist/core/oauth2TokenSource.d.ts +21 -0
- package/dist/core/observabilityExport.d.ts +501 -0
- package/dist/core/openaiTTS.d.ts +18 -0
- package/dist/core/operationalStatus.d.ts +87 -0
- package/dist/core/operationsRecord.d.ts +371 -0
- package/dist/{ops.d.ts → core/ops.d.ts} +70 -70
- package/dist/core/opsActionAuditRoutes.d.ts +99 -0
- package/dist/{opsConsoleRoutes.d.ts → core/opsConsoleRoutes.d.ts} +11 -8
- package/dist/{opsPresets.d.ts → core/opsPresets.d.ts} +2 -2
- package/dist/core/opsRecovery.d.ts +137 -0
- package/dist/{opsRuntime.d.ts → core/opsRuntime.d.ts} +6 -6
- package/dist/{opsSinks.d.ts → core/opsSinks.d.ts} +19 -19
- package/dist/core/opsStatus.d.ts +76 -0
- package/dist/core/opsStatusRoutes.d.ts +33 -0
- package/dist/{opsWebhook.d.ts → core/opsWebhook.d.ts} +15 -15
- package/dist/core/otelExporter.d.ts +83 -0
- package/dist/core/outcomeContract.d.ts +146 -0
- package/dist/{outcomeRecipes.d.ts → core/outcomeRecipes.d.ts} +4 -4
- package/dist/core/pathway.d.ts +94 -0
- package/dist/core/pathwayCompiler.d.ts +31 -0
- package/dist/core/pathwayGenerator.d.ts +27 -0
- package/dist/core/pathwayRuntime.d.ts +57 -0
- package/dist/core/pathwaySlotCollector.d.ts +29 -0
- package/dist/core/pathwayVisualizer.d.ts +8 -0
- package/dist/core/phoneAgent.d.ts +139 -0
- package/dist/core/phoneAgentProductionSmoke.d.ts +115 -0
- package/dist/core/phoneProvisioning.d.ts +29 -0
- package/dist/core/platformCoverage.d.ts +91 -0
- package/dist/{plugin.d.ts → core/plugin.d.ts} +11 -8
- package/dist/core/postCallAnalysis.d.ts +98 -0
- package/dist/core/postCallSurvey.d.ts +41 -0
- package/dist/{postgresStore.d.ts → core/postgresStore.d.ts} +20 -9
- package/dist/{presets.d.ts → core/presets.d.ts} +3 -3
- package/dist/core/productionReadiness.d.ts +757 -0
- package/dist/core/profileSwitchRecommendation.d.ts +350 -0
- package/dist/core/promptInjectionGuard.d.ts +30 -0
- package/dist/core/proofAssertions.d.ts +32 -0
- package/dist/core/proofPack.d.ts +211 -0
- package/dist/core/proofRunner.d.ts +79 -0
- package/dist/core/proofTrends.d.ts +966 -0
- package/dist/{providerAdapters.d.ts → core/providerAdapters.d.ts} +5 -5
- package/dist/{providerCapabilities.d.ts → core/providerCapabilities.d.ts} +9 -9
- package/dist/core/providerDecisionTraces.d.ts +130 -0
- package/dist/{providerHealth.d.ts → core/providerHealth.d.ts} +15 -5
- package/dist/core/providerOrchestration.d.ts +109 -0
- package/dist/core/providerRouterTraces.d.ts +35 -0
- package/dist/core/providerRoutingContract.d.ts +71 -0
- package/dist/core/providerSlo.d.ts +142 -0
- package/dist/core/providerStackRecommendations.d.ts +188 -0
- package/dist/core/qualityDriftDetector.d.ts +44 -0
- package/dist/{qualityRoutes.d.ts → core/qualityRoutes.d.ts} +7 -7
- package/dist/{queue.d.ts → core/queue.d.ts} +48 -39
- package/dist/core/ragTool.d.ts +52 -0
- package/dist/core/readinessProfiles.d.ts +45 -0
- package/dist/core/realtimeChannel.d.ts +136 -0
- package/dist/core/realtimeProviderContracts.d.ts +133 -0
- package/dist/core/reconnectContract.d.ts +177 -0
- package/dist/core/recordingRedaction.d.ts +47 -0
- package/dist/core/recordingStore.d.ts +60 -0
- package/dist/core/redaction.d.ts +13 -0
- package/dist/core/reminderScheduler.d.ts +43 -0
- package/dist/{resilienceRoutes.d.ts → core/resilienceRoutes.d.ts} +34 -5
- package/dist/core/retention.d.ts +37 -0
- package/dist/core/retryPolicy.d.ts +38 -0
- package/dist/core/routeAuth.d.ts +58 -0
- package/dist/{routing.d.ts → core/routing.d.ts} +2 -2
- package/dist/{runtimeOps.d.ts → core/runtimeOps.d.ts} +3 -3
- package/dist/{s3Store.d.ts → core/s3Store.d.ts} +12 -3
- package/dist/core/scorecardCalibration.d.ts +31 -0
- package/dist/core/scribe.d.ts +50 -0
- package/dist/core/semanticTurn.d.ts +37 -0
- package/dist/{session.d.ts → core/session.d.ts} +1 -1
- package/dist/core/sessionObservability.d.ts +145 -0
- package/dist/{sessionReplay.d.ts → core/sessionReplay.d.ts} +31 -19
- package/dist/core/sessionSnapshot.d.ts +109 -0
- package/dist/core/simulationSuite.d.ts +144 -0
- package/dist/core/sloCalibration.d.ts +185 -0
- package/dist/{sqliteStore.d.ts → core/sqliteStore.d.ts} +20 -9
- package/dist/{store.d.ts → core/store.d.ts} +1 -1
- package/dist/core/supervisorPermissions.d.ts +33 -0
- package/dist/core/supervisorPresence.d.ts +49 -0
- package/dist/core/telephonyMediaRoutes.d.ts +72 -0
- package/dist/core/telephonyOutcome.d.ts +269 -0
- package/dist/{toolContract.d.ts → core/toolContract.d.ts} +46 -15
- package/dist/{toolRuntime.d.ts → core/toolRuntime.d.ts} +4 -4
- 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 +1755 -0
- package/dist/core/vapiAdapter.d.ts +160 -0
- package/dist/core/variableAnalytics.d.ts +47 -0
- package/dist/core/voiceConfiguration.d.ts +8 -0
- package/dist/core/voiceMonitoring.d.ts +444 -0
- package/dist/core/webhookFanout.d.ts +48 -0
- package/dist/core/webhookVerification.d.ts +27 -0
- package/dist/core/whisperChannel.d.ts +50 -0
- package/dist/{workflowContract.d.ts → core/workflowContract.d.ts} +21 -21
- package/dist/core/writeBehindStore.d.ts +41 -0
- package/dist/core/zeroDataRetention.d.ts +31 -0
- package/dist/drizzle/assistantMemory.d.ts +100 -0
- package/dist/drizzle/eval.d.ts +55 -0
- package/dist/drizzle/handoff.d.ts +56 -0
- package/dist/drizzle/incidentBundle.d.ts +55 -0
- package/dist/drizzle/index.d.ts +991 -0
- package/dist/drizzle/index.js +3059 -0
- package/dist/drizzle/observabilityExport.d.ts +55 -0
- package/dist/drizzle/proofTrends.d.ts +114 -0
- package/dist/drizzle/runtimeStorage.d.ts +1183 -0
- package/dist/drizzle/shared.d.ts +69 -0
- package/dist/embed/index.d.ts +38 -0
- package/dist/embed/index.js +2064 -0
- package/dist/embed/voice-widget.js +10 -0
- package/dist/index.d.ts +379 -80
- package/dist/index.js +48304 -8042
- 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/manifest.d.ts +17 -0
- package/dist/manifest.js +424 -0
- package/dist/manifest.json +427 -0
- package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
- package/dist/react/VoiceCallDebuggerLaunch.d.ts +6 -0
- package/dist/react/VoiceCallPlayer.d.ts +11 -0
- package/dist/react/VoiceCostDashboard.d.ts +10 -0
- package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
- package/dist/react/VoiceLiveAgentConsole.d.ts +11 -0
- package/dist/react/VoiceLiveCallViewer.d.ts +9 -0
- package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
- package/dist/react/VoiceOpsStatus.d.ts +1 -1
- package/dist/react/VoicePlatformCoverage.d.ts +6 -0
- package/dist/react/VoiceProfileComparison.d.ts +6 -0
- package/dist/react/VoiceProfileSwitchRecommendation.d.ts +6 -0
- package/dist/react/VoiceProofTrends.d.ts +6 -0
- package/dist/react/VoiceProviderCapabilities.d.ts +1 -1
- package/dist/react/VoiceProviderContracts.d.ts +6 -0
- package/dist/react/VoiceProviderSimulationControls.d.ts +1 -1
- package/dist/react/VoiceProviderStatus.d.ts +1 -1
- package/dist/react/VoiceReadinessFailures.d.ts +6 -0
- package/dist/react/VoiceReconnectProfileEvidence.d.ts +6 -0
- package/dist/react/VoiceReplayTimeline.d.ts +6 -0
- package/dist/react/VoiceRoutingStatus.d.ts +1 -1
- package/dist/react/VoiceSessionObservability.d.ts +6 -0
- package/dist/react/VoiceSessionSnapshot.d.ts +6 -0
- package/dist/react/VoiceTraceTimeline.d.ts +6 -0
- package/dist/react/VoiceTurnLatency.d.ts +6 -0
- package/dist/react/VoiceTurnQuality.d.ts +6 -0
- package/dist/react/VoiceWidget.d.ts +13 -0
- package/dist/react/index.d.ts +80 -13
- package/dist/react/index.js +13728 -1784
- 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 +6 -2
- 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 +1 -1
- package/dist/react/useVoiceProviderContracts.d.ts +8 -0
- package/dist/react/useVoiceProviderSimulationControls.d.ts +1 -1
- package/dist/react/useVoiceProviderStatus.d.ts +1 -1
- package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
- package/dist/react/useVoiceReconnectProfileEvidence.d.ts +8 -0
- package/dist/react/useVoiceRoutingStatus.d.ts +1 -1
- package/dist/react/useVoiceSessionObservability.d.ts +8 -0
- package/dist/react/useVoiceSessionSnapshot.d.ts +9 -0
- package/dist/react/useVoiceStream.d.ts +6 -2
- package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
- package/dist/react/useVoiceTurnLatency.d.ts +9 -0
- package/dist/react/useVoiceTurnQuality.d.ts +8 -0
- package/dist/react/useVoiceWorkflowStatus.d.ts +1 -1
- package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/svelte/createVoiceCallDebugger.d.ts +10 -0
- package/dist/svelte/createVoiceCallPlayer.d.ts +33 -0
- package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
- package/dist/svelte/createVoiceCostDashboard.d.ts +13 -0
- package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
- package/dist/svelte/createVoiceLiveAgentConsole.d.ts +23 -0
- package/dist/svelte/createVoiceLiveCallViewer.d.ts +26 -0
- package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
- package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
- package/dist/svelte/createVoiceOpsStatus.d.ts +4 -4
- package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
- package/dist/svelte/createVoiceProfileComparison.d.ts +7 -0
- package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
- package/dist/svelte/createVoiceProviderCapabilities.d.ts +2 -2
- package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
- package/dist/svelte/createVoiceProviderSimulationControls.d.ts +2 -2
- package/dist/svelte/createVoiceProviderStatus.d.ts +2 -2
- package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
- package/dist/svelte/createVoiceReconnectProfileEvidence.d.ts +7 -0
- package/dist/svelte/createVoiceReplayTimeline.d.ts +13 -0
- package/dist/svelte/createVoiceRoutingStatus.d.ts +2 -2
- package/dist/svelte/createVoiceSessionObservability.d.ts +10 -0
- package/dist/svelte/createVoiceSessionSnapshot.d.ts +11 -0
- package/dist/svelte/createVoiceStream.d.ts +1 -1
- package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
- package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
- package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
- package/dist/svelte/createVoiceWidget.d.ts +19 -0
- package/dist/svelte/createVoiceWorkflowStatus.d.ts +2 -2
- package/dist/svelte/index.d.ts +37 -9
- package/dist/svelte/index.js +7494 -1540
- 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 +153 -15
- package/dist/testing/accuracy.d.ts +18 -1
- package/dist/testing/audioMatrix.d.ts +22 -0
- package/dist/testing/benchmark.d.ts +24 -15
- package/dist/testing/confidenceCalibration.d.ts +19 -0
- package/dist/testing/conformance.d.ts +11 -0
- package/dist/testing/corrected.d.ts +9 -9
- package/dist/testing/criticalFields.d.ts +22 -0
- package/dist/testing/duplex.d.ts +5 -5
- package/dist/testing/fixtures.d.ts +7 -4
- package/dist/testing/index.d.ts +21 -13
- package/dist/testing/index.js +8889 -1153
- package/dist/testing/ioProviderSimulator.d.ts +5 -5
- package/dist/testing/outcomes.d.ts +12 -0
- package/dist/testing/provenance.d.ts +44 -0
- package/dist/testing/providerSimulator.d.ts +5 -5
- package/dist/testing/review.d.ts +8 -8
- package/dist/testing/routingBenchmark.d.ts +16 -0
- package/dist/testing/sessionBenchmark.d.ts +13 -13
- package/dist/testing/statistics.d.ts +30 -0
- package/dist/testing/stt.d.ts +3 -3
- package/dist/testing/telephony.d.ts +27 -2
- package/dist/testing/tts.d.ts +2 -2
- package/dist/vue/VoiceCallDebuggerLaunch.d.ts +72 -0
- package/dist/vue/VoiceCallPlayer.d.ts +40 -0
- package/dist/vue/VoiceCostDashboard.d.ts +57 -0
- package/dist/vue/VoiceDeliveryRuntime.d.ts +34 -0
- package/dist/vue/VoiceLiveAgentConsole.d.ts +50 -0
- package/dist/vue/VoiceLiveCallViewer.d.ts +35 -0
- package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
- package/dist/vue/VoiceOpsStatus.d.ts +4 -0
- package/dist/vue/VoicePlatformCoverage.d.ts +27 -0
- package/dist/vue/VoiceProofTrends.d.ts +25 -0
- package/dist/vue/VoiceProviderCapabilities.d.ts +4 -0
- package/dist/vue/VoiceProviderContracts.d.ts +25 -0
- package/dist/vue/VoiceProviderSimulationControls.d.ts +3 -3
- package/dist/vue/VoiceProviderStatus.d.ts +4 -0
- package/dist/vue/VoiceReadinessFailures.d.ts +25 -0
- package/dist/vue/VoiceReconnectProfileEvidence.d.ts +25 -0
- package/dist/vue/VoiceReplayTimeline.d.ts +17 -0
- package/dist/vue/VoiceRoutingStatus.d.ts +4 -0
- package/dist/vue/VoiceSessionObservability.d.ts +27 -0
- package/dist/vue/VoiceSessionSnapshot.d.ts +72 -0
- package/dist/vue/VoiceTurnLatency.d.ts +73 -0
- package/dist/vue/VoiceTurnQuality.d.ts +55 -0
- package/dist/vue/VoiceWidget.d.ts +77 -0
- package/dist/vue/index.d.ts +49 -13
- package/dist/vue/index.js +13050 -1931
- 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 +3 -3
- package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
- package/dist/vue/useVoiceProviderSimulationControls.d.ts +2 -2
- package/dist/vue/useVoiceProviderStatus.d.ts +3 -3
- package/dist/vue/useVoiceReadinessFailures.d.ts +959 -0
- package/dist/vue/useVoiceReconnectProfileEvidence.d.ts +9 -0
- package/dist/vue/useVoiceRoutingStatus.d.ts +2 -2
- package/dist/vue/useVoiceSessionObservability.d.ts +9 -0
- package/dist/vue/useVoiceSessionSnapshot.d.ts +10 -0
- package/dist/vue/useVoiceStream.d.ts +10 -6
- package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
- package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
- package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
- package/dist/vue/useVoiceWorkflowStatus.d.ts +3 -3
- package/package.json +247 -256
- package/dist/angular/voice-app-kit-status.service.d.ts +0 -12
- package/dist/angular/voice-ops-status.component.d.ts +0 -15
- package/dist/appKit.d.ts +0 -94
- package/dist/client/appKitStatus.d.ts +0 -19
- package/dist/react/useVoiceAppKitStatus.d.ts +0 -8
- package/dist/svelte/createVoiceAppKitStatus.d.ts +0 -8
- package/dist/turnProfiles.d.ts +0 -6
- package/dist/types.d.ts +0 -968
- package/dist/vue/useVoiceAppKitStatus.d.ts +0 -9
- package/fixtures/README.md +0 -57
- package/fixtures/manifest.json +0 -199
- package/fixtures/pcm/dialogue-three-clean.pcm +0 -0
- package/fixtures/pcm/dialogue-three-mixed.pcm +0 -0
- package/fixtures/pcm/dialogue-two-clean.pcm +0 -0
- package/fixtures/pcm/dialogue-two-noisy.pcm +0 -0
- package/fixtures/pcm/multiturn-three-mixed.pcm +0 -0
- package/fixtures/pcm/multiturn-two-clean.pcm +0 -0
- package/fixtures/pcm/quietly-alone-clean.pcm +0 -0
- package/fixtures/pcm/rainstorms-noisy.pcm +0 -0
- package/fixtures/pcm/stella-bulgaria-bulgarian20.pcm +0 -0
- package/fixtures/pcm/stella-ghana-english507.pcm +0 -0
- package/fixtures/pcm/stella-india-english37.pcm +0 -0
- package/fixtures/pcm/stella-jamaica-jamaican-creole-english1.pcm +0 -0
- package/fixtures/pcm/stella-liberia-liberian-pidgin-english2.pcm +0 -0
- package/fixtures/pcm/stella-pakistan-english519.pcm +0 -0
- package/fixtures/pcm/stella-sierra-leone-krio5.pcm +0 -0
- package/fixtures/pcm/stella-singapore-english655.pcm +0 -0
- package/fixtures/pcm/traveled-back-route-clean.pcm +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/internal/html.ts
|
|
2
|
+
var escapeHtml = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3
|
+
|
|
1
4
|
// src/client/htmx.ts
|
|
2
5
|
var DEFAULT_EVENT_NAME = "voice-refresh";
|
|
3
6
|
var DEFAULT_QUERY_PARAM = "sessionId";
|
|
@@ -100,37 +103,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
100
103
|
let sourceNode = null;
|
|
101
104
|
let processorNode = null;
|
|
102
105
|
let mediaStream = null;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
let lifecycleGeneration = 0;
|
|
107
|
+
let startPromise = null;
|
|
108
|
+
const release = (resources) => {
|
|
109
|
+
resources.processorNode?.disconnect();
|
|
110
|
+
resources.sourceNode?.disconnect();
|
|
111
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
112
|
+
resources.audioContext?.close();
|
|
113
|
+
};
|
|
114
|
+
const start = () => {
|
|
115
|
+
if (mediaStream)
|
|
116
|
+
return Promise.resolve();
|
|
117
|
+
if (startPromise)
|
|
118
|
+
return startPromise;
|
|
119
|
+
const generation = ++lifecycleGeneration;
|
|
120
|
+
const operation = async () => {
|
|
121
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
122
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
123
|
+
}
|
|
124
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
125
|
+
if (!AudioContextCtor) {
|
|
126
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
127
|
+
}
|
|
128
|
+
let pendingAudioContext = null;
|
|
129
|
+
let pendingMediaStream = null;
|
|
130
|
+
let pendingProcessorNode = null;
|
|
131
|
+
let pendingSourceNode = null;
|
|
132
|
+
try {
|
|
133
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
134
|
+
audio: {
|
|
135
|
+
autoGainControl: true,
|
|
136
|
+
channelCount: options.channelCount ?? 1,
|
|
137
|
+
echoCancellation: true,
|
|
138
|
+
noiseSuppression: true
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (generation !== lifecycleGeneration) {
|
|
142
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
143
|
+
}
|
|
144
|
+
pendingAudioContext = new AudioContextCtor;
|
|
145
|
+
if (pendingAudioContext.state === "suspended") {
|
|
146
|
+
await pendingAudioContext.resume();
|
|
147
|
+
}
|
|
148
|
+
if (generation !== lifecycleGeneration) {
|
|
149
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
150
|
+
}
|
|
151
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
152
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
153
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
154
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
155
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
156
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
157
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
158
|
+
options.onAudio(pcm);
|
|
159
|
+
};
|
|
160
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
161
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
162
|
+
if (generation !== lifecycleGeneration) {
|
|
163
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
164
|
+
}
|
|
165
|
+
audioContext = pendingAudioContext;
|
|
166
|
+
mediaStream = pendingMediaStream;
|
|
167
|
+
processorNode = pendingProcessorNode;
|
|
168
|
+
sourceNode = pendingSourceNode;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
release({
|
|
171
|
+
audioContext: pendingAudioContext,
|
|
172
|
+
mediaStream: pendingMediaStream,
|
|
173
|
+
processorNode: pendingProcessorNode,
|
|
174
|
+
sourceNode: pendingSourceNode
|
|
175
|
+
});
|
|
176
|
+
throw error;
|
|
114
177
|
}
|
|
115
|
-
});
|
|
116
|
-
audioContext = new AudioContextCtor;
|
|
117
|
-
sourceNode = audioContext.createMediaStreamSource(mediaStream);
|
|
118
|
-
processorNode = audioContext.createScriptProcessor(4096, 1, 1);
|
|
119
|
-
processorNode.onaudioprocess = (event) => {
|
|
120
|
-
const channel = event.inputBuffer.getChannelData(0);
|
|
121
|
-
const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
122
|
-
const pcm = floatTo16BitPCM(downsampled);
|
|
123
|
-
options.onLevel?.(getPcmLevel(pcm));
|
|
124
|
-
options.onAudio(pcm);
|
|
125
178
|
};
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
const pending = operation().finally(() => {
|
|
180
|
+
if (startPromise === pending)
|
|
181
|
+
startPromise = null;
|
|
182
|
+
});
|
|
183
|
+
startPromise = pending;
|
|
184
|
+
return pending;
|
|
128
185
|
};
|
|
129
186
|
const stop = () => {
|
|
130
|
-
|
|
131
|
-
sourceNode
|
|
132
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
133
|
-
audioContext?.close();
|
|
187
|
+
lifecycleGeneration += 1;
|
|
188
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
134
189
|
options.onLevel?.(0);
|
|
135
190
|
audioContext = null;
|
|
136
191
|
mediaStream = null;
|
|
@@ -181,13 +236,25 @@ var serverMessageToAction = (message) => {
|
|
|
181
236
|
case "assistant":
|
|
182
237
|
return {
|
|
183
238
|
text: message.text,
|
|
239
|
+
turnId: message.turnId,
|
|
184
240
|
type: "assistant"
|
|
185
241
|
};
|
|
242
|
+
case "assistant_delta":
|
|
243
|
+
return {
|
|
244
|
+
delta: message.delta,
|
|
245
|
+
turnId: message.turnId,
|
|
246
|
+
type: "assistant_delta"
|
|
247
|
+
};
|
|
186
248
|
case "complete":
|
|
187
249
|
return {
|
|
188
250
|
sessionId: message.sessionId,
|
|
189
251
|
type: "complete"
|
|
190
252
|
};
|
|
253
|
+
case "connection":
|
|
254
|
+
return {
|
|
255
|
+
reconnect: message.reconnect,
|
|
256
|
+
type: "connection"
|
|
257
|
+
};
|
|
191
258
|
case "call_lifecycle":
|
|
192
259
|
return {
|
|
193
260
|
event: message.event,
|
|
@@ -197,8 +264,11 @@ var serverMessageToAction = (message) => {
|
|
|
197
264
|
case "error":
|
|
198
265
|
return {
|
|
199
266
|
message: normalizeErrorMessage(message.message),
|
|
267
|
+
recoverable: message.recoverable,
|
|
200
268
|
type: "error"
|
|
201
269
|
};
|
|
270
|
+
case "call_control_ack":
|
|
271
|
+
return null;
|
|
202
272
|
case "final":
|
|
203
273
|
return {
|
|
204
274
|
transcript: message.transcript,
|
|
@@ -209,9 +279,22 @@ var serverMessageToAction = (message) => {
|
|
|
209
279
|
transcript: message.transcript,
|
|
210
280
|
type: "partial"
|
|
211
281
|
};
|
|
282
|
+
case "replay":
|
|
283
|
+
return {
|
|
284
|
+
assistantTexts: message.assistantTexts,
|
|
285
|
+
call: message.call,
|
|
286
|
+
partial: message.partial,
|
|
287
|
+
scenarioId: message.scenarioId,
|
|
288
|
+
sessionId: message.sessionId,
|
|
289
|
+
sessionMetadata: message.sessionMetadata,
|
|
290
|
+
status: message.status,
|
|
291
|
+
turns: message.turns,
|
|
292
|
+
type: "replay"
|
|
293
|
+
};
|
|
212
294
|
case "session":
|
|
213
295
|
return {
|
|
214
296
|
sessionId: message.sessionId,
|
|
297
|
+
sessionMetadata: message.sessionMetadata,
|
|
215
298
|
scenarioId: message.scenarioId,
|
|
216
299
|
status: message.status,
|
|
217
300
|
type: "session"
|
|
@@ -226,30 +309,276 @@ var serverMessageToAction = (message) => {
|
|
|
226
309
|
}
|
|
227
310
|
};
|
|
228
311
|
|
|
312
|
+
// node_modules/@absolutejs/media/dist/index.js
|
|
313
|
+
var TAU = Math.PI * 2;
|
|
314
|
+
var pushIssue = (issues, severity, code, message) => {
|
|
315
|
+
issues.push({ code, message, severity });
|
|
316
|
+
};
|
|
317
|
+
var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
|
|
318
|
+
var max = (values) => values.length === 0 ? undefined : Math.max(...values);
|
|
319
|
+
var numericStat = (stat, key) => {
|
|
320
|
+
const value = stat[key];
|
|
321
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
322
|
+
};
|
|
323
|
+
var booleanStat = (stat, key) => {
|
|
324
|
+
const value = stat[key];
|
|
325
|
+
return typeof value === "boolean" ? value : undefined;
|
|
326
|
+
};
|
|
327
|
+
var stringStat = (stat, key) => {
|
|
328
|
+
const value = stat[key];
|
|
329
|
+
return typeof value === "string" ? value : undefined;
|
|
330
|
+
};
|
|
331
|
+
var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
|
|
332
|
+
var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
333
|
+
var normalizeWebRTCStat = (stat) => {
|
|
334
|
+
const sample = {};
|
|
335
|
+
for (const [key, value] of Object.entries(stat)) {
|
|
336
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
337
|
+
sample[key] = value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return sample;
|
|
341
|
+
};
|
|
342
|
+
var buildMediaWebRTCStatsReport = (input = {}) => {
|
|
343
|
+
const stats = input.stats ?? [];
|
|
344
|
+
const issues = [];
|
|
345
|
+
const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
346
|
+
const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
347
|
+
const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
|
|
348
|
+
const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
|
|
349
|
+
const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
|
|
350
|
+
const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
|
|
351
|
+
const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
|
|
352
|
+
const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
|
|
353
|
+
const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
|
|
354
|
+
const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
|
|
355
|
+
const packetLossDenominator = inboundPackets + packetsLost;
|
|
356
|
+
const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
|
|
357
|
+
const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
|
|
358
|
+
const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
|
|
359
|
+
const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
|
|
360
|
+
const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
|
|
361
|
+
const jitterBufferDelayMs = max(inbound.map((stat) => {
|
|
362
|
+
const delay = numericStat(stat, "jitterBufferDelay");
|
|
363
|
+
const emitted = numericStat(stat, "jitterBufferEmittedCount");
|
|
364
|
+
return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
|
|
365
|
+
}).filter((value) => value !== undefined));
|
|
366
|
+
const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
|
|
367
|
+
if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
|
|
368
|
+
pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
|
|
369
|
+
}
|
|
370
|
+
if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
|
|
371
|
+
pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
|
|
372
|
+
}
|
|
373
|
+
if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
|
|
374
|
+
pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
|
|
375
|
+
}
|
|
376
|
+
if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
|
|
377
|
+
pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
|
|
378
|
+
}
|
|
379
|
+
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
380
|
+
pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
activeCandidatePairs,
|
|
384
|
+
audioLevelAverage: average(audioLevels),
|
|
385
|
+
bytesReceived,
|
|
386
|
+
bytesSent,
|
|
387
|
+
checkedAt: Date.now(),
|
|
388
|
+
endedAudioTracks,
|
|
389
|
+
inboundPackets,
|
|
390
|
+
issues,
|
|
391
|
+
jitterBufferDelayMs,
|
|
392
|
+
jitterMs,
|
|
393
|
+
liveAudioTracks,
|
|
394
|
+
outboundPackets,
|
|
395
|
+
packetLossRatio,
|
|
396
|
+
packetsLost,
|
|
397
|
+
roundTripTimeMs,
|
|
398
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
399
|
+
totalStats: stats.length
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
var collectMediaWebRTCStats = async (input) => {
|
|
403
|
+
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
404
|
+
return [...report.values()].map(normalizeWebRTCStat);
|
|
405
|
+
};
|
|
406
|
+
var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
|
|
407
|
+
const stats = input.stats ?? [];
|
|
408
|
+
const previousStats = input.previousStats ?? [];
|
|
409
|
+
const issues = [];
|
|
410
|
+
const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
|
|
411
|
+
const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
|
|
412
|
+
const streams = audioRtp.map((stat) => {
|
|
413
|
+
const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
|
|
414
|
+
const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
|
|
415
|
+
const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
|
|
416
|
+
const previous = previousByKey.get(statKey(stat));
|
|
417
|
+
const currentPackets = numericStat(stat, packetsKey);
|
|
418
|
+
const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
|
|
419
|
+
const currentBytes = numericStat(stat, bytesKey);
|
|
420
|
+
const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
|
|
421
|
+
const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
|
|
422
|
+
return {
|
|
423
|
+
bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
|
|
424
|
+
currentPackets,
|
|
425
|
+
direction,
|
|
426
|
+
id: statKey(stat),
|
|
427
|
+
packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
|
|
428
|
+
previousPackets,
|
|
429
|
+
timeDeltaMs
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
const inbound = streams.filter((stream) => stream.direction === "inbound");
|
|
433
|
+
const outbound = streams.filter((stream) => stream.direction === "outbound");
|
|
434
|
+
const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
|
|
435
|
+
const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
436
|
+
const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
437
|
+
if (input.requireInboundAudio && inbound.length === 0) {
|
|
438
|
+
pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
|
|
439
|
+
}
|
|
440
|
+
if (input.requireOutboundAudio && outbound.length === 0) {
|
|
441
|
+
pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
|
|
442
|
+
}
|
|
443
|
+
if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
|
|
444
|
+
pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
|
|
445
|
+
}
|
|
446
|
+
if (stalledInboundStreams > 0) {
|
|
447
|
+
pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
|
|
448
|
+
}
|
|
449
|
+
if (stalledOutboundStreams > 0) {
|
|
450
|
+
pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
checkedAt: Date.now(),
|
|
454
|
+
inboundAudioStreams: inbound.length,
|
|
455
|
+
issues,
|
|
456
|
+
maxObservedGapMs,
|
|
457
|
+
outboundAudioStreams: outbound.length,
|
|
458
|
+
stalledInboundStreams,
|
|
459
|
+
stalledOutboundStreams,
|
|
460
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
461
|
+
streams,
|
|
462
|
+
totalStats: stats.length
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/client/browserMedia.ts
|
|
467
|
+
var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
|
|
468
|
+
var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
|
|
469
|
+
var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
|
|
470
|
+
var postBrowserMediaReport = async (payload, options) => {
|
|
471
|
+
const requestFetch = options.fetch ?? globalThis.fetch;
|
|
472
|
+
if (!requestFetch) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
|
|
476
|
+
body: JSON.stringify(payload),
|
|
477
|
+
headers: {
|
|
478
|
+
"Content-Type": "application/json"
|
|
479
|
+
},
|
|
480
|
+
keepalive: true,
|
|
481
|
+
method: "POST"
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
var createVoiceBrowserMediaReporter = (options) => {
|
|
485
|
+
let interval = null;
|
|
486
|
+
let previousStats = [];
|
|
487
|
+
const reportOnce = async () => {
|
|
488
|
+
const peerConnection = await resolvePeerConnection(options);
|
|
489
|
+
if (!peerConnection) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const stats = await collectMediaWebRTCStats({ peerConnection });
|
|
493
|
+
const report = buildMediaWebRTCStatsReport({
|
|
494
|
+
...options,
|
|
495
|
+
stats
|
|
496
|
+
});
|
|
497
|
+
const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
|
|
498
|
+
...options.continuity,
|
|
499
|
+
previousStats,
|
|
500
|
+
stats
|
|
501
|
+
});
|
|
502
|
+
const payload = {
|
|
503
|
+
at: Date.now(),
|
|
504
|
+
continuity,
|
|
505
|
+
report,
|
|
506
|
+
scenarioId: options.getScenarioId?.() ?? null,
|
|
507
|
+
sessionId: options.getSessionId?.() ?? null
|
|
508
|
+
};
|
|
509
|
+
previousStats = stats;
|
|
510
|
+
options.onReport?.(payload);
|
|
511
|
+
await postBrowserMediaReport(payload, options);
|
|
512
|
+
return payload;
|
|
513
|
+
};
|
|
514
|
+
const run = () => {
|
|
515
|
+
reportOnce().catch((error) => {
|
|
516
|
+
options.onError?.(error);
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
const stop = () => {
|
|
520
|
+
if (interval) {
|
|
521
|
+
clearInterval(interval);
|
|
522
|
+
interval = null;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
close: stop,
|
|
527
|
+
reportOnce,
|
|
528
|
+
stop,
|
|
529
|
+
start: () => {
|
|
530
|
+
if (interval) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
run();
|
|
534
|
+
interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
|
|
229
539
|
// src/client/connection.ts
|
|
230
540
|
var WS_OPEN = 1;
|
|
231
541
|
var WS_CLOSED = 3;
|
|
232
542
|
var WS_NORMAL_CLOSURE = 1000;
|
|
233
|
-
var
|
|
543
|
+
var WS_UNAUTHORIZED = 4401;
|
|
544
|
+
var WS_FORBIDDEN = 4403;
|
|
545
|
+
var WS_INITIALIZATION_FAILED = 4500;
|
|
546
|
+
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
234
547
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
235
|
-
var
|
|
548
|
+
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
549
|
+
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
550
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
551
|
+
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
552
|
+
var RECONNECT_BASE_DELAY_MS = 500;
|
|
553
|
+
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
554
|
+
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
236
555
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
556
|
+
|
|
557
|
+
class VoiceReconnectRejectedError extends Error {
|
|
558
|
+
constructor(message = "Voice authorization could not be renewed.") {
|
|
559
|
+
super(message);
|
|
560
|
+
this.name = "VoiceReconnectRejectedError";
|
|
561
|
+
}
|
|
562
|
+
}
|
|
237
563
|
var noop = () => {};
|
|
564
|
+
var noopAsync = () => Promise.resolve();
|
|
238
565
|
var noopUnsubscribe = () => noop;
|
|
239
566
|
var NOOP_CONNECTION = {
|
|
240
|
-
callControl:
|
|
567
|
+
callControl: noopAsync,
|
|
241
568
|
close: noop,
|
|
569
|
+
disconnect: noop,
|
|
242
570
|
endTurn: noop,
|
|
571
|
+
send: noop,
|
|
572
|
+
sendAudio: noop,
|
|
573
|
+
simulateDisconnect: noop,
|
|
574
|
+
subscribe: noopUnsubscribe,
|
|
243
575
|
getReadyState: () => WS_CLOSED,
|
|
244
576
|
getScenarioId: () => "",
|
|
245
577
|
getSessionId: () => "",
|
|
246
|
-
|
|
247
|
-
sendAudio: noop,
|
|
248
|
-
start: () => {},
|
|
249
|
-
subscribe: noopUnsubscribe
|
|
578
|
+
start: () => {}
|
|
250
579
|
};
|
|
251
580
|
var createSessionId = () => crypto.randomUUID();
|
|
252
|
-
var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
581
|
+
var buildWsUrl = (path, sessionId, scenarioId, query) => {
|
|
253
582
|
const { hostname, port, protocol } = window.location;
|
|
254
583
|
const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
|
|
255
584
|
const portSuffix = port ? `:${port}` : "";
|
|
@@ -258,6 +587,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
|
258
587
|
if (scenarioId) {
|
|
259
588
|
url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
|
|
260
589
|
}
|
|
590
|
+
for (const [key, value] of Object.entries(query)) {
|
|
591
|
+
if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
|
|
592
|
+
url.searchParams.set(key, value);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
261
595
|
return url.toString();
|
|
262
596
|
};
|
|
263
597
|
var isVoiceServerMessage = (value) => {
|
|
@@ -268,11 +602,14 @@ var isVoiceServerMessage = (value) => {
|
|
|
268
602
|
case "audio":
|
|
269
603
|
case "assistant":
|
|
270
604
|
case "call_lifecycle":
|
|
605
|
+
case "call_control_ack":
|
|
271
606
|
case "complete":
|
|
607
|
+
case "connection":
|
|
272
608
|
case "error":
|
|
273
609
|
case "final":
|
|
274
610
|
case "partial":
|
|
275
611
|
case "pong":
|
|
612
|
+
case "replay":
|
|
276
613
|
case "session":
|
|
277
614
|
case "turn":
|
|
278
615
|
return true;
|
|
@@ -296,19 +633,45 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
296
633
|
return NOOP_CONNECTION;
|
|
297
634
|
}
|
|
298
635
|
const listeners = new Set;
|
|
636
|
+
const pendingCallControls = new Map;
|
|
299
637
|
const shouldReconnect = options.reconnect !== false;
|
|
300
638
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
639
|
+
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
301
640
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
641
|
+
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
642
|
+
let lifecycleGeneration = 0;
|
|
643
|
+
let disposed = false;
|
|
644
|
+
let reconnectPreparationAbort = null;
|
|
645
|
+
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
302
646
|
const state = {
|
|
303
647
|
isConnected: false,
|
|
304
648
|
pendingMessages: [],
|
|
305
649
|
scenarioId: options.scenarioId ?? null,
|
|
306
650
|
pingInterval: null,
|
|
307
651
|
reconnectAttempts: 0,
|
|
652
|
+
reconnectResetTimeout: null,
|
|
308
653
|
reconnectTimeout: null,
|
|
309
654
|
sessionId: options.sessionId ?? createSessionId(),
|
|
310
655
|
ws: null
|
|
311
656
|
};
|
|
657
|
+
const emitConnection = (reconnect) => {
|
|
658
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
659
|
+
};
|
|
660
|
+
const emitTerminalFailure = (message) => {
|
|
661
|
+
listeners.forEach((listener) => listener({
|
|
662
|
+
message,
|
|
663
|
+
recoverable: false,
|
|
664
|
+
type: "error"
|
|
665
|
+
}));
|
|
666
|
+
emitConnection({
|
|
667
|
+
reconnect: {
|
|
668
|
+
attempts: state.reconnectAttempts,
|
|
669
|
+
maxAttempts: maxReconnectAttempts,
|
|
670
|
+
status: "exhausted"
|
|
671
|
+
},
|
|
672
|
+
type: "connection"
|
|
673
|
+
});
|
|
674
|
+
};
|
|
312
675
|
const clearTimers = () => {
|
|
313
676
|
if (state.pingInterval) {
|
|
314
677
|
clearInterval(state.pingInterval);
|
|
@@ -318,6 +681,10 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
318
681
|
clearTimeout(state.reconnectTimeout);
|
|
319
682
|
state.reconnectTimeout = null;
|
|
320
683
|
}
|
|
684
|
+
if (state.reconnectResetTimeout) {
|
|
685
|
+
clearTimeout(state.reconnectResetTimeout);
|
|
686
|
+
state.reconnectResetTimeout = null;
|
|
687
|
+
}
|
|
321
688
|
};
|
|
322
689
|
const flushPendingMessages = () => {
|
|
323
690
|
if (state.ws?.readyState !== WS_OPEN) {
|
|
@@ -330,22 +697,111 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
330
697
|
}
|
|
331
698
|
}
|
|
332
699
|
};
|
|
700
|
+
const rejectPendingCallControls = (message) => {
|
|
701
|
+
for (const pending of pendingCallControls.values()) {
|
|
702
|
+
clearTimeout(pending.timeout);
|
|
703
|
+
pending.reject(new Error(message));
|
|
704
|
+
}
|
|
705
|
+
pendingCallControls.clear();
|
|
706
|
+
};
|
|
333
707
|
const scheduleReconnect = () => {
|
|
708
|
+
if (disposed)
|
|
709
|
+
return;
|
|
710
|
+
const scheduledGeneration = lifecycleGeneration;
|
|
334
711
|
state.reconnectAttempts += 1;
|
|
335
|
-
|
|
712
|
+
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
713
|
+
const nextAttemptAt = Date.now() + delayMs;
|
|
714
|
+
emitConnection({
|
|
715
|
+
reconnect: {
|
|
716
|
+
attempts: state.reconnectAttempts,
|
|
717
|
+
lastDisconnectAt: Date.now(),
|
|
718
|
+
maxAttempts: maxReconnectAttempts,
|
|
719
|
+
nextAttemptAt,
|
|
720
|
+
status: "reconnecting"
|
|
721
|
+
},
|
|
722
|
+
type: "connection"
|
|
723
|
+
});
|
|
724
|
+
state.reconnectTimeout = setTimeout(async () => {
|
|
725
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
726
|
+
return;
|
|
336
727
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
728
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
if (options.prepareReconnect) {
|
|
733
|
+
const preparationAbort = new AbortController;
|
|
734
|
+
reconnectPreparationAbort = preparationAbort;
|
|
735
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
736
|
+
try {
|
|
737
|
+
await Promise.race([
|
|
738
|
+
options.prepareReconnect({
|
|
739
|
+
attempt: state.reconnectAttempts,
|
|
740
|
+
path,
|
|
741
|
+
scenarioId: state.scenarioId,
|
|
742
|
+
sessionId: state.sessionId,
|
|
743
|
+
signal: preparationAbort.signal
|
|
744
|
+
}),
|
|
745
|
+
new Promise((_, reject) => {
|
|
746
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
747
|
+
})
|
|
748
|
+
]);
|
|
749
|
+
} finally {
|
|
750
|
+
clearTimeout(timeout);
|
|
751
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
752
|
+
reconnectPreparationAbort = null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
758
|
+
return;
|
|
759
|
+
if (error instanceof VoiceReconnectRejectedError) {
|
|
760
|
+
emitTerminalFailure(error.message);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
764
|
+
scheduleReconnect();
|
|
765
|
+
} else {
|
|
766
|
+
emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
|
|
767
|
+
}
|
|
337
768
|
return;
|
|
338
769
|
}
|
|
770
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
771
|
+
return;
|
|
339
772
|
connect();
|
|
340
|
-
},
|
|
773
|
+
}, delayMs);
|
|
341
774
|
};
|
|
342
775
|
const connect = () => {
|
|
343
|
-
|
|
776
|
+
if (disposed)
|
|
777
|
+
return;
|
|
778
|
+
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
344
779
|
ws.binaryType = "arraybuffer";
|
|
345
780
|
ws.onopen = () => {
|
|
781
|
+
if (state.ws !== ws) {
|
|
782
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
346
786
|
state.isConnected = true;
|
|
347
|
-
state.reconnectAttempts = 0;
|
|
348
787
|
flushPendingMessages();
|
|
788
|
+
if (wasReconnecting) {
|
|
789
|
+
emitConnection({
|
|
790
|
+
reconnect: {
|
|
791
|
+
attempts: state.reconnectAttempts,
|
|
792
|
+
lastResumedAt: Date.now(),
|
|
793
|
+
maxAttempts: maxReconnectAttempts,
|
|
794
|
+
status: "resumed"
|
|
795
|
+
},
|
|
796
|
+
type: "connection"
|
|
797
|
+
});
|
|
798
|
+
state.reconnectResetTimeout = setTimeout(() => {
|
|
799
|
+
if (state.ws === ws && ws.readyState === WS_OPEN) {
|
|
800
|
+
state.reconnectAttempts = 0;
|
|
801
|
+
}
|
|
802
|
+
state.reconnectResetTimeout = null;
|
|
803
|
+
}, reconnectResetAfterMs);
|
|
804
|
+
}
|
|
349
805
|
listeners.forEach((listener) => listener({
|
|
350
806
|
scenarioId: state.scenarioId ?? undefined,
|
|
351
807
|
sessionId: state.sessionId,
|
|
@@ -359,6 +815,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
359
815
|
}, pingInterval);
|
|
360
816
|
};
|
|
361
817
|
ws.onmessage = (event) => {
|
|
818
|
+
if (state.ws !== ws)
|
|
819
|
+
return;
|
|
362
820
|
const parsed = parseServerMessage(event);
|
|
363
821
|
if (!parsed) {
|
|
364
822
|
return;
|
|
@@ -367,29 +825,86 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
367
825
|
state.sessionId = parsed.sessionId;
|
|
368
826
|
state.scenarioId = parsed.scenarioId ?? state.scenarioId;
|
|
369
827
|
}
|
|
828
|
+
if (parsed.type === "call_control_ack") {
|
|
829
|
+
const pending = pendingCallControls.get(parsed.requestId);
|
|
830
|
+
if (pending) {
|
|
831
|
+
clearTimeout(pending.timeout);
|
|
832
|
+
pendingCallControls.delete(parsed.requestId);
|
|
833
|
+
if (parsed.ok) {
|
|
834
|
+
pending.resolve();
|
|
835
|
+
} else {
|
|
836
|
+
pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
370
840
|
listeners.forEach((listener) => listener(parsed));
|
|
371
841
|
};
|
|
372
842
|
ws.onclose = (event) => {
|
|
843
|
+
if (state.ws !== ws)
|
|
844
|
+
return;
|
|
845
|
+
state.ws = null;
|
|
373
846
|
state.isConnected = false;
|
|
374
847
|
clearTimers();
|
|
375
|
-
|
|
848
|
+
rejectPendingCallControls("Voice connection interrupted");
|
|
849
|
+
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && event.code !== WS_INITIALIZATION_FAILED && state.reconnectAttempts < maxReconnectAttempts;
|
|
850
|
+
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
851
|
+
const initializationFailure = event.code === WS_INITIALIZATION_FAILED;
|
|
852
|
+
if (authorizationFailure) {
|
|
853
|
+
listeners.forEach((listener) => listener({
|
|
854
|
+
message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
|
|
855
|
+
recoverable: false,
|
|
856
|
+
type: "error"
|
|
857
|
+
}));
|
|
858
|
+
}
|
|
859
|
+
if (initializationFailure) {
|
|
860
|
+
listeners.forEach((listener) => listener({
|
|
861
|
+
message: "Voice provider initialization failed.",
|
|
862
|
+
recoverable: false,
|
|
863
|
+
type: "error"
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
376
866
|
if (reconnectable) {
|
|
377
867
|
scheduleReconnect();
|
|
868
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
869
|
+
if (authorizationFailure) {
|
|
870
|
+
emitConnection({
|
|
871
|
+
reconnect: {
|
|
872
|
+
attempts: state.reconnectAttempts,
|
|
873
|
+
lastDisconnectAt: Date.now(),
|
|
874
|
+
maxAttempts: maxReconnectAttempts,
|
|
875
|
+
status: "exhausted"
|
|
876
|
+
},
|
|
877
|
+
type: "connection"
|
|
878
|
+
});
|
|
879
|
+
} else if (!initializationFailure) {
|
|
880
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
881
|
+
}
|
|
378
882
|
}
|
|
379
883
|
};
|
|
380
884
|
state.ws = ws;
|
|
381
885
|
};
|
|
382
886
|
const sendSerialized = (value) => {
|
|
887
|
+
if (disposed)
|
|
888
|
+
return;
|
|
383
889
|
if (state.ws?.readyState === WS_OPEN) {
|
|
384
890
|
state.ws.send(value);
|
|
385
891
|
return;
|
|
386
892
|
}
|
|
893
|
+
if (typeof value !== "string") {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
|
|
897
|
+
state.pendingMessages.shift();
|
|
898
|
+
}
|
|
387
899
|
state.pendingMessages.push(value);
|
|
388
900
|
};
|
|
389
901
|
const send = (message) => {
|
|
390
902
|
sendSerialized(JSON.stringify(message));
|
|
391
903
|
};
|
|
392
904
|
const start = (input = {}) => {
|
|
905
|
+
if (input.sessionId && input.sessionId !== state.sessionId) {
|
|
906
|
+
throw new Error("Voice session switching requires a new connection instance.");
|
|
907
|
+
}
|
|
393
908
|
if (input.sessionId) {
|
|
394
909
|
state.sessionId = input.sessionId;
|
|
395
910
|
}
|
|
@@ -397,9 +912,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
397
912
|
state.scenarioId = input.scenarioId;
|
|
398
913
|
}
|
|
399
914
|
send({
|
|
400
|
-
|
|
915
|
+
scenarioId: state.scenarioId ?? undefined,
|
|
401
916
|
sessionId: state.sessionId,
|
|
402
|
-
|
|
917
|
+
type: "start"
|
|
403
918
|
});
|
|
404
919
|
};
|
|
405
920
|
const sendAudio = (audio) => {
|
|
@@ -409,20 +924,63 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
409
924
|
send({ type: "end_turn" });
|
|
410
925
|
};
|
|
411
926
|
const callControl = (message) => {
|
|
412
|
-
|
|
927
|
+
if (state.ws?.readyState !== WS_OPEN) {
|
|
928
|
+
return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
|
|
929
|
+
}
|
|
930
|
+
const requestId = crypto.randomUUID();
|
|
931
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
932
|
+
const timeout = setTimeout(() => {
|
|
933
|
+
pendingCallControls.delete(requestId);
|
|
934
|
+
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
935
|
+
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
936
|
+
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
937
|
+
state.ws.send(JSON.stringify({
|
|
413
938
|
...message,
|
|
939
|
+
requestId,
|
|
414
940
|
type: "call_control"
|
|
415
|
-
});
|
|
941
|
+
}));
|
|
942
|
+
return promise;
|
|
943
|
+
};
|
|
944
|
+
const close = (reason = "client-close") => {
|
|
945
|
+
disposed = true;
|
|
946
|
+
reconnectPreparationAbort?.abort();
|
|
947
|
+
reconnectPreparationAbort = null;
|
|
948
|
+
lifecycleGeneration += 1;
|
|
949
|
+
clearTimers();
|
|
950
|
+
rejectPendingCallControls("Voice connection closed");
|
|
951
|
+
state.pendingMessages.length = 0;
|
|
952
|
+
if (state.ws) {
|
|
953
|
+
const ws = state.ws;
|
|
954
|
+
state.ws = null;
|
|
955
|
+
if (ws.readyState === WS_OPEN) {
|
|
956
|
+
ws.send(JSON.stringify({ reason, type: "close" }));
|
|
957
|
+
}
|
|
958
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
959
|
+
}
|
|
960
|
+
state.isConnected = false;
|
|
961
|
+
listeners.clear();
|
|
416
962
|
};
|
|
417
|
-
const
|
|
963
|
+
const disconnect = () => {
|
|
964
|
+
disposed = true;
|
|
965
|
+
reconnectPreparationAbort?.abort();
|
|
966
|
+
reconnectPreparationAbort = null;
|
|
967
|
+
lifecycleGeneration += 1;
|
|
418
968
|
clearTimers();
|
|
969
|
+
rejectPendingCallControls("Voice connection disconnected");
|
|
970
|
+
state.pendingMessages.length = 0;
|
|
419
971
|
if (state.ws) {
|
|
420
|
-
state.ws
|
|
972
|
+
const ws = state.ws;
|
|
421
973
|
state.ws = null;
|
|
974
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
422
975
|
}
|
|
423
976
|
state.isConnected = false;
|
|
424
977
|
listeners.clear();
|
|
425
978
|
};
|
|
979
|
+
const simulateDisconnect = () => {
|
|
980
|
+
if (state.ws?.readyState === WS_OPEN) {
|
|
981
|
+
state.ws.close(4000, "absolutejs-voice-reconnect-proof");
|
|
982
|
+
}
|
|
983
|
+
};
|
|
426
984
|
const subscribe = (callback) => {
|
|
427
985
|
listeners.add(callback);
|
|
428
986
|
return () => {
|
|
@@ -433,32 +991,64 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
433
991
|
return {
|
|
434
992
|
callControl,
|
|
435
993
|
close,
|
|
994
|
+
disconnect,
|
|
436
995
|
endTurn,
|
|
437
|
-
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
438
|
-
getScenarioId: () => state.scenarioId ?? "",
|
|
439
|
-
getSessionId: () => state.sessionId,
|
|
440
996
|
send,
|
|
441
997
|
sendAudio,
|
|
998
|
+
simulateDisconnect,
|
|
442
999
|
start,
|
|
443
|
-
subscribe
|
|
1000
|
+
subscribe,
|
|
1001
|
+
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
1002
|
+
getScenarioId: () => state.scenarioId ?? "",
|
|
1003
|
+
getSessionId: () => state.sessionId
|
|
444
1004
|
};
|
|
445
1005
|
};
|
|
446
1006
|
|
|
447
1007
|
// src/client/store.ts
|
|
1008
|
+
var createInitialReconnectState = () => ({
|
|
1009
|
+
attempts: 0,
|
|
1010
|
+
maxAttempts: 0,
|
|
1011
|
+
status: "idle"
|
|
1012
|
+
});
|
|
1013
|
+
var appendSegmentText = (accumulated, next) => {
|
|
1014
|
+
const nextText = next.trim().replace(/\s+/g, " ");
|
|
1015
|
+
if (!nextText)
|
|
1016
|
+
return accumulated;
|
|
1017
|
+
if (!accumulated)
|
|
1018
|
+
return nextText;
|
|
1019
|
+
if (accumulated === nextText || accumulated.endsWith(nextText)) {
|
|
1020
|
+
return accumulated;
|
|
1021
|
+
}
|
|
1022
|
+
if (nextText.includes(accumulated))
|
|
1023
|
+
return nextText;
|
|
1024
|
+
return `${accumulated} ${nextText}`;
|
|
1025
|
+
};
|
|
1026
|
+
var joinPartial = (finalized, interim) => {
|
|
1027
|
+
const interimText = interim.trim().replace(/\s+/g, " ");
|
|
1028
|
+
if (!finalized)
|
|
1029
|
+
return interimText;
|
|
1030
|
+
if (!interimText || finalized.endsWith(interimText))
|
|
1031
|
+
return finalized;
|
|
1032
|
+
return `${finalized} ${interimText}`;
|
|
1033
|
+
};
|
|
448
1034
|
var createInitialState = () => ({
|
|
449
1035
|
assistantAudio: [],
|
|
1036
|
+
assistantStreamingText: "",
|
|
450
1037
|
assistantTexts: [],
|
|
451
1038
|
call: null,
|
|
452
1039
|
error: null,
|
|
453
1040
|
isConnected: false,
|
|
454
|
-
scenarioId: null,
|
|
455
1041
|
partial: "",
|
|
1042
|
+
reconnect: createInitialReconnectState(),
|
|
1043
|
+
scenarioId: null,
|
|
456
1044
|
sessionId: null,
|
|
1045
|
+
sessionMetadata: null,
|
|
457
1046
|
status: "idle",
|
|
458
1047
|
turns: []
|
|
459
1048
|
});
|
|
460
1049
|
var createVoiceStreamStore = () => {
|
|
461
1050
|
let state = createInitialState();
|
|
1051
|
+
let turnFinalText = "";
|
|
462
1052
|
const subscribers = new Set;
|
|
463
1053
|
const notify = () => {
|
|
464
1054
|
subscribers.forEach((subscriber) => subscriber());
|
|
@@ -482,9 +1072,16 @@ var createVoiceStreamStore = () => {
|
|
|
482
1072
|
case "assistant":
|
|
483
1073
|
state = {
|
|
484
1074
|
...state,
|
|
1075
|
+
assistantStreamingText: "",
|
|
485
1076
|
assistantTexts: [...state.assistantTexts, action.text]
|
|
486
1077
|
};
|
|
487
1078
|
break;
|
|
1079
|
+
case "assistant_delta":
|
|
1080
|
+
state = {
|
|
1081
|
+
...state,
|
|
1082
|
+
assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
|
|
1083
|
+
};
|
|
1084
|
+
break;
|
|
488
1085
|
case "complete":
|
|
489
1086
|
state = {
|
|
490
1087
|
...state,
|
|
@@ -509,7 +1106,19 @@ var createVoiceStreamStore = () => {
|
|
|
509
1106
|
case "connected":
|
|
510
1107
|
state = {
|
|
511
1108
|
...state,
|
|
512
|
-
isConnected: true
|
|
1109
|
+
isConnected: true,
|
|
1110
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
1111
|
+
...state.reconnect,
|
|
1112
|
+
lastResumedAt: Date.now(),
|
|
1113
|
+
nextAttemptAt: undefined,
|
|
1114
|
+
status: "resumed"
|
|
1115
|
+
} : state.reconnect
|
|
1116
|
+
};
|
|
1117
|
+
break;
|
|
1118
|
+
case "connection":
|
|
1119
|
+
state = {
|
|
1120
|
+
...state,
|
|
1121
|
+
reconnect: action.reconnect
|
|
513
1122
|
};
|
|
514
1123
|
break;
|
|
515
1124
|
case "disconnected":
|
|
@@ -521,20 +1130,44 @@ var createVoiceStreamStore = () => {
|
|
|
521
1130
|
case "error":
|
|
522
1131
|
state = {
|
|
523
1132
|
...state,
|
|
524
|
-
error: action.message
|
|
1133
|
+
error: action.message,
|
|
1134
|
+
...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
|
|
525
1135
|
};
|
|
526
1136
|
break;
|
|
527
1137
|
case "final":
|
|
1138
|
+
turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
|
|
528
1139
|
state = {
|
|
529
1140
|
...state,
|
|
530
|
-
partial:
|
|
531
|
-
turns: state.turns.map((turn) => turn)
|
|
1141
|
+
partial: turnFinalText
|
|
532
1142
|
};
|
|
533
1143
|
break;
|
|
534
1144
|
case "partial":
|
|
535
1145
|
state = {
|
|
536
1146
|
...state,
|
|
537
|
-
partial: action.transcript.text
|
|
1147
|
+
partial: joinPartial(turnFinalText, action.transcript.text)
|
|
1148
|
+
};
|
|
1149
|
+
break;
|
|
1150
|
+
case "replay":
|
|
1151
|
+
turnFinalText = action.partial;
|
|
1152
|
+
state = {
|
|
1153
|
+
...state,
|
|
1154
|
+
assistantStreamingText: "",
|
|
1155
|
+
assistantTexts: [...action.assistantTexts],
|
|
1156
|
+
call: action.call ?? null,
|
|
1157
|
+
error: null,
|
|
1158
|
+
isConnected: action.status === "active",
|
|
1159
|
+
partial: action.partial,
|
|
1160
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
1161
|
+
...state.reconnect,
|
|
1162
|
+
lastResumedAt: Date.now(),
|
|
1163
|
+
nextAttemptAt: undefined,
|
|
1164
|
+
status: "resumed"
|
|
1165
|
+
} : state.reconnect,
|
|
1166
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
1167
|
+
sessionId: action.sessionId,
|
|
1168
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
1169
|
+
status: action.status,
|
|
1170
|
+
turns: [...action.turns]
|
|
538
1171
|
};
|
|
539
1172
|
break;
|
|
540
1173
|
case "session":
|
|
@@ -544,10 +1177,12 @@ var createVoiceStreamStore = () => {
|
|
|
544
1177
|
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
545
1178
|
isConnected: action.status === "active",
|
|
546
1179
|
sessionId: action.sessionId,
|
|
1180
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
547
1181
|
status: action.status
|
|
548
1182
|
};
|
|
549
1183
|
break;
|
|
550
1184
|
case "turn":
|
|
1185
|
+
turnFinalText = "";
|
|
551
1186
|
state = {
|
|
552
1187
|
...state,
|
|
553
1188
|
partial: "",
|
|
@@ -574,30 +1209,86 @@ var createVoiceStreamStore = () => {
|
|
|
574
1209
|
var createVoiceStream = (path, options = {}) => {
|
|
575
1210
|
const connection = createVoiceConnection(path, options);
|
|
576
1211
|
const store = createVoiceStreamStore();
|
|
1212
|
+
const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
|
|
1213
|
+
...options.browserMedia,
|
|
1214
|
+
getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
|
|
1215
|
+
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1216
|
+
}) : null;
|
|
577
1217
|
const subscribers = new Set;
|
|
1218
|
+
let disposed = false;
|
|
578
1219
|
const start = (input) => Promise.resolve().then(() => {
|
|
1220
|
+
if (disposed)
|
|
1221
|
+
return;
|
|
579
1222
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
580
1223
|
return;
|
|
581
1224
|
}
|
|
582
1225
|
connection.start(input);
|
|
1226
|
+
browserMediaReporter?.start();
|
|
583
1227
|
});
|
|
584
1228
|
const notify = () => {
|
|
585
1229
|
subscribers.forEach((subscriber) => subscriber());
|
|
586
1230
|
};
|
|
1231
|
+
const reportReconnect = () => {
|
|
1232
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const snapshot = store.getSnapshot();
|
|
1236
|
+
const body = JSON.stringify({
|
|
1237
|
+
at: Date.now(),
|
|
1238
|
+
reconnect: snapshot.reconnect,
|
|
1239
|
+
scenarioId: snapshot.scenarioId,
|
|
1240
|
+
sessionId: connection.getSessionId(),
|
|
1241
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
1242
|
+
});
|
|
1243
|
+
fetch(options.reconnectReportPath, {
|
|
1244
|
+
body,
|
|
1245
|
+
headers: {
|
|
1246
|
+
"Content-Type": "application/json"
|
|
1247
|
+
},
|
|
1248
|
+
keepalive: true,
|
|
1249
|
+
method: "POST"
|
|
1250
|
+
}).catch(() => {});
|
|
1251
|
+
};
|
|
587
1252
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
588
1253
|
const action = serverMessageToAction(message);
|
|
589
1254
|
if (action) {
|
|
590
1255
|
store.dispatch(action);
|
|
1256
|
+
if (message.type === "connection") {
|
|
1257
|
+
reportReconnect();
|
|
1258
|
+
}
|
|
591
1259
|
notify();
|
|
592
1260
|
}
|
|
593
1261
|
});
|
|
594
1262
|
return {
|
|
1263
|
+
start,
|
|
1264
|
+
get assistantAudio() {
|
|
1265
|
+
return store.getSnapshot().assistantAudio;
|
|
1266
|
+
},
|
|
1267
|
+
get assistantTexts() {
|
|
1268
|
+
return store.getSnapshot().assistantTexts;
|
|
1269
|
+
},
|
|
1270
|
+
get assistantStreamingText() {
|
|
1271
|
+
return store.getSnapshot().assistantStreamingText;
|
|
1272
|
+
},
|
|
1273
|
+
get call() {
|
|
1274
|
+
return store.getSnapshot().call;
|
|
1275
|
+
},
|
|
595
1276
|
callControl(message) {
|
|
596
|
-
connection.callControl(message);
|
|
1277
|
+
return connection.callControl(message);
|
|
1278
|
+
},
|
|
1279
|
+
close(reason) {
|
|
1280
|
+
disposed = true;
|
|
1281
|
+
unsubscribeConnection();
|
|
1282
|
+
browserMediaReporter?.close();
|
|
1283
|
+
connection.close(reason);
|
|
1284
|
+
store.dispatch({ type: "disconnected" });
|
|
1285
|
+
notify();
|
|
597
1286
|
},
|
|
598
|
-
|
|
1287
|
+
disconnect() {
|
|
1288
|
+
disposed = true;
|
|
599
1289
|
unsubscribeConnection();
|
|
600
|
-
|
|
1290
|
+
browserMediaReporter?.close();
|
|
1291
|
+
connection.disconnect();
|
|
601
1292
|
store.dispatch({ type: "disconnected" });
|
|
602
1293
|
notify();
|
|
603
1294
|
},
|
|
@@ -616,44 +1307,43 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
616
1307
|
get isConnected() {
|
|
617
1308
|
return store.getSnapshot().isConnected;
|
|
618
1309
|
},
|
|
619
|
-
get scenarioId() {
|
|
620
|
-
return store.getSnapshot().scenarioId;
|
|
621
|
-
},
|
|
622
|
-
start,
|
|
623
1310
|
get partial() {
|
|
624
1311
|
return store.getSnapshot().partial;
|
|
625
1312
|
},
|
|
626
|
-
get
|
|
627
|
-
return
|
|
1313
|
+
get reconnect() {
|
|
1314
|
+
return store.getSnapshot().reconnect;
|
|
628
1315
|
},
|
|
629
|
-
get
|
|
630
|
-
return store.getSnapshot().
|
|
1316
|
+
get scenarioId() {
|
|
1317
|
+
return store.getSnapshot().scenarioId;
|
|
631
1318
|
},
|
|
632
|
-
|
|
633
|
-
|
|
1319
|
+
sendAudio(audio) {
|
|
1320
|
+
connection.sendAudio(audio);
|
|
634
1321
|
},
|
|
635
|
-
get
|
|
636
|
-
return
|
|
1322
|
+
get sessionId() {
|
|
1323
|
+
return connection.getSessionId();
|
|
637
1324
|
},
|
|
638
|
-
get
|
|
639
|
-
return store.getSnapshot().
|
|
1325
|
+
get sessionMetadata() {
|
|
1326
|
+
return store.getSnapshot().sessionMetadata;
|
|
640
1327
|
},
|
|
641
|
-
|
|
642
|
-
|
|
1328
|
+
simulateDisconnect() {
|
|
1329
|
+
connection.simulateDisconnect();
|
|
643
1330
|
},
|
|
644
|
-
|
|
645
|
-
|
|
1331
|
+
get status() {
|
|
1332
|
+
return store.getSnapshot().status;
|
|
646
1333
|
},
|
|
647
1334
|
subscribe(subscriber) {
|
|
648
1335
|
subscribers.add(subscriber);
|
|
649
1336
|
return () => {
|
|
650
1337
|
subscribers.delete(subscriber);
|
|
651
1338
|
};
|
|
1339
|
+
},
|
|
1340
|
+
get turns() {
|
|
1341
|
+
return store.getSnapshot().turns;
|
|
652
1342
|
}
|
|
653
1343
|
};
|
|
654
1344
|
};
|
|
655
1345
|
|
|
656
|
-
// src/audioConditioning.ts
|
|
1346
|
+
// src/core/audioConditioning.ts
|
|
657
1347
|
var DEFAULT_TARGET_LEVEL = 0.08;
|
|
658
1348
|
var DEFAULT_MAX_GAIN = 3;
|
|
659
1349
|
var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
|
|
@@ -671,34 +1361,37 @@ var resolveAudioConditioningConfig = (config) => {
|
|
|
671
1361
|
};
|
|
672
1362
|
};
|
|
673
1363
|
|
|
674
|
-
// src/turnProfiles.ts
|
|
1364
|
+
// src/core/turnProfiles.ts
|
|
675
1365
|
var TURN_PROFILE_DEFAULTS = {
|
|
676
1366
|
balanced: {
|
|
677
1367
|
qualityProfile: "general",
|
|
1368
|
+
minSilenceMs: 400,
|
|
678
1369
|
silenceMs: 1400,
|
|
679
1370
|
speechThreshold: 0.012,
|
|
680
1371
|
transcriptStabilityMs: 1000
|
|
681
1372
|
},
|
|
682
1373
|
fast: {
|
|
683
1374
|
qualityProfile: "general",
|
|
1375
|
+
minSilenceMs: 300,
|
|
684
1376
|
silenceMs: 700,
|
|
685
1377
|
speechThreshold: 0.015,
|
|
686
1378
|
transcriptStabilityMs: 450
|
|
687
1379
|
},
|
|
688
1380
|
"long-form": {
|
|
689
1381
|
qualityProfile: "general",
|
|
1382
|
+
minSilenceMs: 600,
|
|
690
1383
|
silenceMs: 2200,
|
|
691
1384
|
speechThreshold: 0.01,
|
|
692
1385
|
transcriptStabilityMs: 1500
|
|
693
1386
|
}
|
|
694
1387
|
};
|
|
695
1388
|
var QUALITY_PROFILE_DEFAULTS = {
|
|
696
|
-
general: {},
|
|
697
1389
|
"accent-heavy": {
|
|
698
1390
|
silenceMs: 1200,
|
|
699
1391
|
speechThreshold: 0.01,
|
|
700
1392
|
transcriptStabilityMs: 1200
|
|
701
1393
|
},
|
|
1394
|
+
general: {},
|
|
702
1395
|
"noisy-room": {
|
|
703
1396
|
silenceMs: 2000,
|
|
704
1397
|
speechThreshold: 0.02,
|
|
@@ -717,16 +1410,18 @@ var resolveTurnDetectionConfig = (config) => {
|
|
|
717
1410
|
const qualityProfile = config?.qualityProfile ?? DEFAULT_QUALITY_PROFILE;
|
|
718
1411
|
const preset = TURN_PROFILE_DEFAULTS[profile];
|
|
719
1412
|
const quality = QUALITY_PROFILE_DEFAULTS[qualityProfile];
|
|
1413
|
+
const silenceMs = config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs;
|
|
720
1414
|
return {
|
|
721
1415
|
profile,
|
|
722
1416
|
qualityProfile,
|
|
723
|
-
|
|
1417
|
+
minSilenceMs: Math.min(silenceMs, config?.minSilenceMs ?? quality.minSilenceMs ?? preset.minSilenceMs),
|
|
1418
|
+
silenceMs,
|
|
724
1419
|
speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
|
|
725
1420
|
transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
|
|
726
1421
|
};
|
|
727
1422
|
};
|
|
728
1423
|
|
|
729
|
-
// src/presets.ts
|
|
1424
|
+
// src/core/presets.ts
|
|
730
1425
|
var PRESET_INPUTS = {
|
|
731
1426
|
chat: {
|
|
732
1427
|
audioConditioning: {
|
|
@@ -747,8 +1442,8 @@ var PRESET_INPUTS = {
|
|
|
747
1442
|
},
|
|
748
1443
|
sttLifecycle: "continuous",
|
|
749
1444
|
turnDetection: {
|
|
750
|
-
|
|
751
|
-
|
|
1445
|
+
profile: "balanced",
|
|
1446
|
+
qualityProfile: "short-command"
|
|
752
1447
|
}
|
|
753
1448
|
},
|
|
754
1449
|
default: {
|
|
@@ -763,8 +1458,8 @@ var PRESET_INPUTS = {
|
|
|
763
1458
|
},
|
|
764
1459
|
sttLifecycle: "continuous",
|
|
765
1460
|
turnDetection: {
|
|
766
|
-
|
|
767
|
-
|
|
1461
|
+
profile: "fast",
|
|
1462
|
+
qualityProfile: "general"
|
|
768
1463
|
}
|
|
769
1464
|
},
|
|
770
1465
|
dictation: {
|
|
@@ -786,8 +1481,8 @@ var PRESET_INPUTS = {
|
|
|
786
1481
|
},
|
|
787
1482
|
sttLifecycle: "continuous",
|
|
788
1483
|
turnDetection: {
|
|
789
|
-
|
|
790
|
-
|
|
1484
|
+
profile: "long-form",
|
|
1485
|
+
qualityProfile: "accent-heavy"
|
|
791
1486
|
}
|
|
792
1487
|
},
|
|
793
1488
|
"guided-intake": {
|
|
@@ -809,8 +1504,8 @@ var PRESET_INPUTS = {
|
|
|
809
1504
|
},
|
|
810
1505
|
sttLifecycle: "turn-scoped",
|
|
811
1506
|
turnDetection: {
|
|
812
|
-
|
|
813
|
-
|
|
1507
|
+
profile: "long-form",
|
|
1508
|
+
qualityProfile: "accent-heavy"
|
|
814
1509
|
}
|
|
815
1510
|
},
|
|
816
1511
|
"noisy-room": {
|
|
@@ -832,8 +1527,8 @@ var PRESET_INPUTS = {
|
|
|
832
1527
|
},
|
|
833
1528
|
sttLifecycle: "continuous",
|
|
834
1529
|
turnDetection: {
|
|
835
|
-
qualityProfile: "noisy-room",
|
|
836
1530
|
profile: "long-form",
|
|
1531
|
+
qualityProfile: "noisy-room",
|
|
837
1532
|
silenceMs: 2100,
|
|
838
1533
|
speechThreshold: 0.02,
|
|
839
1534
|
transcriptStabilityMs: 1650
|
|
@@ -858,8 +1553,8 @@ var PRESET_INPUTS = {
|
|
|
858
1553
|
},
|
|
859
1554
|
sttLifecycle: "continuous",
|
|
860
1555
|
turnDetection: {
|
|
861
|
-
qualityProfile: "noisy-room",
|
|
862
1556
|
profile: "long-form",
|
|
1557
|
+
qualityProfile: "noisy-room",
|
|
863
1558
|
silenceMs: 660,
|
|
864
1559
|
speechThreshold: 0.012,
|
|
865
1560
|
transcriptStabilityMs: 300
|
|
@@ -884,8 +1579,8 @@ var PRESET_INPUTS = {
|
|
|
884
1579
|
},
|
|
885
1580
|
sttLifecycle: "continuous",
|
|
886
1581
|
turnDetection: {
|
|
887
|
-
qualityProfile: "noisy-room",
|
|
888
1582
|
profile: "long-form",
|
|
1583
|
+
qualityProfile: "noisy-room",
|
|
889
1584
|
silenceMs: 620,
|
|
890
1585
|
speechThreshold: 0.012,
|
|
891
1586
|
transcriptStabilityMs: 280
|
|
@@ -910,8 +1605,8 @@ var PRESET_INPUTS = {
|
|
|
910
1605
|
},
|
|
911
1606
|
sttLifecycle: "continuous",
|
|
912
1607
|
turnDetection: {
|
|
913
|
-
|
|
914
|
-
|
|
1608
|
+
profile: "long-form",
|
|
1609
|
+
qualityProfile: "noisy-room"
|
|
915
1610
|
}
|
|
916
1611
|
}
|
|
917
1612
|
};
|
|
@@ -935,14 +1630,17 @@ var resolveVoiceRuntimePreset = (name = "default") => {
|
|
|
935
1630
|
// src/client/controller.ts
|
|
936
1631
|
var createInitialState2 = (stream) => ({
|
|
937
1632
|
assistantAudio: [...stream.assistantAudio],
|
|
1633
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
938
1634
|
assistantTexts: [...stream.assistantTexts],
|
|
939
1635
|
call: stream.call,
|
|
940
1636
|
error: stream.error,
|
|
941
1637
|
isConnected: stream.isConnected,
|
|
942
1638
|
isRecording: false,
|
|
943
1639
|
partial: stream.partial,
|
|
1640
|
+
reconnect: stream.reconnect,
|
|
944
1641
|
recordingError: null,
|
|
945
1642
|
sessionId: stream.sessionId,
|
|
1643
|
+
sessionMetadata: stream.sessionMetadata,
|
|
946
1644
|
scenarioId: stream.scenarioId,
|
|
947
1645
|
status: stream.status,
|
|
948
1646
|
turns: [...stream.turns]
|
|
@@ -954,6 +1652,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
954
1652
|
...options.connection
|
|
955
1653
|
});
|
|
956
1654
|
let capture = null;
|
|
1655
|
+
let captureGeneration = 0;
|
|
1656
|
+
let disposed = false;
|
|
1657
|
+
let startRecordingPromise = null;
|
|
957
1658
|
let state = createInitialState2(stream);
|
|
958
1659
|
const subscribers = new Set;
|
|
959
1660
|
const notify = () => {
|
|
@@ -965,12 +1666,15 @@ var createVoiceController = (path, options = {}) => {
|
|
|
965
1666
|
state = {
|
|
966
1667
|
...state,
|
|
967
1668
|
assistantAudio: [...stream.assistantAudio],
|
|
1669
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
968
1670
|
assistantTexts: [...stream.assistantTexts],
|
|
969
1671
|
call: stream.call,
|
|
970
1672
|
error: stream.error,
|
|
971
1673
|
isConnected: stream.isConnected,
|
|
972
1674
|
partial: stream.partial,
|
|
1675
|
+
reconnect: stream.reconnect,
|
|
973
1676
|
sessionId: stream.sessionId,
|
|
1677
|
+
sessionMetadata: stream.sessionMetadata,
|
|
974
1678
|
scenarioId: stream.scenarioId,
|
|
975
1679
|
status: stream.status,
|
|
976
1680
|
turns: [...stream.turns]
|
|
@@ -994,12 +1698,20 @@ var createVoiceController = (path, options = {}) => {
|
|
|
994
1698
|
capture = createMicrophoneCapture({
|
|
995
1699
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
996
1700
|
onLevel: options.capture?.onLevel,
|
|
997
|
-
onAudio: (audio) =>
|
|
998
|
-
|
|
1701
|
+
onAudio: (audio) => {
|
|
1702
|
+
if (options.capture?.onAudio) {
|
|
1703
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
stream.sendAudio(audio);
|
|
1707
|
+
},
|
|
1708
|
+
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
|
|
1709
|
+
...options.capture?.stream ? { stream: options.capture.stream } : {}
|
|
999
1710
|
});
|
|
1000
1711
|
return capture;
|
|
1001
1712
|
};
|
|
1002
1713
|
const stopRecording = () => {
|
|
1714
|
+
captureGeneration += 1;
|
|
1003
1715
|
capture?.stop();
|
|
1004
1716
|
capture = null;
|
|
1005
1717
|
state = {
|
|
@@ -1009,43 +1721,88 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1009
1721
|
notify();
|
|
1010
1722
|
};
|
|
1011
1723
|
const startRecording = async () => {
|
|
1724
|
+
if (disposed) {
|
|
1725
|
+
throw new Error("Voice controller is closed.");
|
|
1726
|
+
}
|
|
1012
1727
|
if (state.isRecording) {
|
|
1013
1728
|
return;
|
|
1014
1729
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1730
|
+
if (startRecordingPromise)
|
|
1731
|
+
return startRecordingPromise;
|
|
1732
|
+
const generation = ++captureGeneration;
|
|
1733
|
+
const activeCapture = ensureCapture();
|
|
1734
|
+
const operation = (async () => {
|
|
1735
|
+
try {
|
|
1736
|
+
state = {
|
|
1737
|
+
...state,
|
|
1738
|
+
recordingError: null
|
|
1739
|
+
};
|
|
1740
|
+
notify();
|
|
1741
|
+
await activeCapture.start();
|
|
1742
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
1743
|
+
activeCapture.stop();
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
state = {
|
|
1747
|
+
...state,
|
|
1748
|
+
isRecording: true
|
|
1749
|
+
};
|
|
1750
|
+
notify();
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
activeCapture.stop();
|
|
1753
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
1754
|
+
throw error;
|
|
1755
|
+
}
|
|
1756
|
+
capture = null;
|
|
1757
|
+
state = {
|
|
1758
|
+
...state,
|
|
1759
|
+
isRecording: false,
|
|
1760
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
1761
|
+
};
|
|
1762
|
+
notify();
|
|
1763
|
+
throw error;
|
|
1764
|
+
}
|
|
1765
|
+
})();
|
|
1766
|
+
const pending = operation.finally(() => {
|
|
1767
|
+
if (startRecordingPromise === pending)
|
|
1768
|
+
startRecordingPromise = null;
|
|
1769
|
+
});
|
|
1770
|
+
startRecordingPromise = pending;
|
|
1771
|
+
return startRecordingPromise;
|
|
1772
|
+
};
|
|
1773
|
+
const close = (reason) => {
|
|
1774
|
+
disposed = true;
|
|
1775
|
+
unsubscribeStream();
|
|
1776
|
+
stopRecording();
|
|
1777
|
+
stream.close(reason);
|
|
1037
1778
|
};
|
|
1038
|
-
const
|
|
1779
|
+
const disconnect = () => {
|
|
1780
|
+
disposed = true;
|
|
1039
1781
|
unsubscribeStream();
|
|
1040
1782
|
stopRecording();
|
|
1041
|
-
stream.
|
|
1783
|
+
stream.disconnect();
|
|
1042
1784
|
};
|
|
1043
1785
|
return {
|
|
1044
|
-
|
|
1045
|
-
|
|
1786
|
+
close,
|
|
1787
|
+
disconnect,
|
|
1788
|
+
startRecording,
|
|
1789
|
+
stopRecording,
|
|
1790
|
+
get assistantAudio() {
|
|
1791
|
+
return state.assistantAudio;
|
|
1792
|
+
},
|
|
1793
|
+
get assistantTexts() {
|
|
1794
|
+
return state.assistantTexts;
|
|
1795
|
+
},
|
|
1796
|
+
get assistantStreamingText() {
|
|
1797
|
+
return state.assistantStreamingText;
|
|
1798
|
+
},
|
|
1799
|
+
bindHTMX(bindingOptions) {
|
|
1800
|
+
return bindVoiceHTMX(stream, bindingOptions);
|
|
1801
|
+
},
|
|
1802
|
+
get call() {
|
|
1803
|
+
return state.call;
|
|
1046
1804
|
},
|
|
1047
1805
|
callControl: (message) => stream.callControl(message),
|
|
1048
|
-
close,
|
|
1049
1806
|
endTurn: () => stream.endTurn(),
|
|
1050
1807
|
get error() {
|
|
1051
1808
|
return state.error;
|
|
@@ -1061,21 +1818,26 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1061
1818
|
get partial() {
|
|
1062
1819
|
return state.partial;
|
|
1063
1820
|
},
|
|
1821
|
+
get reconnect() {
|
|
1822
|
+
return state.reconnect;
|
|
1823
|
+
},
|
|
1064
1824
|
get recordingError() {
|
|
1065
1825
|
return state.recordingError;
|
|
1066
1826
|
},
|
|
1827
|
+
get scenarioId() {
|
|
1828
|
+
return state.scenarioId;
|
|
1829
|
+
},
|
|
1067
1830
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1068
1831
|
get sessionId() {
|
|
1069
1832
|
return state.sessionId;
|
|
1070
1833
|
},
|
|
1071
|
-
get
|
|
1072
|
-
return state.
|
|
1834
|
+
get sessionMetadata() {
|
|
1835
|
+
return state.sessionMetadata;
|
|
1073
1836
|
},
|
|
1074
|
-
|
|
1837
|
+
simulateDisconnect: () => stream.simulateDisconnect(),
|
|
1075
1838
|
get status() {
|
|
1076
1839
|
return state.status;
|
|
1077
1840
|
},
|
|
1078
|
-
stopRecording,
|
|
1079
1841
|
subscribe: (subscriber) => {
|
|
1080
1842
|
subscribers.add(subscriber);
|
|
1081
1843
|
return () => {
|
|
@@ -1091,15 +1853,778 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1091
1853
|
},
|
|
1092
1854
|
get turns() {
|
|
1093
1855
|
return state.turns;
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
// src/client/timeStretch.ts
|
|
1861
|
+
var HOP_MS = 10;
|
|
1862
|
+
var SEEK_MS = 5;
|
|
1863
|
+
var ENERGY_EPSILON = 0.000001;
|
|
1864
|
+
var HALF = 0.5;
|
|
1865
|
+
var MS_PER_SECOND = 1000;
|
|
1866
|
+
var makeHann = (length) => {
|
|
1867
|
+
const weights = new Float32Array(length);
|
|
1868
|
+
for (let index = 0;index < length; index += 1) {
|
|
1869
|
+
weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
|
|
1870
|
+
}
|
|
1871
|
+
return weights;
|
|
1872
|
+
};
|
|
1873
|
+
var correlationScore = (base, start, ref, length) => {
|
|
1874
|
+
let dot = 0;
|
|
1875
|
+
let energy = 0;
|
|
1876
|
+
for (let index = 0;index < length; index += 1) {
|
|
1877
|
+
const sample = base[start + index] ?? 0;
|
|
1878
|
+
dot += sample * (ref[index] ?? 0);
|
|
1879
|
+
energy += sample * sample;
|
|
1880
|
+
}
|
|
1881
|
+
return dot / Math.sqrt(energy + ENERGY_EPSILON);
|
|
1882
|
+
};
|
|
1883
|
+
var overlapAddGrain = (src, off, tail, weights, hop) => {
|
|
1884
|
+
const out = new Float32Array(hop);
|
|
1885
|
+
const nextTail = new Float32Array(hop);
|
|
1886
|
+
for (let index = 0;index < hop; index += 1) {
|
|
1887
|
+
out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
|
|
1888
|
+
nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
|
|
1889
|
+
}
|
|
1890
|
+
return { nextTail, out };
|
|
1891
|
+
};
|
|
1892
|
+
var createTimeStretcher = () => {
|
|
1893
|
+
let sampleRate = 0;
|
|
1894
|
+
let channelCount = 0;
|
|
1895
|
+
let hop = 0;
|
|
1896
|
+
let frameLen = 0;
|
|
1897
|
+
let seek = 0;
|
|
1898
|
+
let weights = new Float32Array(0);
|
|
1899
|
+
let buffers = [];
|
|
1900
|
+
let inputStart = 0;
|
|
1901
|
+
let analysisPos = 0;
|
|
1902
|
+
let olaTail = [];
|
|
1903
|
+
let naturalRef = null;
|
|
1904
|
+
const init = (rate, channels) => {
|
|
1905
|
+
sampleRate = rate;
|
|
1906
|
+
channelCount = channels;
|
|
1907
|
+
hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
|
|
1908
|
+
frameLen = hop * 2;
|
|
1909
|
+
seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
|
|
1910
|
+
weights = makeHann(frameLen);
|
|
1911
|
+
buffers = Array.from({ length: channels }, () => new Float32Array(0));
|
|
1912
|
+
olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
|
|
1913
|
+
inputStart = 0;
|
|
1914
|
+
analysisPos = seek;
|
|
1915
|
+
naturalRef = null;
|
|
1916
|
+
};
|
|
1917
|
+
const reset = () => {
|
|
1918
|
+
buffers = buffers.map(() => new Float32Array(0));
|
|
1919
|
+
olaTail = olaTail.map(() => new Float32Array(hop));
|
|
1920
|
+
inputStart = 0;
|
|
1921
|
+
analysisPos = seek;
|
|
1922
|
+
naturalRef = null;
|
|
1923
|
+
};
|
|
1924
|
+
const append = (input) => {
|
|
1925
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1926
|
+
const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
|
|
1927
|
+
const existing = buffers[channel] ?? new Float32Array(0);
|
|
1928
|
+
const merged = new Float32Array(existing.length + incoming.length);
|
|
1929
|
+
merged.set(existing, 0);
|
|
1930
|
+
merged.set(incoming, existing.length);
|
|
1931
|
+
buffers[channel] = merged;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
|
|
1935
|
+
const compact = () => {
|
|
1936
|
+
const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
|
|
1937
|
+
if (keepFrom <= inputStart)
|
|
1938
|
+
return;
|
|
1939
|
+
const drop = keepFrom - inputStart;
|
|
1940
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1941
|
+
buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
|
|
1942
|
+
}
|
|
1943
|
+
inputStart = keepFrom;
|
|
1944
|
+
};
|
|
1945
|
+
const bestOffset = (center) => {
|
|
1946
|
+
if (!naturalRef)
|
|
1947
|
+
return 0;
|
|
1948
|
+
const [base] = buffers;
|
|
1949
|
+
if (!base)
|
|
1950
|
+
return 0;
|
|
1951
|
+
let bestDelta = 0;
|
|
1952
|
+
let bestScore = -Infinity;
|
|
1953
|
+
for (let delta = -seek;delta <= seek; delta += 1) {
|
|
1954
|
+
const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
|
|
1955
|
+
if (score <= bestScore)
|
|
1956
|
+
continue;
|
|
1957
|
+
bestScore = score;
|
|
1958
|
+
bestDelta = delta;
|
|
1959
|
+
}
|
|
1960
|
+
return bestDelta;
|
|
1961
|
+
};
|
|
1962
|
+
const process = (input, speed, rate) => {
|
|
1963
|
+
const channels = Math.max(1, input.length);
|
|
1964
|
+
if (sampleRate !== rate || channelCount !== channels)
|
|
1965
|
+
init(rate, channels);
|
|
1966
|
+
append(input);
|
|
1967
|
+
const analysisHop = hop * speed;
|
|
1968
|
+
const segments = Array.from({ length: channelCount }, () => []);
|
|
1969
|
+
const emitGrain = (pos) => {
|
|
1970
|
+
const off = pos - inputStart;
|
|
1971
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1972
|
+
const src = buffers[channel];
|
|
1973
|
+
const tail = olaTail[channel];
|
|
1974
|
+
if (!src || !tail)
|
|
1975
|
+
continue;
|
|
1976
|
+
const grain = overlapAddGrain(src, off, tail, weights, hop);
|
|
1977
|
+
olaTail[channel] = grain.nextTail;
|
|
1978
|
+
segments[channel]?.push(grain.out);
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
const captureRef = (pos) => {
|
|
1982
|
+
const ref = new Float32Array(frameLen);
|
|
1983
|
+
const refOff = pos + hop - inputStart;
|
|
1984
|
+
const [base] = buffers;
|
|
1985
|
+
if (base)
|
|
1986
|
+
ref.set(base.subarray(refOff, refOff + frameLen));
|
|
1987
|
+
naturalRef = ref;
|
|
1988
|
+
};
|
|
1989
|
+
const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
|
|
1990
|
+
while (canEmit()) {
|
|
1991
|
+
const center = Math.round(analysisPos);
|
|
1992
|
+
const pos = center + bestOffset(center);
|
|
1993
|
+
emitGrain(pos);
|
|
1994
|
+
captureRef(pos);
|
|
1995
|
+
analysisPos += analysisHop;
|
|
1996
|
+
}
|
|
1997
|
+
compact();
|
|
1998
|
+
return segments.map((channelSegments) => {
|
|
1999
|
+
const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
|
|
2000
|
+
const merged = new Float32Array(total);
|
|
2001
|
+
let offset = 0;
|
|
2002
|
+
for (const seg of channelSegments) {
|
|
2003
|
+
merged.set(seg, offset);
|
|
2004
|
+
offset += seg.length;
|
|
2005
|
+
}
|
|
2006
|
+
return merged;
|
|
2007
|
+
});
|
|
2008
|
+
};
|
|
2009
|
+
return { process, reset };
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
// src/client/audioPlayer.ts
|
|
2013
|
+
var activeAudioPlayers = new Set;
|
|
2014
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
2015
|
+
var DEFAULT_VOLUME = 1;
|
|
2016
|
+
var DEFAULT_PLAYBACK_RATE = 1;
|
|
2017
|
+
var MIN_PLAYBACK_RATE = 0.5;
|
|
2018
|
+
var MAX_PLAYBACK_RATE = 2;
|
|
2019
|
+
var STRETCH_BYPASS_EPSILON = 0.01;
|
|
2020
|
+
var ANALYSER_FFT_SIZE = 256;
|
|
2021
|
+
var PCM_BYTE_MIDPOINT = 128;
|
|
2022
|
+
var GAP_EPSILON_S = 0.03;
|
|
2023
|
+
var createInitialState3 = () => ({
|
|
2024
|
+
activeSourceCount: 0,
|
|
2025
|
+
error: null,
|
|
2026
|
+
isActive: false,
|
|
2027
|
+
isPlaying: false,
|
|
2028
|
+
lastInterruptLatencyMs: undefined,
|
|
2029
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
2030
|
+
processedChunkCount: 0,
|
|
2031
|
+
queuedChunkCount: 0
|
|
2032
|
+
});
|
|
2033
|
+
var getAudioContextCtor = () => {
|
|
2034
|
+
if (typeof window === "undefined") {
|
|
2035
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
2036
|
+
}
|
|
2037
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
2038
|
+
};
|
|
2039
|
+
var clampVolume = (volume) => {
|
|
2040
|
+
if (typeof volume !== "number" || !Number.isFinite(volume)) {
|
|
2041
|
+
return DEFAULT_VOLUME;
|
|
2042
|
+
}
|
|
2043
|
+
return Math.min(1, Math.max(0, volume));
|
|
2044
|
+
};
|
|
2045
|
+
var clampPlaybackRate = (rate) => {
|
|
2046
|
+
if (typeof rate !== "number" || !Number.isFinite(rate)) {
|
|
2047
|
+
return DEFAULT_PLAYBACK_RATE;
|
|
2048
|
+
}
|
|
2049
|
+
return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
|
|
2050
|
+
};
|
|
2051
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
2052
|
+
const { format } = chunk;
|
|
2053
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
2054
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
2055
|
+
}
|
|
2056
|
+
const bytes = chunk.chunk;
|
|
2057
|
+
const channels = Math.max(1, format.channels);
|
|
2058
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
2059
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
2060
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
2061
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2062
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2063
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
2064
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
2065
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
2066
|
+
const sampleOffset = sampleIndex * 2;
|
|
2067
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
2068
|
+
channelData[frameIndex] = 0;
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return audioBuffer;
|
|
2075
|
+
};
|
|
2076
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
2077
|
+
const subscribers = new Set;
|
|
2078
|
+
const sourceNodes = new Set;
|
|
2079
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
2080
|
+
let state = createInitialState3();
|
|
2081
|
+
let audioContext = null;
|
|
2082
|
+
let outputNode = null;
|
|
2083
|
+
let analyserNode = null;
|
|
2084
|
+
let analyserBuffer = null;
|
|
2085
|
+
let volume = clampVolume(options.volume);
|
|
2086
|
+
let playbackRate = clampPlaybackRate(options.playbackRate);
|
|
2087
|
+
let stretcher = null;
|
|
2088
|
+
let queueEndTime = 0;
|
|
2089
|
+
let syncPromise = Promise.resolve();
|
|
2090
|
+
let interruptStartedAt = null;
|
|
2091
|
+
let interruptPromise = null;
|
|
2092
|
+
let resolveInterruptPromise = null;
|
|
2093
|
+
let interruptFallbackTimer = null;
|
|
2094
|
+
let concurrencyCounted = false;
|
|
2095
|
+
let observedPeakConcurrency = 0;
|
|
2096
|
+
let scheduledDurationMs = 0;
|
|
2097
|
+
let gapCount = 0;
|
|
2098
|
+
let totalGapMs = 0;
|
|
2099
|
+
let maxGapMs = 0;
|
|
2100
|
+
let errorCount = 0;
|
|
2101
|
+
let hasScheduledAnyChunk = false;
|
|
2102
|
+
const sampleConcurrency = () => {
|
|
2103
|
+
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
2104
|
+
observedPeakConcurrency = activeAudioPlayers.size;
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
const markPlayerActive = () => {
|
|
2108
|
+
if (concurrencyCounted) {
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
concurrencyCounted = true;
|
|
2112
|
+
activeAudioPlayers.add(sampleConcurrency);
|
|
2113
|
+
for (const sample of activeAudioPlayers) {
|
|
2114
|
+
sample();
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
const markPlayerInactive = () => {
|
|
2118
|
+
if (!concurrencyCounted) {
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
concurrencyCounted = false;
|
|
2122
|
+
activeAudioPlayers.delete(sampleConcurrency);
|
|
2123
|
+
};
|
|
2124
|
+
const notify = () => {
|
|
2125
|
+
for (const subscriber of subscribers) {
|
|
2126
|
+
subscriber();
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
const setState = (next) => {
|
|
2130
|
+
state = {
|
|
2131
|
+
...state,
|
|
2132
|
+
...next
|
|
2133
|
+
};
|
|
2134
|
+
notify();
|
|
2135
|
+
};
|
|
2136
|
+
const clearError = () => {
|
|
2137
|
+
if (state.error !== null) {
|
|
2138
|
+
setState({ error: null });
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
const clearInterruptTimer = () => {
|
|
2142
|
+
if (interruptFallbackTimer !== null) {
|
|
2143
|
+
clearTimeout(interruptFallbackTimer);
|
|
2144
|
+
interruptFallbackTimer = null;
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
const resolveInterrupt = (latencyMs) => {
|
|
2148
|
+
clearInterruptTimer();
|
|
2149
|
+
interruptStartedAt = null;
|
|
2150
|
+
stretcher?.reset();
|
|
2151
|
+
setState({
|
|
2152
|
+
activeSourceCount: sourceNodes.size,
|
|
2153
|
+
isPlaying: false,
|
|
2154
|
+
lastInterruptLatencyMs: latencyMs,
|
|
2155
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
2156
|
+
});
|
|
2157
|
+
resolveInterruptPromise?.();
|
|
2158
|
+
resolveInterruptPromise = null;
|
|
2159
|
+
interruptPromise = null;
|
|
2160
|
+
};
|
|
2161
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
2162
|
+
if (!context) {
|
|
2163
|
+
return 0;
|
|
2164
|
+
}
|
|
2165
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
2166
|
+
};
|
|
2167
|
+
const applyOutputGain = (context) => {
|
|
2168
|
+
if (!outputNode) {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const gainValue = volume;
|
|
2172
|
+
if (outputNode.gain.setValueAtTime) {
|
|
2173
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
outputNode.gain.value = gainValue;
|
|
2177
|
+
};
|
|
2178
|
+
const muteOutputGain = (context) => {
|
|
2179
|
+
if (!outputNode) {
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
const gainValue = 0;
|
|
2183
|
+
if (outputNode.gain.setValueAtTime) {
|
|
2184
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
outputNode.gain.value = gainValue;
|
|
2188
|
+
};
|
|
2189
|
+
const maybeResolveInterrupt = () => {
|
|
2190
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
2194
|
+
};
|
|
2195
|
+
const ensureAudioContext = async () => {
|
|
2196
|
+
if (audioContext) {
|
|
2197
|
+
return audioContext;
|
|
2198
|
+
}
|
|
2199
|
+
if (options.createAudioContext) {
|
|
2200
|
+
audioContext = options.createAudioContext();
|
|
2201
|
+
} else {
|
|
2202
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
2203
|
+
if (!AudioContextCtor) {
|
|
2204
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
2205
|
+
}
|
|
2206
|
+
audioContext = new AudioContextCtor;
|
|
2207
|
+
}
|
|
2208
|
+
if (audioContext.createGain) {
|
|
2209
|
+
outputNode = audioContext.createGain();
|
|
2210
|
+
outputNode.connect?.(audioContext.destination);
|
|
2211
|
+
if (audioContext.createAnalyser) {
|
|
2212
|
+
analyserNode = audioContext.createAnalyser();
|
|
2213
|
+
analyserNode.fftSize = ANALYSER_FFT_SIZE;
|
|
2214
|
+
analyserBuffer = new Uint8Array(analyserNode.fftSize);
|
|
2215
|
+
outputNode.connect?.(analyserNode);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
queueEndTime = audioContext.currentTime;
|
|
2219
|
+
return audioContext;
|
|
2220
|
+
};
|
|
2221
|
+
const scheduleBuffer = (context, buffer, rate) => {
|
|
2222
|
+
const node = context.createBufferSource();
|
|
2223
|
+
node.buffer = buffer;
|
|
2224
|
+
if (node.playbackRate) {
|
|
2225
|
+
node.playbackRate.value = rate;
|
|
2226
|
+
}
|
|
2227
|
+
node.connect(outputNode ?? context.destination);
|
|
2228
|
+
node.onended = () => {
|
|
2229
|
+
sourceNodes.delete(node);
|
|
2230
|
+
node.disconnect?.();
|
|
2231
|
+
setState({
|
|
2232
|
+
activeSourceCount: sourceNodes.size,
|
|
2233
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
2234
|
+
});
|
|
2235
|
+
maybeResolveInterrupt();
|
|
2236
|
+
};
|
|
2237
|
+
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
2238
|
+
const startAt = Math.max(earliestStart, queueEndTime);
|
|
2239
|
+
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
2240
|
+
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
2241
|
+
gapCount += 1;
|
|
2242
|
+
totalGapMs += gapMs;
|
|
2243
|
+
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
2244
|
+
}
|
|
2245
|
+
hasScheduledAnyChunk = true;
|
|
2246
|
+
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
2247
|
+
sampleConcurrency();
|
|
2248
|
+
queueEndTime = startAt + buffer.duration / rate;
|
|
2249
|
+
sourceNodes.add(node);
|
|
2250
|
+
setState({
|
|
2251
|
+
activeSourceCount: sourceNodes.size,
|
|
2252
|
+
isPlaying: true
|
|
2253
|
+
});
|
|
2254
|
+
node.start(startAt);
|
|
2255
|
+
};
|
|
2256
|
+
const scheduleChunk = async (chunk) => {
|
|
2257
|
+
const context = await ensureAudioContext();
|
|
2258
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
2259
|
+
if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
|
|
2260
|
+
stretcher?.reset();
|
|
2261
|
+
scheduleBuffer(context, buffer, playbackRate);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const channels = Math.max(1, chunk.format.channels);
|
|
2265
|
+
const input = [];
|
|
2266
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2267
|
+
input.push(buffer.getChannelData(channelIndex));
|
|
2268
|
+
}
|
|
2269
|
+
stretcher ??= createTimeStretcher();
|
|
2270
|
+
const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
|
|
2271
|
+
const outLength = stretched[0]?.length ?? 0;
|
|
2272
|
+
if (outLength === 0) {
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
|
|
2276
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2277
|
+
const channelOut = stretched[channelIndex];
|
|
2278
|
+
if (!channelOut)
|
|
2279
|
+
continue;
|
|
2280
|
+
outBuffer.getChannelData(channelIndex).set(channelOut);
|
|
2281
|
+
}
|
|
2282
|
+
scheduleBuffer(context, outBuffer, 1);
|
|
2283
|
+
};
|
|
2284
|
+
const stopQueuedPlayback = (options2) => {
|
|
2285
|
+
for (const node of [...sourceNodes]) {
|
|
2286
|
+
node.stop?.();
|
|
2287
|
+
}
|
|
2288
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
2289
|
+
if (options2?.forceClear) {
|
|
2290
|
+
for (const node of sourceNodes) {
|
|
2291
|
+
node.disconnect?.();
|
|
2292
|
+
}
|
|
2293
|
+
sourceNodes.clear();
|
|
2294
|
+
maybeResolveInterrupt();
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
const sync = async () => {
|
|
2298
|
+
if (!state.isActive) {
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
2302
|
+
if (nextChunks.length === 0) {
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
try {
|
|
2306
|
+
clearError();
|
|
2307
|
+
for (const chunk of nextChunks) {
|
|
2308
|
+
await scheduleChunk(chunk);
|
|
2309
|
+
}
|
|
2310
|
+
setState({
|
|
2311
|
+
processedChunkCount: source.assistantAudio.length,
|
|
2312
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
2313
|
+
});
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
errorCount += 1;
|
|
2316
|
+
setState({
|
|
2317
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
const queueSync = () => {
|
|
2322
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
2323
|
+
return syncPromise;
|
|
2324
|
+
};
|
|
2325
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
2326
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
2327
|
+
player.start();
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (state.isActive) {
|
|
2331
|
+
queueSync();
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
const player = {
|
|
2335
|
+
get activeSourceCount() {
|
|
2336
|
+
return state.activeSourceCount;
|
|
1094
2337
|
},
|
|
1095
|
-
|
|
1096
|
-
|
|
2338
|
+
close: async () => {
|
|
2339
|
+
markPlayerInactive();
|
|
2340
|
+
unsubscribeSource();
|
|
2341
|
+
stopQueuedPlayback({ forceClear: true });
|
|
2342
|
+
clearInterruptTimer();
|
|
2343
|
+
resolveInterruptPromise?.();
|
|
2344
|
+
resolveInterruptPromise = null;
|
|
2345
|
+
interruptPromise = null;
|
|
2346
|
+
interruptStartedAt = null;
|
|
2347
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
2348
|
+
await audioContext.close();
|
|
2349
|
+
}
|
|
2350
|
+
audioContext = null;
|
|
2351
|
+
outputNode?.disconnect?.();
|
|
2352
|
+
outputNode = null;
|
|
2353
|
+
analyserNode?.disconnect?.();
|
|
2354
|
+
analyserNode = null;
|
|
2355
|
+
analyserBuffer = null;
|
|
2356
|
+
queueEndTime = 0;
|
|
2357
|
+
setState({
|
|
2358
|
+
activeSourceCount: 0,
|
|
2359
|
+
isActive: false,
|
|
2360
|
+
isPlaying: false
|
|
2361
|
+
});
|
|
1097
2362
|
},
|
|
1098
|
-
get
|
|
1099
|
-
return state.
|
|
2363
|
+
get error() {
|
|
2364
|
+
return state.error;
|
|
1100
2365
|
},
|
|
1101
|
-
|
|
1102
|
-
|
|
2366
|
+
getOutputLevel: () => {
|
|
2367
|
+
if (!analyserNode || !analyserBuffer) {
|
|
2368
|
+
return 0;
|
|
2369
|
+
}
|
|
2370
|
+
analyserNode.getByteTimeDomainData(analyserBuffer);
|
|
2371
|
+
let sumSquares = 0;
|
|
2372
|
+
for (const sample of analyserBuffer) {
|
|
2373
|
+
const centered = (sample - PCM_BYTE_MIDPOINT) / PCM_BYTE_MIDPOINT;
|
|
2374
|
+
sumSquares += centered * centered;
|
|
2375
|
+
}
|
|
2376
|
+
return Math.sqrt(sumSquares / analyserBuffer.length);
|
|
2377
|
+
},
|
|
2378
|
+
getSnapshot: () => state,
|
|
2379
|
+
getIntegritySummary: () => {
|
|
2380
|
+
const chunksReceived = source.assistantAudio.length;
|
|
2381
|
+
const chunksScheduled = state.processedChunkCount;
|
|
2382
|
+
return {
|
|
2383
|
+
chunksReceived,
|
|
2384
|
+
chunksScheduled,
|
|
2385
|
+
errorCount,
|
|
2386
|
+
gapCount,
|
|
2387
|
+
maxConcurrentPlayers: observedPeakConcurrency,
|
|
2388
|
+
maxGapMs: Math.round(maxGapMs),
|
|
2389
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
|
|
2390
|
+
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
2391
|
+
totalGapMs: Math.round(totalGapMs)
|
|
2392
|
+
};
|
|
2393
|
+
},
|
|
2394
|
+
interrupt: async () => {
|
|
2395
|
+
const startedAt = Date.now();
|
|
2396
|
+
const context = await ensureAudioContext();
|
|
2397
|
+
interruptStartedAt = startedAt;
|
|
2398
|
+
muteOutputGain(context);
|
|
2399
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
2400
|
+
setState({
|
|
2401
|
+
isActive: false,
|
|
2402
|
+
isPlaying: sourceNodes.size > 0,
|
|
2403
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
2404
|
+
});
|
|
2405
|
+
if (sourceNodes.size === 0) {
|
|
2406
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
if (!interruptPromise) {
|
|
2410
|
+
interruptPromise = new Promise((resolve) => {
|
|
2411
|
+
resolveInterruptPromise = resolve;
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
clearInterruptTimer();
|
|
2415
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
2416
|
+
for (const node of sourceNodes) {
|
|
2417
|
+
node.disconnect?.();
|
|
2418
|
+
}
|
|
2419
|
+
sourceNodes.clear();
|
|
2420
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
2421
|
+
}, 250);
|
|
2422
|
+
stopQueuedPlayback();
|
|
2423
|
+
await interruptPromise;
|
|
2424
|
+
},
|
|
2425
|
+
get isActive() {
|
|
2426
|
+
return state.isActive;
|
|
2427
|
+
},
|
|
2428
|
+
get isPlaying() {
|
|
2429
|
+
return state.isPlaying;
|
|
2430
|
+
},
|
|
2431
|
+
get lastInterruptLatencyMs() {
|
|
2432
|
+
return state.lastInterruptLatencyMs;
|
|
2433
|
+
},
|
|
2434
|
+
get lastPlaybackStopLatencyMs() {
|
|
2435
|
+
return state.lastPlaybackStopLatencyMs;
|
|
2436
|
+
},
|
|
2437
|
+
pause: async () => {
|
|
2438
|
+
if (!audioContext) {
|
|
2439
|
+
setState({
|
|
2440
|
+
activeSourceCount: 0,
|
|
2441
|
+
isActive: false,
|
|
2442
|
+
isPlaying: false
|
|
2443
|
+
});
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
await audioContext.suspend();
|
|
2447
|
+
markPlayerInactive();
|
|
2448
|
+
setState({
|
|
2449
|
+
activeSourceCount: sourceNodes.size,
|
|
2450
|
+
isActive: false,
|
|
2451
|
+
isPlaying: false
|
|
2452
|
+
});
|
|
2453
|
+
},
|
|
2454
|
+
get playbackRate() {
|
|
2455
|
+
return playbackRate;
|
|
2456
|
+
},
|
|
2457
|
+
get processedChunkCount() {
|
|
2458
|
+
return state.processedChunkCount;
|
|
2459
|
+
},
|
|
2460
|
+
get queuedChunkCount() {
|
|
2461
|
+
return state.queuedChunkCount;
|
|
2462
|
+
},
|
|
2463
|
+
setPlaybackRate: (nextRate) => {
|
|
2464
|
+
playbackRate = clampPlaybackRate(nextRate);
|
|
2465
|
+
},
|
|
2466
|
+
setVolume: (nextVolume) => {
|
|
2467
|
+
volume = clampVolume(nextVolume);
|
|
2468
|
+
applyOutputGain(audioContext);
|
|
2469
|
+
},
|
|
2470
|
+
start: async () => {
|
|
2471
|
+
try {
|
|
2472
|
+
clearError();
|
|
2473
|
+
const context = await ensureAudioContext();
|
|
2474
|
+
applyOutputGain(context);
|
|
2475
|
+
if (context.state === "suspended") {
|
|
2476
|
+
await context.resume();
|
|
2477
|
+
}
|
|
2478
|
+
markPlayerActive();
|
|
2479
|
+
setState({
|
|
2480
|
+
activeSourceCount: sourceNodes.size,
|
|
2481
|
+
isActive: true,
|
|
2482
|
+
isPlaying: context.state === "running"
|
|
2483
|
+
});
|
|
2484
|
+
await queueSync();
|
|
2485
|
+
} catch (error) {
|
|
2486
|
+
errorCount += 1;
|
|
2487
|
+
markPlayerInactive();
|
|
2488
|
+
setState({
|
|
2489
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2490
|
+
isActive: false,
|
|
2491
|
+
isPlaying: false
|
|
2492
|
+
});
|
|
2493
|
+
throw error;
|
|
2494
|
+
}
|
|
2495
|
+
},
|
|
2496
|
+
subscribe: (subscriber) => {
|
|
2497
|
+
subscribers.add(subscriber);
|
|
2498
|
+
return () => {
|
|
2499
|
+
subscribers.delete(subscriber);
|
|
2500
|
+
};
|
|
2501
|
+
},
|
|
2502
|
+
get volume() {
|
|
2503
|
+
return volume;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
return player;
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
// src/client/bargeInMonitor.ts
|
|
2510
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
2511
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
2512
|
+
var summarize = (events, thresholdMs) => {
|
|
2513
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
2514
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
2515
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
2516
|
+
const passed = stopped.length - failed;
|
|
2517
|
+
return {
|
|
2518
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
2519
|
+
events: [...events],
|
|
2520
|
+
failed,
|
|
2521
|
+
lastEvent: events.at(-1),
|
|
2522
|
+
passed,
|
|
2523
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
2524
|
+
thresholdMs,
|
|
2525
|
+
total: stopped.length
|
|
2526
|
+
};
|
|
2527
|
+
};
|
|
2528
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
2529
|
+
const listeners = new Set;
|
|
2530
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
2531
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2532
|
+
const events = [];
|
|
2533
|
+
const emit = () => {
|
|
2534
|
+
for (const listener of listeners) {
|
|
2535
|
+
listener();
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
const postEvent = (event) => {
|
|
2539
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
fetchImpl(options.path, {
|
|
2543
|
+
body: JSON.stringify(event),
|
|
2544
|
+
headers: {
|
|
2545
|
+
"Content-Type": "application/json"
|
|
2546
|
+
},
|
|
2547
|
+
method: "POST"
|
|
2548
|
+
}).catch(() => {});
|
|
2549
|
+
};
|
|
2550
|
+
const record = (status, input) => {
|
|
2551
|
+
const event = {
|
|
2552
|
+
at: Date.now(),
|
|
2553
|
+
id: createEventId(),
|
|
2554
|
+
latencyMs: input.latencyMs,
|
|
2555
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
2556
|
+
reason: input.reason,
|
|
2557
|
+
sessionId: input.sessionId,
|
|
2558
|
+
status,
|
|
2559
|
+
thresholdMs
|
|
2560
|
+
};
|
|
2561
|
+
events.push(event);
|
|
2562
|
+
postEvent(event);
|
|
2563
|
+
emit();
|
|
2564
|
+
return event;
|
|
2565
|
+
};
|
|
2566
|
+
return {
|
|
2567
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
2568
|
+
recordRequested: (input) => record("requested", input),
|
|
2569
|
+
recordSkipped: (input) => record("skipped", input),
|
|
2570
|
+
recordStopped: (input) => record("stopped", input),
|
|
2571
|
+
subscribe: (subscriber) => {
|
|
2572
|
+
listeners.add(subscriber);
|
|
2573
|
+
return () => {
|
|
2574
|
+
listeners.delete(subscriber);
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// src/client/duplex.ts
|
|
2581
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
2582
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
2583
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
2584
|
+
let lastPartial = controller.partial;
|
|
2585
|
+
const interruptIfPlaying = (reason) => {
|
|
2586
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
2587
|
+
options.monitor?.recordSkipped({
|
|
2588
|
+
reason,
|
|
2589
|
+
sessionId: controller.sessionId
|
|
2590
|
+
});
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
options.monitor?.recordRequested({
|
|
2594
|
+
reason,
|
|
2595
|
+
sessionId: controller.sessionId
|
|
2596
|
+
});
|
|
2597
|
+
player.interrupt().then(() => {
|
|
2598
|
+
options.monitor?.recordStopped({
|
|
2599
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
2600
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
2601
|
+
reason,
|
|
2602
|
+
sessionId: controller.sessionId
|
|
2603
|
+
});
|
|
2604
|
+
});
|
|
2605
|
+
};
|
|
2606
|
+
const unsubscribe = controller.subscribe(() => {
|
|
2607
|
+
if (options.interruptOnPartial === false) {
|
|
2608
|
+
lastPartial = controller.partial;
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
if (!lastPartial && controller.partial) {
|
|
2612
|
+
interruptIfPlaying("partial-transcript");
|
|
2613
|
+
}
|
|
2614
|
+
lastPartial = controller.partial;
|
|
2615
|
+
});
|
|
2616
|
+
return {
|
|
2617
|
+
close: () => {
|
|
2618
|
+
unsubscribe();
|
|
2619
|
+
},
|
|
2620
|
+
handleLevel: (level) => {
|
|
2621
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
2622
|
+
interruptIfPlaying("input-level");
|
|
2623
|
+
}
|
|
2624
|
+
},
|
|
2625
|
+
sendAudio: (audio) => {
|
|
2626
|
+
interruptIfPlaying("manual-audio");
|
|
2627
|
+
controller.sendAudio(audio);
|
|
1103
2628
|
}
|
|
1104
2629
|
};
|
|
1105
2630
|
};
|
|
@@ -1126,8 +2651,7 @@ var DEFAULT_GUIDED_PROMPTS = [
|
|
|
1126
2651
|
"Now describe what you are trying to do or test.",
|
|
1127
2652
|
"Finish with any detail that feels blocked, risky, or unclear."
|
|
1128
2653
|
];
|
|
1129
|
-
var clamp = (value, min,
|
|
1130
|
-
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2654
|
+
var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
|
|
1131
2655
|
var readErrorField = (value, key) => {
|
|
1132
2656
|
const candidate = value[key];
|
|
1133
2657
|
if (typeof candidate === "string" && candidate.trim()) {
|
|
@@ -1160,6 +2684,17 @@ var formatErrorMessage = (error) => {
|
|
|
1160
2684
|
}
|
|
1161
2685
|
return "Unexpected error";
|
|
1162
2686
|
};
|
|
2687
|
+
var formatReconnectState = (reconnect) => {
|
|
2688
|
+
const pieces = [reconnect.status];
|
|
2689
|
+
if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
|
|
2690
|
+
pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
|
|
2691
|
+
}
|
|
2692
|
+
if (reconnect.nextAttemptAt) {
|
|
2693
|
+
const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
|
|
2694
|
+
pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
|
|
2695
|
+
}
|
|
2696
|
+
return pieces.join(" · ");
|
|
2697
|
+
};
|
|
1163
2698
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1164
2699
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1165
2700
|
const next = levels.slice(-(count - 1));
|
|
@@ -1216,6 +2751,20 @@ var parsePromptList = (value) => {
|
|
|
1216
2751
|
} catch {}
|
|
1217
2752
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1218
2753
|
};
|
|
2754
|
+
var parseOptionalNumber = (value) => {
|
|
2755
|
+
if (!value) {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const parsed = Number(value);
|
|
2759
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2760
|
+
};
|
|
2761
|
+
var resolveElement2 = (root, selector, ctor) => {
|
|
2762
|
+
if (!selector) {
|
|
2763
|
+
return null;
|
|
2764
|
+
}
|
|
2765
|
+
const value = document.querySelector(selector);
|
|
2766
|
+
return value instanceof ctor ? value : null;
|
|
2767
|
+
};
|
|
1219
2768
|
var requireElement = (root, selector, ctor, name) => {
|
|
1220
2769
|
const value = selector ? document.querySelector(selector) : null;
|
|
1221
2770
|
if (value instanceof ctor) {
|
|
@@ -1266,11 +2815,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1266
2815
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1267
2816
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1268
2817
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
2818
|
+
const reconnectReportPath = root.dataset.voiceReconnectReportPath;
|
|
2819
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
2820
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
2821
|
+
path: bargeInPath,
|
|
2822
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
2823
|
+
}) : null;
|
|
2824
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
2825
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1269
2826
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1270
2827
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1271
2828
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1272
2829
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1273
2830
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
2831
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1274
2832
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1275
2833
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1276
2834
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1279,35 +2837,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1279
2837
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1280
2838
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1281
2839
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
2840
|
+
let activeMode = null;
|
|
2841
|
+
let hasStartedModes = {
|
|
2842
|
+
general: false,
|
|
2843
|
+
guided: false
|
|
2844
|
+
};
|
|
2845
|
+
let isCapturing = false;
|
|
2846
|
+
let micError = null;
|
|
2847
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
2848
|
+
let guidedBargeInBinding = null;
|
|
2849
|
+
let generalBargeInBinding = null;
|
|
1282
2850
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1283
2851
|
capture: {
|
|
2852
|
+
onAudio: (audio, sendAudio) => {
|
|
2853
|
+
if (guidedBargeInBinding) {
|
|
2854
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
sendAudio(audio);
|
|
2858
|
+
},
|
|
1284
2859
|
onLevel: (level) => {
|
|
2860
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1285
2861
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1286
2862
|
renderWave();
|
|
1287
2863
|
}
|
|
1288
2864
|
},
|
|
2865
|
+
connection: {
|
|
2866
|
+
reconnectReportPath
|
|
2867
|
+
},
|
|
1289
2868
|
preset: "guided-intake"
|
|
1290
2869
|
});
|
|
1291
2870
|
const generalVoice = createVoiceController(generalPath, {
|
|
1292
2871
|
capture: {
|
|
2872
|
+
onAudio: (audio, sendAudio) => {
|
|
2873
|
+
if (generalBargeInBinding) {
|
|
2874
|
+
generalBargeInBinding.sendAudio(audio);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
sendAudio(audio);
|
|
2878
|
+
},
|
|
1293
2879
|
onLevel: (level) => {
|
|
2880
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1294
2881
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1295
2882
|
renderWave();
|
|
1296
2883
|
}
|
|
1297
2884
|
},
|
|
2885
|
+
connection: {
|
|
2886
|
+
reconnectReportPath
|
|
2887
|
+
},
|
|
1298
2888
|
preset: "dictation"
|
|
1299
2889
|
});
|
|
1300
2890
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1301
2891
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2892
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
2893
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
2894
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
2895
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2896
|
+
monitor: bargeInMonitor ?? undefined
|
|
2897
|
+
});
|
|
2898
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
2899
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2900
|
+
monitor: bargeInMonitor ?? undefined
|
|
2901
|
+
});
|
|
1310
2902
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
2903
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1311
2904
|
const renderWave = () => {
|
|
1312
2905
|
const path = createVoiceWavePath(waveLevels);
|
|
1313
2906
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1319,9 +2912,12 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1319
2912
|
const render = () => {
|
|
1320
2913
|
const voice = currentVoice();
|
|
1321
2914
|
const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
|
|
1322
|
-
const status = voice
|
|
2915
|
+
const { status } = voice;
|
|
1323
2916
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1324
2917
|
errorStatus.textContent = micError || voice.error || "None";
|
|
2918
|
+
if (reconnectStatus) {
|
|
2919
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2920
|
+
}
|
|
1325
2921
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1326
2922
|
promptStatus.textContent = resolvePromptMessage({
|
|
1327
2923
|
guidedPrompts,
|
|
@@ -1385,8 +2981,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1385
2981
|
render();
|
|
1386
2982
|
}
|
|
1387
2983
|
};
|
|
1388
|
-
guidedVoice.subscribe(
|
|
1389
|
-
|
|
2984
|
+
guidedVoice.subscribe(() => {
|
|
2985
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
2986
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
2987
|
+
}
|
|
2988
|
+
render();
|
|
2989
|
+
});
|
|
2990
|
+
generalVoice.subscribe(() => {
|
|
2991
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
2992
|
+
generalAudioPlayer.start().catch(() => {});
|
|
2993
|
+
}
|
|
2994
|
+
render();
|
|
2995
|
+
});
|
|
1390
2996
|
startGuidedButton.addEventListener("click", () => {
|
|
1391
2997
|
startMode("guided");
|
|
1392
2998
|
});
|
|
@@ -1396,9 +3002,16 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1396
3002
|
stopButton.addEventListener("click", () => {
|
|
1397
3003
|
stopMic();
|
|
1398
3004
|
});
|
|
3005
|
+
root.addEventListener("absolute-voice-simulate-disconnect", () => {
|
|
3006
|
+
currentVoice().simulateDisconnect();
|
|
3007
|
+
});
|
|
1399
3008
|
window.addEventListener("beforeunload", () => {
|
|
1400
3009
|
guidedVoice.stopRecording();
|
|
1401
3010
|
generalVoice.stopRecording();
|
|
3011
|
+
guidedBargeInBinding?.close();
|
|
3012
|
+
generalBargeInBinding?.close();
|
|
3013
|
+
guidedAudioPlayer.close();
|
|
3014
|
+
generalAudioPlayer.close();
|
|
1402
3015
|
stopGuidedBinding();
|
|
1403
3016
|
stopGeneralBinding();
|
|
1404
3017
|
guidedVoice.close();
|