@absolutejs/voice 0.0.22-beta.67 → 0.0.22-beta.670
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 +4562 -1179
- package/dist/angular/index.d.ts +36 -8
- package/dist/angular/index.js +4362 -537
- 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 +2 -2
- 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 +201 -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 +1808 -159
- package/dist/client/htmxDashboardRenderers.d.ts +71 -0
- package/dist/client/index.d.ts +109 -33
- package/dist/client/index.js +10799 -646
- 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 +5 -3
- package/dist/client/turnQualityWidget.d.ts +6 -6
- package/dist/client/voiceWidgetView.d.ts +48 -0
- package/dist/client/workflowStatus.d.ts +5 -3
- package/dist/core/agent.d.ts +251 -0
- 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} +24 -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/{turnQuality.d.ts → core/turnQuality.d.ts} +9 -9
- package/dist/core/types.d.ts +1786 -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 +3060 -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 +2100 -0
- package/dist/embed/voice-widget.js +10 -0
- package/dist/index.d.ts +379 -82
- package/dist/index.js +48514 -8241
- 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 +1 -1
- package/dist/react/VoiceWidget.d.ts +13 -0
- package/dist/react/index.d.ts +80 -15
- package/dist/react/index.js +13740 -2052
- 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 +9 -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 +9 -2
- package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
- package/dist/react/useVoiceTurnLatency.d.ts +9 -0
- package/dist/react/useVoiceTurnQuality.d.ts +1 -1
- 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 +2 -2
- package/dist/svelte/createVoiceWidget.d.ts +19 -0
- package/dist/svelte/createVoiceWorkflowStatus.d.ts +2 -2
- package/dist/svelte/index.d.ts +37 -10
- package/dist/svelte/index.js +7465 -1676
- package/dist/telephony/contract.d.ts +61 -0
- package/dist/telephony/matrix.d.ts +97 -0
- package/dist/telephony/plivo.d.ts +302 -0
- package/dist/telephony/response.d.ts +1 -1
- package/dist/telephony/security.d.ts +182 -0
- package/dist/telephony/telnyx.d.ts +290 -0
- package/dist/telephony/twilio.d.ts +164 -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 +9171 -1290
- 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 +4 -0
- package/dist/vue/VoiceWidget.d.ts +77 -0
- package/dist/vue/index.d.ts +49 -15
- package/dist/vue/index.js +13097 -2219
- 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 +3 -3
- package/dist/vue/useVoiceWorkflowStatus.d.ts +3 -3
- package/package.json +247 -256
- package/dist/agent.d.ts +0 -115
- 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,29 @@ var serverMessageToAction = (message) => {
|
|
|
209
279
|
transcript: message.transcript,
|
|
210
280
|
type: "partial"
|
|
211
281
|
};
|
|
282
|
+
case "playback_rate":
|
|
283
|
+
return {
|
|
284
|
+
rate: message.rate,
|
|
285
|
+
type: "playback_rate"
|
|
286
|
+
};
|
|
287
|
+
case "replay":
|
|
288
|
+
return {
|
|
289
|
+
assistantTexts: message.assistantTexts,
|
|
290
|
+
call: message.call,
|
|
291
|
+
partial: message.partial,
|
|
292
|
+
scenarioId: message.scenarioId,
|
|
293
|
+
sessionId: message.sessionId,
|
|
294
|
+
sessionMetadata: message.sessionMetadata,
|
|
295
|
+
status: message.status,
|
|
296
|
+
turns: message.turns,
|
|
297
|
+
type: "replay"
|
|
298
|
+
};
|
|
212
299
|
case "session":
|
|
213
300
|
return {
|
|
301
|
+
paused: message.paused,
|
|
302
|
+
pauseExpiresAt: message.pauseExpiresAt,
|
|
214
303
|
sessionId: message.sessionId,
|
|
304
|
+
sessionMetadata: message.sessionMetadata,
|
|
215
305
|
scenarioId: message.scenarioId,
|
|
216
306
|
status: message.status,
|
|
217
307
|
type: "session"
|
|
@@ -226,30 +316,276 @@ var serverMessageToAction = (message) => {
|
|
|
226
316
|
}
|
|
227
317
|
};
|
|
228
318
|
|
|
319
|
+
// node_modules/@absolutejs/media/dist/index.js
|
|
320
|
+
var TAU = Math.PI * 2;
|
|
321
|
+
var pushIssue = (issues, severity, code, message) => {
|
|
322
|
+
issues.push({ code, message, severity });
|
|
323
|
+
};
|
|
324
|
+
var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
|
|
325
|
+
var max = (values) => values.length === 0 ? undefined : Math.max(...values);
|
|
326
|
+
var numericStat = (stat, key) => {
|
|
327
|
+
const value = stat[key];
|
|
328
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
329
|
+
};
|
|
330
|
+
var booleanStat = (stat, key) => {
|
|
331
|
+
const value = stat[key];
|
|
332
|
+
return typeof value === "boolean" ? value : undefined;
|
|
333
|
+
};
|
|
334
|
+
var stringStat = (stat, key) => {
|
|
335
|
+
const value = stat[key];
|
|
336
|
+
return typeof value === "string" ? value : undefined;
|
|
337
|
+
};
|
|
338
|
+
var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
|
|
339
|
+
var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
340
|
+
var normalizeWebRTCStat = (stat) => {
|
|
341
|
+
const sample = {};
|
|
342
|
+
for (const [key, value] of Object.entries(stat)) {
|
|
343
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
344
|
+
sample[key] = value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return sample;
|
|
348
|
+
};
|
|
349
|
+
var buildMediaWebRTCStatsReport = (input = {}) => {
|
|
350
|
+
const stats = input.stats ?? [];
|
|
351
|
+
const issues = [];
|
|
352
|
+
const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
353
|
+
const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
354
|
+
const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
|
|
355
|
+
const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
|
|
356
|
+
const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
|
|
357
|
+
const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
|
|
358
|
+
const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
|
|
359
|
+
const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
|
|
360
|
+
const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
|
|
361
|
+
const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
|
|
362
|
+
const packetLossDenominator = inboundPackets + packetsLost;
|
|
363
|
+
const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
|
|
364
|
+
const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
|
|
365
|
+
const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
|
|
366
|
+
const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
|
|
367
|
+
const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
|
|
368
|
+
const jitterBufferDelayMs = max(inbound.map((stat) => {
|
|
369
|
+
const delay = numericStat(stat, "jitterBufferDelay");
|
|
370
|
+
const emitted = numericStat(stat, "jitterBufferEmittedCount");
|
|
371
|
+
return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
|
|
372
|
+
}).filter((value) => value !== undefined));
|
|
373
|
+
const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
|
|
374
|
+
if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
|
|
375
|
+
pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
|
|
376
|
+
}
|
|
377
|
+
if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
|
|
378
|
+
pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
|
|
379
|
+
}
|
|
380
|
+
if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
|
|
381
|
+
pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
|
|
382
|
+
}
|
|
383
|
+
if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
|
|
384
|
+
pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
|
|
385
|
+
}
|
|
386
|
+
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
387
|
+
pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
activeCandidatePairs,
|
|
391
|
+
audioLevelAverage: average(audioLevels),
|
|
392
|
+
bytesReceived,
|
|
393
|
+
bytesSent,
|
|
394
|
+
checkedAt: Date.now(),
|
|
395
|
+
endedAudioTracks,
|
|
396
|
+
inboundPackets,
|
|
397
|
+
issues,
|
|
398
|
+
jitterBufferDelayMs,
|
|
399
|
+
jitterMs,
|
|
400
|
+
liveAudioTracks,
|
|
401
|
+
outboundPackets,
|
|
402
|
+
packetLossRatio,
|
|
403
|
+
packetsLost,
|
|
404
|
+
roundTripTimeMs,
|
|
405
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
406
|
+
totalStats: stats.length
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
var collectMediaWebRTCStats = async (input) => {
|
|
410
|
+
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
411
|
+
return [...report.values()].map(normalizeWebRTCStat);
|
|
412
|
+
};
|
|
413
|
+
var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
|
|
414
|
+
const stats = input.stats ?? [];
|
|
415
|
+
const previousStats = input.previousStats ?? [];
|
|
416
|
+
const issues = [];
|
|
417
|
+
const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
|
|
418
|
+
const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
|
|
419
|
+
const streams = audioRtp.map((stat) => {
|
|
420
|
+
const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
|
|
421
|
+
const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
|
|
422
|
+
const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
|
|
423
|
+
const previous = previousByKey.get(statKey(stat));
|
|
424
|
+
const currentPackets = numericStat(stat, packetsKey);
|
|
425
|
+
const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
|
|
426
|
+
const currentBytes = numericStat(stat, bytesKey);
|
|
427
|
+
const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
|
|
428
|
+
const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
|
|
429
|
+
return {
|
|
430
|
+
bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
|
|
431
|
+
currentPackets,
|
|
432
|
+
direction,
|
|
433
|
+
id: statKey(stat),
|
|
434
|
+
packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
|
|
435
|
+
previousPackets,
|
|
436
|
+
timeDeltaMs
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
const inbound = streams.filter((stream) => stream.direction === "inbound");
|
|
440
|
+
const outbound = streams.filter((stream) => stream.direction === "outbound");
|
|
441
|
+
const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
|
|
442
|
+
const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
443
|
+
const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
444
|
+
if (input.requireInboundAudio && inbound.length === 0) {
|
|
445
|
+
pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
|
|
446
|
+
}
|
|
447
|
+
if (input.requireOutboundAudio && outbound.length === 0) {
|
|
448
|
+
pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
|
|
449
|
+
}
|
|
450
|
+
if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
|
|
451
|
+
pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
|
|
452
|
+
}
|
|
453
|
+
if (stalledInboundStreams > 0) {
|
|
454
|
+
pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
|
|
455
|
+
}
|
|
456
|
+
if (stalledOutboundStreams > 0) {
|
|
457
|
+
pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
checkedAt: Date.now(),
|
|
461
|
+
inboundAudioStreams: inbound.length,
|
|
462
|
+
issues,
|
|
463
|
+
maxObservedGapMs,
|
|
464
|
+
outboundAudioStreams: outbound.length,
|
|
465
|
+
stalledInboundStreams,
|
|
466
|
+
stalledOutboundStreams,
|
|
467
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
468
|
+
streams,
|
|
469
|
+
totalStats: stats.length
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// src/client/browserMedia.ts
|
|
474
|
+
var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
|
|
475
|
+
var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
|
|
476
|
+
var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
|
|
477
|
+
var postBrowserMediaReport = async (payload, options) => {
|
|
478
|
+
const requestFetch = options.fetch ?? globalThis.fetch;
|
|
479
|
+
if (!requestFetch) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
|
|
483
|
+
body: JSON.stringify(payload),
|
|
484
|
+
headers: {
|
|
485
|
+
"Content-Type": "application/json"
|
|
486
|
+
},
|
|
487
|
+
keepalive: true,
|
|
488
|
+
method: "POST"
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
var createVoiceBrowserMediaReporter = (options) => {
|
|
492
|
+
let interval = null;
|
|
493
|
+
let previousStats = [];
|
|
494
|
+
const reportOnce = async () => {
|
|
495
|
+
const peerConnection = await resolvePeerConnection(options);
|
|
496
|
+
if (!peerConnection) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const stats = await collectMediaWebRTCStats({ peerConnection });
|
|
500
|
+
const report = buildMediaWebRTCStatsReport({
|
|
501
|
+
...options,
|
|
502
|
+
stats
|
|
503
|
+
});
|
|
504
|
+
const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
|
|
505
|
+
...options.continuity,
|
|
506
|
+
previousStats,
|
|
507
|
+
stats
|
|
508
|
+
});
|
|
509
|
+
const payload = {
|
|
510
|
+
at: Date.now(),
|
|
511
|
+
continuity,
|
|
512
|
+
report,
|
|
513
|
+
scenarioId: options.getScenarioId?.() ?? null,
|
|
514
|
+
sessionId: options.getSessionId?.() ?? null
|
|
515
|
+
};
|
|
516
|
+
previousStats = stats;
|
|
517
|
+
options.onReport?.(payload);
|
|
518
|
+
await postBrowserMediaReport(payload, options);
|
|
519
|
+
return payload;
|
|
520
|
+
};
|
|
521
|
+
const run = () => {
|
|
522
|
+
reportOnce().catch((error) => {
|
|
523
|
+
options.onError?.(error);
|
|
524
|
+
});
|
|
525
|
+
};
|
|
526
|
+
const stop = () => {
|
|
527
|
+
if (interval) {
|
|
528
|
+
clearInterval(interval);
|
|
529
|
+
interval = null;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
close: stop,
|
|
534
|
+
reportOnce,
|
|
535
|
+
stop,
|
|
536
|
+
start: () => {
|
|
537
|
+
if (interval) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
run();
|
|
541
|
+
interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
|
|
229
546
|
// src/client/connection.ts
|
|
230
547
|
var WS_OPEN = 1;
|
|
231
548
|
var WS_CLOSED = 3;
|
|
232
549
|
var WS_NORMAL_CLOSURE = 1000;
|
|
233
|
-
var
|
|
550
|
+
var WS_UNAUTHORIZED = 4401;
|
|
551
|
+
var WS_FORBIDDEN = 4403;
|
|
552
|
+
var WS_INITIALIZATION_FAILED = 4500;
|
|
553
|
+
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
234
554
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
235
|
-
var
|
|
555
|
+
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
556
|
+
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
557
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
558
|
+
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
559
|
+
var RECONNECT_BASE_DELAY_MS = 500;
|
|
560
|
+
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
561
|
+
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
236
562
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
563
|
+
|
|
564
|
+
class VoiceReconnectRejectedError extends Error {
|
|
565
|
+
constructor(message = "Voice authorization could not be renewed.") {
|
|
566
|
+
super(message);
|
|
567
|
+
this.name = "VoiceReconnectRejectedError";
|
|
568
|
+
}
|
|
569
|
+
}
|
|
237
570
|
var noop = () => {};
|
|
571
|
+
var noopAsync = () => Promise.resolve();
|
|
238
572
|
var noopUnsubscribe = () => noop;
|
|
239
573
|
var NOOP_CONNECTION = {
|
|
240
|
-
callControl:
|
|
574
|
+
callControl: noopAsync,
|
|
241
575
|
close: noop,
|
|
576
|
+
disconnect: noop,
|
|
242
577
|
endTurn: noop,
|
|
578
|
+
send: noop,
|
|
579
|
+
sendAudio: noop,
|
|
580
|
+
simulateDisconnect: noop,
|
|
581
|
+
subscribe: noopUnsubscribe,
|
|
243
582
|
getReadyState: () => WS_CLOSED,
|
|
244
583
|
getScenarioId: () => "",
|
|
245
584
|
getSessionId: () => "",
|
|
246
|
-
|
|
247
|
-
sendAudio: noop,
|
|
248
|
-
start: () => {},
|
|
249
|
-
subscribe: noopUnsubscribe
|
|
585
|
+
start: () => {}
|
|
250
586
|
};
|
|
251
587
|
var createSessionId = () => crypto.randomUUID();
|
|
252
|
-
var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
588
|
+
var buildWsUrl = (path, sessionId, scenarioId, query) => {
|
|
253
589
|
const { hostname, port, protocol } = window.location;
|
|
254
590
|
const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
|
|
255
591
|
const portSuffix = port ? `:${port}` : "";
|
|
@@ -258,6 +594,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
|
258
594
|
if (scenarioId) {
|
|
259
595
|
url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
|
|
260
596
|
}
|
|
597
|
+
for (const [key, value] of Object.entries(query)) {
|
|
598
|
+
if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
|
|
599
|
+
url.searchParams.set(key, value);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
261
602
|
return url.toString();
|
|
262
603
|
};
|
|
263
604
|
var isVoiceServerMessage = (value) => {
|
|
@@ -268,11 +609,15 @@ var isVoiceServerMessage = (value) => {
|
|
|
268
609
|
case "audio":
|
|
269
610
|
case "assistant":
|
|
270
611
|
case "call_lifecycle":
|
|
612
|
+
case "call_control_ack":
|
|
271
613
|
case "complete":
|
|
614
|
+
case "connection":
|
|
272
615
|
case "error":
|
|
273
616
|
case "final":
|
|
274
617
|
case "partial":
|
|
618
|
+
case "playback_rate":
|
|
275
619
|
case "pong":
|
|
620
|
+
case "replay":
|
|
276
621
|
case "session":
|
|
277
622
|
case "turn":
|
|
278
623
|
return true;
|
|
@@ -296,19 +641,45 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
296
641
|
return NOOP_CONNECTION;
|
|
297
642
|
}
|
|
298
643
|
const listeners = new Set;
|
|
644
|
+
const pendingCallControls = new Map;
|
|
299
645
|
const shouldReconnect = options.reconnect !== false;
|
|
300
646
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
647
|
+
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
301
648
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
649
|
+
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
650
|
+
let lifecycleGeneration = 0;
|
|
651
|
+
let disposed = false;
|
|
652
|
+
let reconnectPreparationAbort = null;
|
|
653
|
+
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
302
654
|
const state = {
|
|
303
655
|
isConnected: false,
|
|
304
656
|
pendingMessages: [],
|
|
305
657
|
scenarioId: options.scenarioId ?? null,
|
|
306
658
|
pingInterval: null,
|
|
307
659
|
reconnectAttempts: 0,
|
|
660
|
+
reconnectResetTimeout: null,
|
|
308
661
|
reconnectTimeout: null,
|
|
309
662
|
sessionId: options.sessionId ?? createSessionId(),
|
|
310
663
|
ws: null
|
|
311
664
|
};
|
|
665
|
+
const emitConnection = (reconnect) => {
|
|
666
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
667
|
+
};
|
|
668
|
+
const emitTerminalFailure = (message) => {
|
|
669
|
+
listeners.forEach((listener) => listener({
|
|
670
|
+
message,
|
|
671
|
+
recoverable: false,
|
|
672
|
+
type: "error"
|
|
673
|
+
}));
|
|
674
|
+
emitConnection({
|
|
675
|
+
reconnect: {
|
|
676
|
+
attempts: state.reconnectAttempts,
|
|
677
|
+
maxAttempts: maxReconnectAttempts,
|
|
678
|
+
status: "exhausted"
|
|
679
|
+
},
|
|
680
|
+
type: "connection"
|
|
681
|
+
});
|
|
682
|
+
};
|
|
312
683
|
const clearTimers = () => {
|
|
313
684
|
if (state.pingInterval) {
|
|
314
685
|
clearInterval(state.pingInterval);
|
|
@@ -318,6 +689,10 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
318
689
|
clearTimeout(state.reconnectTimeout);
|
|
319
690
|
state.reconnectTimeout = null;
|
|
320
691
|
}
|
|
692
|
+
if (state.reconnectResetTimeout) {
|
|
693
|
+
clearTimeout(state.reconnectResetTimeout);
|
|
694
|
+
state.reconnectResetTimeout = null;
|
|
695
|
+
}
|
|
321
696
|
};
|
|
322
697
|
const flushPendingMessages = () => {
|
|
323
698
|
if (state.ws?.readyState !== WS_OPEN) {
|
|
@@ -330,28 +705,111 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
330
705
|
}
|
|
331
706
|
}
|
|
332
707
|
};
|
|
708
|
+
const rejectPendingCallControls = (message) => {
|
|
709
|
+
for (const pending of pendingCallControls.values()) {
|
|
710
|
+
clearTimeout(pending.timeout);
|
|
711
|
+
pending.reject(new Error(message));
|
|
712
|
+
}
|
|
713
|
+
pendingCallControls.clear();
|
|
714
|
+
};
|
|
333
715
|
const scheduleReconnect = () => {
|
|
716
|
+
if (disposed)
|
|
717
|
+
return;
|
|
718
|
+
const scheduledGeneration = lifecycleGeneration;
|
|
334
719
|
state.reconnectAttempts += 1;
|
|
335
|
-
|
|
720
|
+
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
721
|
+
const nextAttemptAt = Date.now() + delayMs;
|
|
722
|
+
emitConnection({
|
|
723
|
+
reconnect: {
|
|
724
|
+
attempts: state.reconnectAttempts,
|
|
725
|
+
lastDisconnectAt: Date.now(),
|
|
726
|
+
maxAttempts: maxReconnectAttempts,
|
|
727
|
+
nextAttemptAt,
|
|
728
|
+
status: "reconnecting"
|
|
729
|
+
},
|
|
730
|
+
type: "connection"
|
|
731
|
+
});
|
|
732
|
+
state.reconnectTimeout = setTimeout(async () => {
|
|
733
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
734
|
+
return;
|
|
336
735
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
736
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
if (options.prepareReconnect) {
|
|
741
|
+
const preparationAbort = new AbortController;
|
|
742
|
+
reconnectPreparationAbort = preparationAbort;
|
|
743
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
744
|
+
try {
|
|
745
|
+
await Promise.race([
|
|
746
|
+
options.prepareReconnect({
|
|
747
|
+
attempt: state.reconnectAttempts,
|
|
748
|
+
path,
|
|
749
|
+
scenarioId: state.scenarioId,
|
|
750
|
+
sessionId: state.sessionId,
|
|
751
|
+
signal: preparationAbort.signal
|
|
752
|
+
}),
|
|
753
|
+
new Promise((_, reject) => {
|
|
754
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
755
|
+
})
|
|
756
|
+
]);
|
|
757
|
+
} finally {
|
|
758
|
+
clearTimeout(timeout);
|
|
759
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
760
|
+
reconnectPreparationAbort = null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
} catch (error) {
|
|
765
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
766
|
+
return;
|
|
767
|
+
if (error instanceof VoiceReconnectRejectedError) {
|
|
768
|
+
emitTerminalFailure(error.message);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
772
|
+
scheduleReconnect();
|
|
773
|
+
} else {
|
|
774
|
+
emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
|
|
775
|
+
}
|
|
337
776
|
return;
|
|
338
777
|
}
|
|
778
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
779
|
+
return;
|
|
339
780
|
connect();
|
|
340
|
-
},
|
|
781
|
+
}, delayMs);
|
|
341
782
|
};
|
|
342
783
|
const connect = () => {
|
|
343
|
-
|
|
784
|
+
if (disposed)
|
|
785
|
+
return;
|
|
786
|
+
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
344
787
|
ws.binaryType = "arraybuffer";
|
|
345
788
|
ws.onopen = () => {
|
|
789
|
+
if (state.ws !== ws) {
|
|
790
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
346
794
|
state.isConnected = true;
|
|
347
|
-
state.reconnectAttempts = 0;
|
|
348
795
|
flushPendingMessages();
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
796
|
+
if (wasReconnecting) {
|
|
797
|
+
emitConnection({
|
|
798
|
+
reconnect: {
|
|
799
|
+
attempts: state.reconnectAttempts,
|
|
800
|
+
lastResumedAt: Date.now(),
|
|
801
|
+
maxAttempts: maxReconnectAttempts,
|
|
802
|
+
status: "resumed"
|
|
803
|
+
},
|
|
804
|
+
type: "connection"
|
|
805
|
+
});
|
|
806
|
+
state.reconnectResetTimeout = setTimeout(() => {
|
|
807
|
+
if (state.ws === ws && ws.readyState === WS_OPEN) {
|
|
808
|
+
state.reconnectAttempts = 0;
|
|
809
|
+
}
|
|
810
|
+
state.reconnectResetTimeout = null;
|
|
811
|
+
}, reconnectResetAfterMs);
|
|
812
|
+
}
|
|
355
813
|
state.pingInterval = setInterval(() => {
|
|
356
814
|
if (ws.readyState === WS_OPEN) {
|
|
357
815
|
ws.send(JSON.stringify({ type: "ping" }));
|
|
@@ -359,6 +817,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
359
817
|
}, pingInterval);
|
|
360
818
|
};
|
|
361
819
|
ws.onmessage = (event) => {
|
|
820
|
+
if (state.ws !== ws)
|
|
821
|
+
return;
|
|
362
822
|
const parsed = parseServerMessage(event);
|
|
363
823
|
if (!parsed) {
|
|
364
824
|
return;
|
|
@@ -367,29 +827,86 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
367
827
|
state.sessionId = parsed.sessionId;
|
|
368
828
|
state.scenarioId = parsed.scenarioId ?? state.scenarioId;
|
|
369
829
|
}
|
|
830
|
+
if (parsed.type === "call_control_ack") {
|
|
831
|
+
const pending = pendingCallControls.get(parsed.requestId);
|
|
832
|
+
if (pending) {
|
|
833
|
+
clearTimeout(pending.timeout);
|
|
834
|
+
pendingCallControls.delete(parsed.requestId);
|
|
835
|
+
if (parsed.ok) {
|
|
836
|
+
pending.resolve();
|
|
837
|
+
} else {
|
|
838
|
+
pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
370
842
|
listeners.forEach((listener) => listener(parsed));
|
|
371
843
|
};
|
|
372
844
|
ws.onclose = (event) => {
|
|
845
|
+
if (state.ws !== ws)
|
|
846
|
+
return;
|
|
847
|
+
state.ws = null;
|
|
373
848
|
state.isConnected = false;
|
|
374
849
|
clearTimers();
|
|
375
|
-
|
|
850
|
+
rejectPendingCallControls("Voice connection interrupted");
|
|
851
|
+
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && event.code !== WS_INITIALIZATION_FAILED && state.reconnectAttempts < maxReconnectAttempts;
|
|
852
|
+
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
853
|
+
const initializationFailure = event.code === WS_INITIALIZATION_FAILED;
|
|
854
|
+
if (authorizationFailure) {
|
|
855
|
+
listeners.forEach((listener) => listener({
|
|
856
|
+
message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
|
|
857
|
+
recoverable: false,
|
|
858
|
+
type: "error"
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
861
|
+
if (initializationFailure) {
|
|
862
|
+
listeners.forEach((listener) => listener({
|
|
863
|
+
message: "Voice provider initialization failed.",
|
|
864
|
+
recoverable: false,
|
|
865
|
+
type: "error"
|
|
866
|
+
}));
|
|
867
|
+
}
|
|
376
868
|
if (reconnectable) {
|
|
377
869
|
scheduleReconnect();
|
|
870
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
871
|
+
if (authorizationFailure) {
|
|
872
|
+
emitConnection({
|
|
873
|
+
reconnect: {
|
|
874
|
+
attempts: state.reconnectAttempts,
|
|
875
|
+
lastDisconnectAt: Date.now(),
|
|
876
|
+
maxAttempts: maxReconnectAttempts,
|
|
877
|
+
status: "exhausted"
|
|
878
|
+
},
|
|
879
|
+
type: "connection"
|
|
880
|
+
});
|
|
881
|
+
} else if (!initializationFailure) {
|
|
882
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
883
|
+
}
|
|
378
884
|
}
|
|
379
885
|
};
|
|
380
886
|
state.ws = ws;
|
|
381
887
|
};
|
|
382
888
|
const sendSerialized = (value) => {
|
|
889
|
+
if (disposed)
|
|
890
|
+
return;
|
|
383
891
|
if (state.ws?.readyState === WS_OPEN) {
|
|
384
892
|
state.ws.send(value);
|
|
385
893
|
return;
|
|
386
894
|
}
|
|
895
|
+
if (typeof value !== "string") {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
|
|
899
|
+
state.pendingMessages.shift();
|
|
900
|
+
}
|
|
387
901
|
state.pendingMessages.push(value);
|
|
388
902
|
};
|
|
389
903
|
const send = (message) => {
|
|
390
904
|
sendSerialized(JSON.stringify(message));
|
|
391
905
|
};
|
|
392
906
|
const start = (input = {}) => {
|
|
907
|
+
if (input.sessionId && input.sessionId !== state.sessionId) {
|
|
908
|
+
throw new Error("Voice session switching requires a new connection instance.");
|
|
909
|
+
}
|
|
393
910
|
if (input.sessionId) {
|
|
394
911
|
state.sessionId = input.sessionId;
|
|
395
912
|
}
|
|
@@ -397,9 +914,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
397
914
|
state.scenarioId = input.scenarioId;
|
|
398
915
|
}
|
|
399
916
|
send({
|
|
400
|
-
|
|
917
|
+
scenarioId: state.scenarioId ?? undefined,
|
|
401
918
|
sessionId: state.sessionId,
|
|
402
|
-
|
|
919
|
+
type: "start"
|
|
403
920
|
});
|
|
404
921
|
};
|
|
405
922
|
const sendAudio = (audio) => {
|
|
@@ -409,20 +926,63 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
409
926
|
send({ type: "end_turn" });
|
|
410
927
|
};
|
|
411
928
|
const callControl = (message) => {
|
|
412
|
-
|
|
929
|
+
if (state.ws?.readyState !== WS_OPEN) {
|
|
930
|
+
return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
|
|
931
|
+
}
|
|
932
|
+
const requestId = crypto.randomUUID();
|
|
933
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
934
|
+
const timeout = setTimeout(() => {
|
|
935
|
+
pendingCallControls.delete(requestId);
|
|
936
|
+
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
937
|
+
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
938
|
+
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
939
|
+
state.ws.send(JSON.stringify({
|
|
413
940
|
...message,
|
|
941
|
+
requestId,
|
|
414
942
|
type: "call_control"
|
|
415
|
-
});
|
|
943
|
+
}));
|
|
944
|
+
return promise;
|
|
945
|
+
};
|
|
946
|
+
const close = (reason = "client-close") => {
|
|
947
|
+
disposed = true;
|
|
948
|
+
reconnectPreparationAbort?.abort();
|
|
949
|
+
reconnectPreparationAbort = null;
|
|
950
|
+
lifecycleGeneration += 1;
|
|
951
|
+
clearTimers();
|
|
952
|
+
rejectPendingCallControls("Voice connection closed");
|
|
953
|
+
state.pendingMessages.length = 0;
|
|
954
|
+
if (state.ws) {
|
|
955
|
+
const ws = state.ws;
|
|
956
|
+
state.ws = null;
|
|
957
|
+
if (ws.readyState === WS_OPEN) {
|
|
958
|
+
ws.send(JSON.stringify({ reason, type: "close" }));
|
|
959
|
+
}
|
|
960
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
961
|
+
}
|
|
962
|
+
state.isConnected = false;
|
|
963
|
+
listeners.clear();
|
|
416
964
|
};
|
|
417
|
-
const
|
|
965
|
+
const disconnect = () => {
|
|
966
|
+
disposed = true;
|
|
967
|
+
reconnectPreparationAbort?.abort();
|
|
968
|
+
reconnectPreparationAbort = null;
|
|
969
|
+
lifecycleGeneration += 1;
|
|
418
970
|
clearTimers();
|
|
971
|
+
rejectPendingCallControls("Voice connection disconnected");
|
|
972
|
+
state.pendingMessages.length = 0;
|
|
419
973
|
if (state.ws) {
|
|
420
|
-
state.ws
|
|
974
|
+
const ws = state.ws;
|
|
421
975
|
state.ws = null;
|
|
976
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
422
977
|
}
|
|
423
978
|
state.isConnected = false;
|
|
424
979
|
listeners.clear();
|
|
425
980
|
};
|
|
981
|
+
const simulateDisconnect = () => {
|
|
982
|
+
if (state.ws?.readyState === WS_OPEN) {
|
|
983
|
+
state.ws.close(4000, "absolutejs-voice-reconnect-proof");
|
|
984
|
+
}
|
|
985
|
+
};
|
|
426
986
|
const subscribe = (callback) => {
|
|
427
987
|
listeners.add(callback);
|
|
428
988
|
return () => {
|
|
@@ -433,32 +993,66 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
433
993
|
return {
|
|
434
994
|
callControl,
|
|
435
995
|
close,
|
|
996
|
+
disconnect,
|
|
436
997
|
endTurn,
|
|
437
|
-
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
438
|
-
getScenarioId: () => state.scenarioId ?? "",
|
|
439
|
-
getSessionId: () => state.sessionId,
|
|
440
998
|
send,
|
|
441
999
|
sendAudio,
|
|
1000
|
+
simulateDisconnect,
|
|
442
1001
|
start,
|
|
443
|
-
subscribe
|
|
1002
|
+
subscribe,
|
|
1003
|
+
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
1004
|
+
getScenarioId: () => state.scenarioId ?? "",
|
|
1005
|
+
getSessionId: () => state.sessionId
|
|
444
1006
|
};
|
|
445
1007
|
};
|
|
446
1008
|
|
|
447
1009
|
// src/client/store.ts
|
|
1010
|
+
var createInitialReconnectState = () => ({
|
|
1011
|
+
attempts: 0,
|
|
1012
|
+
maxAttempts: 0,
|
|
1013
|
+
status: "idle"
|
|
1014
|
+
});
|
|
1015
|
+
var appendSegmentText = (accumulated, next) => {
|
|
1016
|
+
const nextText = next.trim().replace(/\s+/g, " ");
|
|
1017
|
+
if (!nextText)
|
|
1018
|
+
return accumulated;
|
|
1019
|
+
if (!accumulated)
|
|
1020
|
+
return nextText;
|
|
1021
|
+
if (accumulated === nextText || accumulated.endsWith(nextText)) {
|
|
1022
|
+
return accumulated;
|
|
1023
|
+
}
|
|
1024
|
+
if (nextText.includes(accumulated))
|
|
1025
|
+
return nextText;
|
|
1026
|
+
return `${accumulated} ${nextText}`;
|
|
1027
|
+
};
|
|
1028
|
+
var joinPartial = (finalized, interim) => {
|
|
1029
|
+
const interimText = interim.trim().replace(/\s+/g, " ");
|
|
1030
|
+
if (!finalized)
|
|
1031
|
+
return interimText;
|
|
1032
|
+
if (!interimText || finalized.endsWith(interimText))
|
|
1033
|
+
return finalized;
|
|
1034
|
+
return `${finalized} ${interimText}`;
|
|
1035
|
+
};
|
|
448
1036
|
var createInitialState = () => ({
|
|
449
1037
|
assistantAudio: [],
|
|
1038
|
+
assistantStreamingText: "",
|
|
450
1039
|
assistantTexts: [],
|
|
451
1040
|
call: null,
|
|
452
1041
|
error: null,
|
|
453
1042
|
isConnected: false,
|
|
454
|
-
|
|
1043
|
+
paused: false,
|
|
455
1044
|
partial: "",
|
|
1045
|
+
playbackRate: null,
|
|
1046
|
+
reconnect: createInitialReconnectState(),
|
|
1047
|
+
scenarioId: null,
|
|
456
1048
|
sessionId: null,
|
|
1049
|
+
sessionMetadata: null,
|
|
457
1050
|
status: "idle",
|
|
458
1051
|
turns: []
|
|
459
1052
|
});
|
|
460
1053
|
var createVoiceStreamStore = () => {
|
|
461
1054
|
let state = createInitialState();
|
|
1055
|
+
let turnFinalText = "";
|
|
462
1056
|
const subscribers = new Set;
|
|
463
1057
|
const notify = () => {
|
|
464
1058
|
subscribers.forEach((subscriber) => subscriber());
|
|
@@ -482,9 +1076,16 @@ var createVoiceStreamStore = () => {
|
|
|
482
1076
|
case "assistant":
|
|
483
1077
|
state = {
|
|
484
1078
|
...state,
|
|
1079
|
+
assistantStreamingText: "",
|
|
485
1080
|
assistantTexts: [...state.assistantTexts, action.text]
|
|
486
1081
|
};
|
|
487
1082
|
break;
|
|
1083
|
+
case "assistant_delta":
|
|
1084
|
+
state = {
|
|
1085
|
+
...state,
|
|
1086
|
+
assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
|
|
1087
|
+
};
|
|
1088
|
+
break;
|
|
488
1089
|
case "complete":
|
|
489
1090
|
state = {
|
|
490
1091
|
...state,
|
|
@@ -509,7 +1110,19 @@ var createVoiceStreamStore = () => {
|
|
|
509
1110
|
case "connected":
|
|
510
1111
|
state = {
|
|
511
1112
|
...state,
|
|
512
|
-
isConnected: true
|
|
1113
|
+
isConnected: true,
|
|
1114
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
1115
|
+
...state.reconnect,
|
|
1116
|
+
lastResumedAt: Date.now(),
|
|
1117
|
+
nextAttemptAt: undefined,
|
|
1118
|
+
status: "resumed"
|
|
1119
|
+
} : state.reconnect
|
|
1120
|
+
};
|
|
1121
|
+
break;
|
|
1122
|
+
case "connection":
|
|
1123
|
+
state = {
|
|
1124
|
+
...state,
|
|
1125
|
+
reconnect: action.reconnect
|
|
513
1126
|
};
|
|
514
1127
|
break;
|
|
515
1128
|
case "disconnected":
|
|
@@ -521,33 +1134,67 @@ var createVoiceStreamStore = () => {
|
|
|
521
1134
|
case "error":
|
|
522
1135
|
state = {
|
|
523
1136
|
...state,
|
|
524
|
-
error: action.message
|
|
1137
|
+
error: action.message,
|
|
1138
|
+
...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
|
|
525
1139
|
};
|
|
526
1140
|
break;
|
|
527
1141
|
case "final":
|
|
1142
|
+
turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
|
|
528
1143
|
state = {
|
|
529
1144
|
...state,
|
|
530
|
-
partial:
|
|
531
|
-
turns: state.turns.map((turn) => turn)
|
|
1145
|
+
partial: turnFinalText
|
|
532
1146
|
};
|
|
533
1147
|
break;
|
|
534
1148
|
case "partial":
|
|
535
1149
|
state = {
|
|
536
1150
|
...state,
|
|
537
|
-
partial: action.transcript.text
|
|
1151
|
+
partial: joinPartial(turnFinalText, action.transcript.text)
|
|
1152
|
+
};
|
|
1153
|
+
break;
|
|
1154
|
+
case "playback_rate":
|
|
1155
|
+
state = {
|
|
1156
|
+
...state,
|
|
1157
|
+
playbackRate: Math.max(0.5, Math.min(2, action.rate))
|
|
1158
|
+
};
|
|
1159
|
+
break;
|
|
1160
|
+
case "replay":
|
|
1161
|
+
turnFinalText = action.partial;
|
|
1162
|
+
state = {
|
|
1163
|
+
...state,
|
|
1164
|
+
assistantStreamingText: "",
|
|
1165
|
+
assistantTexts: [...action.assistantTexts],
|
|
1166
|
+
call: action.call ?? null,
|
|
1167
|
+
error: null,
|
|
1168
|
+
isConnected: action.status === "active",
|
|
1169
|
+
partial: action.partial,
|
|
1170
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
1171
|
+
...state.reconnect,
|
|
1172
|
+
lastResumedAt: Date.now(),
|
|
1173
|
+
nextAttemptAt: undefined,
|
|
1174
|
+
status: "resumed"
|
|
1175
|
+
} : state.reconnect,
|
|
1176
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
1177
|
+
sessionId: action.sessionId,
|
|
1178
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
1179
|
+
status: action.status,
|
|
1180
|
+
turns: [...action.turns]
|
|
538
1181
|
};
|
|
539
1182
|
break;
|
|
540
1183
|
case "session":
|
|
541
1184
|
state = {
|
|
542
1185
|
...state,
|
|
543
1186
|
error: null,
|
|
1187
|
+
paused: action.paused ?? false,
|
|
1188
|
+
pauseExpiresAt: action.pauseExpiresAt,
|
|
544
1189
|
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
545
1190
|
isConnected: action.status === "active",
|
|
546
1191
|
sessionId: action.sessionId,
|
|
1192
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
547
1193
|
status: action.status
|
|
548
1194
|
};
|
|
549
1195
|
break;
|
|
550
1196
|
case "turn":
|
|
1197
|
+
turnFinalText = "";
|
|
551
1198
|
state = {
|
|
552
1199
|
...state,
|
|
553
1200
|
partial: "",
|
|
@@ -574,30 +1221,86 @@ var createVoiceStreamStore = () => {
|
|
|
574
1221
|
var createVoiceStream = (path, options = {}) => {
|
|
575
1222
|
const connection = createVoiceConnection(path, options);
|
|
576
1223
|
const store = createVoiceStreamStore();
|
|
1224
|
+
const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
|
|
1225
|
+
...options.browserMedia,
|
|
1226
|
+
getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
|
|
1227
|
+
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1228
|
+
}) : null;
|
|
577
1229
|
const subscribers = new Set;
|
|
1230
|
+
let disposed = false;
|
|
578
1231
|
const start = (input) => Promise.resolve().then(() => {
|
|
1232
|
+
if (disposed)
|
|
1233
|
+
return;
|
|
579
1234
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
580
1235
|
return;
|
|
581
1236
|
}
|
|
582
1237
|
connection.start(input);
|
|
1238
|
+
browserMediaReporter?.start();
|
|
583
1239
|
});
|
|
584
1240
|
const notify = () => {
|
|
585
1241
|
subscribers.forEach((subscriber) => subscriber());
|
|
586
1242
|
};
|
|
1243
|
+
const reportReconnect = () => {
|
|
1244
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const snapshot = store.getSnapshot();
|
|
1248
|
+
const body = JSON.stringify({
|
|
1249
|
+
at: Date.now(),
|
|
1250
|
+
reconnect: snapshot.reconnect,
|
|
1251
|
+
scenarioId: snapshot.scenarioId,
|
|
1252
|
+
sessionId: connection.getSessionId(),
|
|
1253
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
1254
|
+
});
|
|
1255
|
+
fetch(options.reconnectReportPath, {
|
|
1256
|
+
body,
|
|
1257
|
+
headers: {
|
|
1258
|
+
"Content-Type": "application/json"
|
|
1259
|
+
},
|
|
1260
|
+
keepalive: true,
|
|
1261
|
+
method: "POST"
|
|
1262
|
+
}).catch(() => {});
|
|
1263
|
+
};
|
|
587
1264
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
588
1265
|
const action = serverMessageToAction(message);
|
|
589
1266
|
if (action) {
|
|
590
1267
|
store.dispatch(action);
|
|
1268
|
+
if (message.type === "connection") {
|
|
1269
|
+
reportReconnect();
|
|
1270
|
+
}
|
|
591
1271
|
notify();
|
|
592
1272
|
}
|
|
593
1273
|
});
|
|
594
1274
|
return {
|
|
1275
|
+
start,
|
|
1276
|
+
get assistantAudio() {
|
|
1277
|
+
return store.getSnapshot().assistantAudio;
|
|
1278
|
+
},
|
|
1279
|
+
get assistantTexts() {
|
|
1280
|
+
return store.getSnapshot().assistantTexts;
|
|
1281
|
+
},
|
|
1282
|
+
get assistantStreamingText() {
|
|
1283
|
+
return store.getSnapshot().assistantStreamingText;
|
|
1284
|
+
},
|
|
1285
|
+
get call() {
|
|
1286
|
+
return store.getSnapshot().call;
|
|
1287
|
+
},
|
|
595
1288
|
callControl(message) {
|
|
596
|
-
connection.callControl(message);
|
|
1289
|
+
return connection.callControl(message);
|
|
1290
|
+
},
|
|
1291
|
+
close(reason) {
|
|
1292
|
+
disposed = true;
|
|
1293
|
+
unsubscribeConnection();
|
|
1294
|
+
browserMediaReporter?.close();
|
|
1295
|
+
connection.close(reason);
|
|
1296
|
+
store.dispatch({ type: "disconnected" });
|
|
1297
|
+
notify();
|
|
597
1298
|
},
|
|
598
|
-
|
|
1299
|
+
disconnect() {
|
|
1300
|
+
disposed = true;
|
|
599
1301
|
unsubscribeConnection();
|
|
600
|
-
|
|
1302
|
+
browserMediaReporter?.close();
|
|
1303
|
+
connection.disconnect();
|
|
601
1304
|
store.dispatch({ type: "disconnected" });
|
|
602
1305
|
notify();
|
|
603
1306
|
},
|
|
@@ -616,44 +1319,52 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
616
1319
|
get isConnected() {
|
|
617
1320
|
return store.getSnapshot().isConnected;
|
|
618
1321
|
},
|
|
619
|
-
get scenarioId() {
|
|
620
|
-
return store.getSnapshot().scenarioId;
|
|
621
|
-
},
|
|
622
|
-
start,
|
|
623
1322
|
get partial() {
|
|
624
1323
|
return store.getSnapshot().partial;
|
|
625
1324
|
},
|
|
626
|
-
get
|
|
627
|
-
return
|
|
628
|
-
},
|
|
629
|
-
get status() {
|
|
630
|
-
return store.getSnapshot().status;
|
|
1325
|
+
get playbackRate() {
|
|
1326
|
+
return store.getSnapshot().playbackRate;
|
|
631
1327
|
},
|
|
632
|
-
get
|
|
633
|
-
return store.getSnapshot().
|
|
1328
|
+
get paused() {
|
|
1329
|
+
return store.getSnapshot().paused;
|
|
634
1330
|
},
|
|
635
|
-
get
|
|
636
|
-
return store.getSnapshot().
|
|
1331
|
+
get pauseExpiresAt() {
|
|
1332
|
+
return store.getSnapshot().pauseExpiresAt;
|
|
637
1333
|
},
|
|
638
|
-
get
|
|
639
|
-
return store.getSnapshot().
|
|
1334
|
+
get reconnect() {
|
|
1335
|
+
return store.getSnapshot().reconnect;
|
|
640
1336
|
},
|
|
641
|
-
get
|
|
642
|
-
return store.getSnapshot().
|
|
1337
|
+
get scenarioId() {
|
|
1338
|
+
return store.getSnapshot().scenarioId;
|
|
643
1339
|
},
|
|
644
1340
|
sendAudio(audio) {
|
|
645
1341
|
connection.sendAudio(audio);
|
|
646
1342
|
},
|
|
1343
|
+
get sessionId() {
|
|
1344
|
+
return connection.getSessionId();
|
|
1345
|
+
},
|
|
1346
|
+
get sessionMetadata() {
|
|
1347
|
+
return store.getSnapshot().sessionMetadata;
|
|
1348
|
+
},
|
|
1349
|
+
simulateDisconnect() {
|
|
1350
|
+
connection.simulateDisconnect();
|
|
1351
|
+
},
|
|
1352
|
+
get status() {
|
|
1353
|
+
return store.getSnapshot().status;
|
|
1354
|
+
},
|
|
647
1355
|
subscribe(subscriber) {
|
|
648
1356
|
subscribers.add(subscriber);
|
|
649
1357
|
return () => {
|
|
650
1358
|
subscribers.delete(subscriber);
|
|
651
1359
|
};
|
|
1360
|
+
},
|
|
1361
|
+
get turns() {
|
|
1362
|
+
return store.getSnapshot().turns;
|
|
652
1363
|
}
|
|
653
1364
|
};
|
|
654
1365
|
};
|
|
655
1366
|
|
|
656
|
-
// src/audioConditioning.ts
|
|
1367
|
+
// src/core/audioConditioning.ts
|
|
657
1368
|
var DEFAULT_TARGET_LEVEL = 0.08;
|
|
658
1369
|
var DEFAULT_MAX_GAIN = 3;
|
|
659
1370
|
var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
|
|
@@ -671,34 +1382,37 @@ var resolveAudioConditioningConfig = (config) => {
|
|
|
671
1382
|
};
|
|
672
1383
|
};
|
|
673
1384
|
|
|
674
|
-
// src/turnProfiles.ts
|
|
1385
|
+
// src/core/turnProfiles.ts
|
|
675
1386
|
var TURN_PROFILE_DEFAULTS = {
|
|
676
1387
|
balanced: {
|
|
677
1388
|
qualityProfile: "general",
|
|
1389
|
+
minSilenceMs: 400,
|
|
678
1390
|
silenceMs: 1400,
|
|
679
1391
|
speechThreshold: 0.012,
|
|
680
1392
|
transcriptStabilityMs: 1000
|
|
681
1393
|
},
|
|
682
1394
|
fast: {
|
|
683
1395
|
qualityProfile: "general",
|
|
1396
|
+
minSilenceMs: 300,
|
|
684
1397
|
silenceMs: 700,
|
|
685
1398
|
speechThreshold: 0.015,
|
|
686
1399
|
transcriptStabilityMs: 450
|
|
687
1400
|
},
|
|
688
1401
|
"long-form": {
|
|
689
1402
|
qualityProfile: "general",
|
|
1403
|
+
minSilenceMs: 600,
|
|
690
1404
|
silenceMs: 2200,
|
|
691
1405
|
speechThreshold: 0.01,
|
|
692
1406
|
transcriptStabilityMs: 1500
|
|
693
1407
|
}
|
|
694
1408
|
};
|
|
695
1409
|
var QUALITY_PROFILE_DEFAULTS = {
|
|
696
|
-
general: {},
|
|
697
1410
|
"accent-heavy": {
|
|
698
1411
|
silenceMs: 1200,
|
|
699
1412
|
speechThreshold: 0.01,
|
|
700
1413
|
transcriptStabilityMs: 1200
|
|
701
1414
|
},
|
|
1415
|
+
general: {},
|
|
702
1416
|
"noisy-room": {
|
|
703
1417
|
silenceMs: 2000,
|
|
704
1418
|
speechThreshold: 0.02,
|
|
@@ -717,16 +1431,18 @@ var resolveTurnDetectionConfig = (config) => {
|
|
|
717
1431
|
const qualityProfile = config?.qualityProfile ?? DEFAULT_QUALITY_PROFILE;
|
|
718
1432
|
const preset = TURN_PROFILE_DEFAULTS[profile];
|
|
719
1433
|
const quality = QUALITY_PROFILE_DEFAULTS[qualityProfile];
|
|
1434
|
+
const silenceMs = config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs;
|
|
720
1435
|
return {
|
|
721
1436
|
profile,
|
|
722
1437
|
qualityProfile,
|
|
723
|
-
|
|
1438
|
+
minSilenceMs: Math.min(silenceMs, config?.minSilenceMs ?? quality.minSilenceMs ?? preset.minSilenceMs),
|
|
1439
|
+
silenceMs,
|
|
724
1440
|
speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
|
|
725
1441
|
transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
|
|
726
1442
|
};
|
|
727
1443
|
};
|
|
728
1444
|
|
|
729
|
-
// src/presets.ts
|
|
1445
|
+
// src/core/presets.ts
|
|
730
1446
|
var PRESET_INPUTS = {
|
|
731
1447
|
chat: {
|
|
732
1448
|
audioConditioning: {
|
|
@@ -747,8 +1463,8 @@ var PRESET_INPUTS = {
|
|
|
747
1463
|
},
|
|
748
1464
|
sttLifecycle: "continuous",
|
|
749
1465
|
turnDetection: {
|
|
750
|
-
|
|
751
|
-
|
|
1466
|
+
profile: "balanced",
|
|
1467
|
+
qualityProfile: "short-command"
|
|
752
1468
|
}
|
|
753
1469
|
},
|
|
754
1470
|
default: {
|
|
@@ -763,8 +1479,8 @@ var PRESET_INPUTS = {
|
|
|
763
1479
|
},
|
|
764
1480
|
sttLifecycle: "continuous",
|
|
765
1481
|
turnDetection: {
|
|
766
|
-
|
|
767
|
-
|
|
1482
|
+
profile: "fast",
|
|
1483
|
+
qualityProfile: "general"
|
|
768
1484
|
}
|
|
769
1485
|
},
|
|
770
1486
|
dictation: {
|
|
@@ -786,8 +1502,8 @@ var PRESET_INPUTS = {
|
|
|
786
1502
|
},
|
|
787
1503
|
sttLifecycle: "continuous",
|
|
788
1504
|
turnDetection: {
|
|
789
|
-
|
|
790
|
-
|
|
1505
|
+
profile: "long-form",
|
|
1506
|
+
qualityProfile: "accent-heavy"
|
|
791
1507
|
}
|
|
792
1508
|
},
|
|
793
1509
|
"guided-intake": {
|
|
@@ -809,8 +1525,8 @@ var PRESET_INPUTS = {
|
|
|
809
1525
|
},
|
|
810
1526
|
sttLifecycle: "turn-scoped",
|
|
811
1527
|
turnDetection: {
|
|
812
|
-
|
|
813
|
-
|
|
1528
|
+
profile: "long-form",
|
|
1529
|
+
qualityProfile: "accent-heavy"
|
|
814
1530
|
}
|
|
815
1531
|
},
|
|
816
1532
|
"noisy-room": {
|
|
@@ -832,8 +1548,8 @@ var PRESET_INPUTS = {
|
|
|
832
1548
|
},
|
|
833
1549
|
sttLifecycle: "continuous",
|
|
834
1550
|
turnDetection: {
|
|
835
|
-
qualityProfile: "noisy-room",
|
|
836
1551
|
profile: "long-form",
|
|
1552
|
+
qualityProfile: "noisy-room",
|
|
837
1553
|
silenceMs: 2100,
|
|
838
1554
|
speechThreshold: 0.02,
|
|
839
1555
|
transcriptStabilityMs: 1650
|
|
@@ -858,8 +1574,8 @@ var PRESET_INPUTS = {
|
|
|
858
1574
|
},
|
|
859
1575
|
sttLifecycle: "continuous",
|
|
860
1576
|
turnDetection: {
|
|
861
|
-
qualityProfile: "noisy-room",
|
|
862
1577
|
profile: "long-form",
|
|
1578
|
+
qualityProfile: "noisy-room",
|
|
863
1579
|
silenceMs: 660,
|
|
864
1580
|
speechThreshold: 0.012,
|
|
865
1581
|
transcriptStabilityMs: 300
|
|
@@ -884,8 +1600,8 @@ var PRESET_INPUTS = {
|
|
|
884
1600
|
},
|
|
885
1601
|
sttLifecycle: "continuous",
|
|
886
1602
|
turnDetection: {
|
|
887
|
-
qualityProfile: "noisy-room",
|
|
888
1603
|
profile: "long-form",
|
|
1604
|
+
qualityProfile: "noisy-room",
|
|
889
1605
|
silenceMs: 620,
|
|
890
1606
|
speechThreshold: 0.012,
|
|
891
1607
|
transcriptStabilityMs: 280
|
|
@@ -910,8 +1626,8 @@ var PRESET_INPUTS = {
|
|
|
910
1626
|
},
|
|
911
1627
|
sttLifecycle: "continuous",
|
|
912
1628
|
turnDetection: {
|
|
913
|
-
|
|
914
|
-
|
|
1629
|
+
profile: "long-form",
|
|
1630
|
+
qualityProfile: "noisy-room"
|
|
915
1631
|
}
|
|
916
1632
|
}
|
|
917
1633
|
};
|
|
@@ -935,14 +1651,20 @@ var resolveVoiceRuntimePreset = (name = "default") => {
|
|
|
935
1651
|
// src/client/controller.ts
|
|
936
1652
|
var createInitialState2 = (stream) => ({
|
|
937
1653
|
assistantAudio: [...stream.assistantAudio],
|
|
1654
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
938
1655
|
assistantTexts: [...stream.assistantTexts],
|
|
939
1656
|
call: stream.call,
|
|
940
1657
|
error: stream.error,
|
|
941
1658
|
isConnected: stream.isConnected,
|
|
942
1659
|
isRecording: false,
|
|
1660
|
+
paused: stream.paused,
|
|
1661
|
+
pauseExpiresAt: stream.pauseExpiresAt,
|
|
943
1662
|
partial: stream.partial,
|
|
1663
|
+
playbackRate: stream.playbackRate,
|
|
1664
|
+
reconnect: stream.reconnect,
|
|
944
1665
|
recordingError: null,
|
|
945
1666
|
sessionId: stream.sessionId,
|
|
1667
|
+
sessionMetadata: stream.sessionMetadata,
|
|
946
1668
|
scenarioId: stream.scenarioId,
|
|
947
1669
|
status: stream.status,
|
|
948
1670
|
turns: [...stream.turns]
|
|
@@ -954,6 +1676,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
954
1676
|
...options.connection
|
|
955
1677
|
});
|
|
956
1678
|
let capture = null;
|
|
1679
|
+
let captureGeneration = 0;
|
|
1680
|
+
let disposed = false;
|
|
1681
|
+
let startRecordingPromise = null;
|
|
957
1682
|
let state = createInitialState2(stream);
|
|
958
1683
|
const subscribers = new Set;
|
|
959
1684
|
const notify = () => {
|
|
@@ -965,12 +1690,18 @@ var createVoiceController = (path, options = {}) => {
|
|
|
965
1690
|
state = {
|
|
966
1691
|
...state,
|
|
967
1692
|
assistantAudio: [...stream.assistantAudio],
|
|
1693
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
968
1694
|
assistantTexts: [...stream.assistantTexts],
|
|
969
1695
|
call: stream.call,
|
|
970
1696
|
error: stream.error,
|
|
971
1697
|
isConnected: stream.isConnected,
|
|
1698
|
+
paused: stream.paused,
|
|
1699
|
+
pauseExpiresAt: stream.pauseExpiresAt,
|
|
972
1700
|
partial: stream.partial,
|
|
1701
|
+
playbackRate: stream.playbackRate,
|
|
1702
|
+
reconnect: stream.reconnect,
|
|
973
1703
|
sessionId: stream.sessionId,
|
|
1704
|
+
sessionMetadata: stream.sessionMetadata,
|
|
974
1705
|
scenarioId: stream.scenarioId,
|
|
975
1706
|
status: stream.status,
|
|
976
1707
|
turns: [...stream.turns]
|
|
@@ -994,12 +1725,20 @@ var createVoiceController = (path, options = {}) => {
|
|
|
994
1725
|
capture = createMicrophoneCapture({
|
|
995
1726
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
996
1727
|
onLevel: options.capture?.onLevel,
|
|
997
|
-
onAudio: (audio) =>
|
|
998
|
-
|
|
1728
|
+
onAudio: (audio) => {
|
|
1729
|
+
if (options.capture?.onAudio) {
|
|
1730
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
stream.sendAudio(audio);
|
|
1734
|
+
},
|
|
1735
|
+
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
|
|
1736
|
+
...options.capture?.stream ? { stream: options.capture.stream } : {}
|
|
999
1737
|
});
|
|
1000
1738
|
return capture;
|
|
1001
1739
|
};
|
|
1002
1740
|
const stopRecording = () => {
|
|
1741
|
+
captureGeneration += 1;
|
|
1003
1742
|
capture?.stop();
|
|
1004
1743
|
capture = null;
|
|
1005
1744
|
state = {
|
|
@@ -1009,43 +1748,88 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1009
1748
|
notify();
|
|
1010
1749
|
};
|
|
1011
1750
|
const startRecording = async () => {
|
|
1751
|
+
if (disposed) {
|
|
1752
|
+
throw new Error("Voice controller is closed.");
|
|
1753
|
+
}
|
|
1012
1754
|
if (state.isRecording) {
|
|
1013
1755
|
return;
|
|
1014
1756
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1757
|
+
if (startRecordingPromise)
|
|
1758
|
+
return startRecordingPromise;
|
|
1759
|
+
const generation = ++captureGeneration;
|
|
1760
|
+
const activeCapture = ensureCapture();
|
|
1761
|
+
const operation = (async () => {
|
|
1762
|
+
try {
|
|
1763
|
+
state = {
|
|
1764
|
+
...state,
|
|
1765
|
+
recordingError: null
|
|
1766
|
+
};
|
|
1767
|
+
notify();
|
|
1768
|
+
await activeCapture.start();
|
|
1769
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
1770
|
+
activeCapture.stop();
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
state = {
|
|
1774
|
+
...state,
|
|
1775
|
+
isRecording: true
|
|
1776
|
+
};
|
|
1777
|
+
notify();
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
activeCapture.stop();
|
|
1780
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
1781
|
+
throw error;
|
|
1782
|
+
}
|
|
1783
|
+
capture = null;
|
|
1784
|
+
state = {
|
|
1785
|
+
...state,
|
|
1786
|
+
isRecording: false,
|
|
1787
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
1788
|
+
};
|
|
1789
|
+
notify();
|
|
1790
|
+
throw error;
|
|
1791
|
+
}
|
|
1792
|
+
})();
|
|
1793
|
+
const pending = operation.finally(() => {
|
|
1794
|
+
if (startRecordingPromise === pending)
|
|
1795
|
+
startRecordingPromise = null;
|
|
1796
|
+
});
|
|
1797
|
+
startRecordingPromise = pending;
|
|
1798
|
+
return startRecordingPromise;
|
|
1799
|
+
};
|
|
1800
|
+
const close = (reason) => {
|
|
1801
|
+
disposed = true;
|
|
1802
|
+
unsubscribeStream();
|
|
1803
|
+
stopRecording();
|
|
1804
|
+
stream.close(reason);
|
|
1805
|
+
};
|
|
1806
|
+
const disconnect = () => {
|
|
1807
|
+
disposed = true;
|
|
1808
|
+
unsubscribeStream();
|
|
1809
|
+
stopRecording();
|
|
1810
|
+
stream.disconnect();
|
|
1811
|
+
};
|
|
1812
|
+
return {
|
|
1813
|
+
close,
|
|
1814
|
+
disconnect,
|
|
1815
|
+
startRecording,
|
|
1816
|
+
stopRecording,
|
|
1817
|
+
get assistantAudio() {
|
|
1818
|
+
return state.assistantAudio;
|
|
1819
|
+
},
|
|
1820
|
+
get assistantTexts() {
|
|
1821
|
+
return state.assistantTexts;
|
|
1822
|
+
},
|
|
1823
|
+
get assistantStreamingText() {
|
|
1824
|
+
return state.assistantStreamingText;
|
|
1825
|
+
},
|
|
1826
|
+
bindHTMX(bindingOptions) {
|
|
1827
|
+
return bindVoiceHTMX(stream, bindingOptions);
|
|
1828
|
+
},
|
|
1829
|
+
get call() {
|
|
1830
|
+
return state.call;
|
|
1831
|
+
},
|
|
1832
|
+
callControl: (message) => stream.callControl(message),
|
|
1049
1833
|
endTurn: () => stream.endTurn(),
|
|
1050
1834
|
get error() {
|
|
1051
1835
|
return state.error;
|
|
@@ -1061,21 +1845,35 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1061
1845
|
get partial() {
|
|
1062
1846
|
return state.partial;
|
|
1063
1847
|
},
|
|
1848
|
+
get playbackRate() {
|
|
1849
|
+
return state.playbackRate;
|
|
1850
|
+
},
|
|
1851
|
+
get paused() {
|
|
1852
|
+
return state.paused;
|
|
1853
|
+
},
|
|
1854
|
+
get pauseExpiresAt() {
|
|
1855
|
+
return state.pauseExpiresAt;
|
|
1856
|
+
},
|
|
1857
|
+
get reconnect() {
|
|
1858
|
+
return state.reconnect;
|
|
1859
|
+
},
|
|
1064
1860
|
get recordingError() {
|
|
1065
1861
|
return state.recordingError;
|
|
1066
1862
|
},
|
|
1863
|
+
get scenarioId() {
|
|
1864
|
+
return state.scenarioId;
|
|
1865
|
+
},
|
|
1067
1866
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1068
1867
|
get sessionId() {
|
|
1069
1868
|
return state.sessionId;
|
|
1070
1869
|
},
|
|
1071
|
-
get
|
|
1072
|
-
return state.
|
|
1870
|
+
get sessionMetadata() {
|
|
1871
|
+
return state.sessionMetadata;
|
|
1073
1872
|
},
|
|
1074
|
-
|
|
1873
|
+
simulateDisconnect: () => stream.simulateDisconnect(),
|
|
1075
1874
|
get status() {
|
|
1076
1875
|
return state.status;
|
|
1077
1876
|
},
|
|
1078
|
-
stopRecording,
|
|
1079
1877
|
subscribe: (subscriber) => {
|
|
1080
1878
|
subscribers.add(subscriber);
|
|
1081
1879
|
return () => {
|
|
@@ -1091,15 +1889,778 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1091
1889
|
},
|
|
1092
1890
|
get turns() {
|
|
1093
1891
|
return state.turns;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
// src/client/timeStretch.ts
|
|
1897
|
+
var HOP_MS = 10;
|
|
1898
|
+
var SEEK_MS = 5;
|
|
1899
|
+
var ENERGY_EPSILON = 0.000001;
|
|
1900
|
+
var HALF = 0.5;
|
|
1901
|
+
var MS_PER_SECOND = 1000;
|
|
1902
|
+
var makeHann = (length) => {
|
|
1903
|
+
const weights = new Float32Array(length);
|
|
1904
|
+
for (let index = 0;index < length; index += 1) {
|
|
1905
|
+
weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
|
|
1906
|
+
}
|
|
1907
|
+
return weights;
|
|
1908
|
+
};
|
|
1909
|
+
var correlationScore = (base, start, ref, length) => {
|
|
1910
|
+
let dot = 0;
|
|
1911
|
+
let energy = 0;
|
|
1912
|
+
for (let index = 0;index < length; index += 1) {
|
|
1913
|
+
const sample = base[start + index] ?? 0;
|
|
1914
|
+
dot += sample * (ref[index] ?? 0);
|
|
1915
|
+
energy += sample * sample;
|
|
1916
|
+
}
|
|
1917
|
+
return dot / Math.sqrt(energy + ENERGY_EPSILON);
|
|
1918
|
+
};
|
|
1919
|
+
var overlapAddGrain = (src, off, tail, weights, hop) => {
|
|
1920
|
+
const out = new Float32Array(hop);
|
|
1921
|
+
const nextTail = new Float32Array(hop);
|
|
1922
|
+
for (let index = 0;index < hop; index += 1) {
|
|
1923
|
+
out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
|
|
1924
|
+
nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
|
|
1925
|
+
}
|
|
1926
|
+
return { nextTail, out };
|
|
1927
|
+
};
|
|
1928
|
+
var createTimeStretcher = () => {
|
|
1929
|
+
let sampleRate = 0;
|
|
1930
|
+
let channelCount = 0;
|
|
1931
|
+
let hop = 0;
|
|
1932
|
+
let frameLen = 0;
|
|
1933
|
+
let seek = 0;
|
|
1934
|
+
let weights = new Float32Array(0);
|
|
1935
|
+
let buffers = [];
|
|
1936
|
+
let inputStart = 0;
|
|
1937
|
+
let analysisPos = 0;
|
|
1938
|
+
let olaTail = [];
|
|
1939
|
+
let naturalRef = null;
|
|
1940
|
+
const init = (rate, channels) => {
|
|
1941
|
+
sampleRate = rate;
|
|
1942
|
+
channelCount = channels;
|
|
1943
|
+
hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
|
|
1944
|
+
frameLen = hop * 2;
|
|
1945
|
+
seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
|
|
1946
|
+
weights = makeHann(frameLen);
|
|
1947
|
+
buffers = Array.from({ length: channels }, () => new Float32Array(0));
|
|
1948
|
+
olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
|
|
1949
|
+
inputStart = 0;
|
|
1950
|
+
analysisPos = seek;
|
|
1951
|
+
naturalRef = null;
|
|
1952
|
+
};
|
|
1953
|
+
const reset = () => {
|
|
1954
|
+
buffers = buffers.map(() => new Float32Array(0));
|
|
1955
|
+
olaTail = olaTail.map(() => new Float32Array(hop));
|
|
1956
|
+
inputStart = 0;
|
|
1957
|
+
analysisPos = seek;
|
|
1958
|
+
naturalRef = null;
|
|
1959
|
+
};
|
|
1960
|
+
const append = (input) => {
|
|
1961
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1962
|
+
const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
|
|
1963
|
+
const existing = buffers[channel] ?? new Float32Array(0);
|
|
1964
|
+
const merged = new Float32Array(existing.length + incoming.length);
|
|
1965
|
+
merged.set(existing, 0);
|
|
1966
|
+
merged.set(incoming, existing.length);
|
|
1967
|
+
buffers[channel] = merged;
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
|
|
1971
|
+
const compact = () => {
|
|
1972
|
+
const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
|
|
1973
|
+
if (keepFrom <= inputStart)
|
|
1974
|
+
return;
|
|
1975
|
+
const drop = keepFrom - inputStart;
|
|
1976
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1977
|
+
buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
|
|
1978
|
+
}
|
|
1979
|
+
inputStart = keepFrom;
|
|
1980
|
+
};
|
|
1981
|
+
const bestOffset = (center) => {
|
|
1982
|
+
if (!naturalRef)
|
|
1983
|
+
return 0;
|
|
1984
|
+
const [base] = buffers;
|
|
1985
|
+
if (!base)
|
|
1986
|
+
return 0;
|
|
1987
|
+
let bestDelta = 0;
|
|
1988
|
+
let bestScore = -Infinity;
|
|
1989
|
+
for (let delta = -seek;delta <= seek; delta += 1) {
|
|
1990
|
+
const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
|
|
1991
|
+
if (score <= bestScore)
|
|
1992
|
+
continue;
|
|
1993
|
+
bestScore = score;
|
|
1994
|
+
bestDelta = delta;
|
|
1995
|
+
}
|
|
1996
|
+
return bestDelta;
|
|
1997
|
+
};
|
|
1998
|
+
const process = (input, speed, rate) => {
|
|
1999
|
+
const channels = Math.max(1, input.length);
|
|
2000
|
+
if (sampleRate !== rate || channelCount !== channels)
|
|
2001
|
+
init(rate, channels);
|
|
2002
|
+
append(input);
|
|
2003
|
+
const analysisHop = hop * speed;
|
|
2004
|
+
const segments = Array.from({ length: channelCount }, () => []);
|
|
2005
|
+
const emitGrain = (pos) => {
|
|
2006
|
+
const off = pos - inputStart;
|
|
2007
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
2008
|
+
const src = buffers[channel];
|
|
2009
|
+
const tail = olaTail[channel];
|
|
2010
|
+
if (!src || !tail)
|
|
2011
|
+
continue;
|
|
2012
|
+
const grain = overlapAddGrain(src, off, tail, weights, hop);
|
|
2013
|
+
olaTail[channel] = grain.nextTail;
|
|
2014
|
+
segments[channel]?.push(grain.out);
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
const captureRef = (pos) => {
|
|
2018
|
+
const ref = new Float32Array(frameLen);
|
|
2019
|
+
const refOff = pos + hop - inputStart;
|
|
2020
|
+
const [base] = buffers;
|
|
2021
|
+
if (base)
|
|
2022
|
+
ref.set(base.subarray(refOff, refOff + frameLen));
|
|
2023
|
+
naturalRef = ref;
|
|
2024
|
+
};
|
|
2025
|
+
const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
|
|
2026
|
+
while (canEmit()) {
|
|
2027
|
+
const center = Math.round(analysisPos);
|
|
2028
|
+
const pos = center + bestOffset(center);
|
|
2029
|
+
emitGrain(pos);
|
|
2030
|
+
captureRef(pos);
|
|
2031
|
+
analysisPos += analysisHop;
|
|
2032
|
+
}
|
|
2033
|
+
compact();
|
|
2034
|
+
return segments.map((channelSegments) => {
|
|
2035
|
+
const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
|
|
2036
|
+
const merged = new Float32Array(total);
|
|
2037
|
+
let offset = 0;
|
|
2038
|
+
for (const seg of channelSegments) {
|
|
2039
|
+
merged.set(seg, offset);
|
|
2040
|
+
offset += seg.length;
|
|
2041
|
+
}
|
|
2042
|
+
return merged;
|
|
2043
|
+
});
|
|
2044
|
+
};
|
|
2045
|
+
return { process, reset };
|
|
2046
|
+
};
|
|
2047
|
+
|
|
2048
|
+
// src/client/audioPlayer.ts
|
|
2049
|
+
var activeAudioPlayers = new Set;
|
|
2050
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
2051
|
+
var DEFAULT_VOLUME = 1;
|
|
2052
|
+
var DEFAULT_PLAYBACK_RATE = 1;
|
|
2053
|
+
var MIN_PLAYBACK_RATE = 0.5;
|
|
2054
|
+
var MAX_PLAYBACK_RATE = 2;
|
|
2055
|
+
var STRETCH_BYPASS_EPSILON = 0.01;
|
|
2056
|
+
var ANALYSER_FFT_SIZE = 256;
|
|
2057
|
+
var PCM_BYTE_MIDPOINT = 128;
|
|
2058
|
+
var GAP_EPSILON_S = 0.03;
|
|
2059
|
+
var createInitialState3 = () => ({
|
|
2060
|
+
activeSourceCount: 0,
|
|
2061
|
+
error: null,
|
|
2062
|
+
isActive: false,
|
|
2063
|
+
isPlaying: false,
|
|
2064
|
+
lastInterruptLatencyMs: undefined,
|
|
2065
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
2066
|
+
processedChunkCount: 0,
|
|
2067
|
+
queuedChunkCount: 0
|
|
2068
|
+
});
|
|
2069
|
+
var getAudioContextCtor = () => {
|
|
2070
|
+
if (typeof window === "undefined") {
|
|
2071
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
2072
|
+
}
|
|
2073
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
2074
|
+
};
|
|
2075
|
+
var clampVolume = (volume) => {
|
|
2076
|
+
if (typeof volume !== "number" || !Number.isFinite(volume)) {
|
|
2077
|
+
return DEFAULT_VOLUME;
|
|
2078
|
+
}
|
|
2079
|
+
return Math.min(1, Math.max(0, volume));
|
|
2080
|
+
};
|
|
2081
|
+
var clampPlaybackRate = (rate) => {
|
|
2082
|
+
if (typeof rate !== "number" || !Number.isFinite(rate)) {
|
|
2083
|
+
return DEFAULT_PLAYBACK_RATE;
|
|
2084
|
+
}
|
|
2085
|
+
return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
|
|
2086
|
+
};
|
|
2087
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
2088
|
+
const { format } = chunk;
|
|
2089
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
2090
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
2091
|
+
}
|
|
2092
|
+
const bytes = chunk.chunk;
|
|
2093
|
+
const channels = Math.max(1, format.channels);
|
|
2094
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
2095
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
2096
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
2097
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2098
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2099
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
2100
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
2101
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
2102
|
+
const sampleOffset = sampleIndex * 2;
|
|
2103
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
2104
|
+
channelData[frameIndex] = 0;
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return audioBuffer;
|
|
2111
|
+
};
|
|
2112
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
2113
|
+
const subscribers = new Set;
|
|
2114
|
+
const sourceNodes = new Set;
|
|
2115
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
2116
|
+
let state = createInitialState3();
|
|
2117
|
+
let audioContext = null;
|
|
2118
|
+
let outputNode = null;
|
|
2119
|
+
let analyserNode = null;
|
|
2120
|
+
let analyserBuffer = null;
|
|
2121
|
+
let volume = clampVolume(options.volume);
|
|
2122
|
+
let playbackRate = clampPlaybackRate(options.playbackRate);
|
|
2123
|
+
let stretcher = null;
|
|
2124
|
+
let queueEndTime = 0;
|
|
2125
|
+
let syncPromise = Promise.resolve();
|
|
2126
|
+
let interruptStartedAt = null;
|
|
2127
|
+
let interruptPromise = null;
|
|
2128
|
+
let resolveInterruptPromise = null;
|
|
2129
|
+
let interruptFallbackTimer = null;
|
|
2130
|
+
let concurrencyCounted = false;
|
|
2131
|
+
let observedPeakConcurrency = 0;
|
|
2132
|
+
let scheduledDurationMs = 0;
|
|
2133
|
+
let gapCount = 0;
|
|
2134
|
+
let totalGapMs = 0;
|
|
2135
|
+
let maxGapMs = 0;
|
|
2136
|
+
let errorCount = 0;
|
|
2137
|
+
let hasScheduledAnyChunk = false;
|
|
2138
|
+
const sampleConcurrency = () => {
|
|
2139
|
+
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
2140
|
+
observedPeakConcurrency = activeAudioPlayers.size;
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
const markPlayerActive = () => {
|
|
2144
|
+
if (concurrencyCounted) {
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
concurrencyCounted = true;
|
|
2148
|
+
activeAudioPlayers.add(sampleConcurrency);
|
|
2149
|
+
for (const sample of activeAudioPlayers) {
|
|
2150
|
+
sample();
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
const markPlayerInactive = () => {
|
|
2154
|
+
if (!concurrencyCounted) {
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
concurrencyCounted = false;
|
|
2158
|
+
activeAudioPlayers.delete(sampleConcurrency);
|
|
2159
|
+
};
|
|
2160
|
+
const notify = () => {
|
|
2161
|
+
for (const subscriber of subscribers) {
|
|
2162
|
+
subscriber();
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
const setState = (next) => {
|
|
2166
|
+
state = {
|
|
2167
|
+
...state,
|
|
2168
|
+
...next
|
|
2169
|
+
};
|
|
2170
|
+
notify();
|
|
2171
|
+
};
|
|
2172
|
+
const clearError = () => {
|
|
2173
|
+
if (state.error !== null) {
|
|
2174
|
+
setState({ error: null });
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
const clearInterruptTimer = () => {
|
|
2178
|
+
if (interruptFallbackTimer !== null) {
|
|
2179
|
+
clearTimeout(interruptFallbackTimer);
|
|
2180
|
+
interruptFallbackTimer = null;
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
const resolveInterrupt = (latencyMs) => {
|
|
2184
|
+
clearInterruptTimer();
|
|
2185
|
+
interruptStartedAt = null;
|
|
2186
|
+
stretcher?.reset();
|
|
2187
|
+
setState({
|
|
2188
|
+
activeSourceCount: sourceNodes.size,
|
|
2189
|
+
isPlaying: false,
|
|
2190
|
+
lastInterruptLatencyMs: latencyMs,
|
|
2191
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
2192
|
+
});
|
|
2193
|
+
resolveInterruptPromise?.();
|
|
2194
|
+
resolveInterruptPromise = null;
|
|
2195
|
+
interruptPromise = null;
|
|
2196
|
+
};
|
|
2197
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
2198
|
+
if (!context) {
|
|
2199
|
+
return 0;
|
|
2200
|
+
}
|
|
2201
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
2202
|
+
};
|
|
2203
|
+
const applyOutputGain = (context) => {
|
|
2204
|
+
if (!outputNode) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const gainValue = volume;
|
|
2208
|
+
if (outputNode.gain.setValueAtTime) {
|
|
2209
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
outputNode.gain.value = gainValue;
|
|
2213
|
+
};
|
|
2214
|
+
const muteOutputGain = (context) => {
|
|
2215
|
+
if (!outputNode) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
const gainValue = 0;
|
|
2219
|
+
if (outputNode.gain.setValueAtTime) {
|
|
2220
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
outputNode.gain.value = gainValue;
|
|
2224
|
+
};
|
|
2225
|
+
const maybeResolveInterrupt = () => {
|
|
2226
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
2230
|
+
};
|
|
2231
|
+
const ensureAudioContext = async () => {
|
|
2232
|
+
if (audioContext) {
|
|
2233
|
+
return audioContext;
|
|
2234
|
+
}
|
|
2235
|
+
if (options.createAudioContext) {
|
|
2236
|
+
audioContext = options.createAudioContext();
|
|
2237
|
+
} else {
|
|
2238
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
2239
|
+
if (!AudioContextCtor) {
|
|
2240
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
2241
|
+
}
|
|
2242
|
+
audioContext = new AudioContextCtor;
|
|
2243
|
+
}
|
|
2244
|
+
if (audioContext.createGain) {
|
|
2245
|
+
outputNode = audioContext.createGain();
|
|
2246
|
+
outputNode.connect?.(audioContext.destination);
|
|
2247
|
+
if (audioContext.createAnalyser) {
|
|
2248
|
+
analyserNode = audioContext.createAnalyser();
|
|
2249
|
+
analyserNode.fftSize = ANALYSER_FFT_SIZE;
|
|
2250
|
+
analyserBuffer = new Uint8Array(analyserNode.fftSize);
|
|
2251
|
+
outputNode.connect?.(analyserNode);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
queueEndTime = audioContext.currentTime;
|
|
2255
|
+
return audioContext;
|
|
2256
|
+
};
|
|
2257
|
+
const scheduleBuffer = (context, buffer, rate) => {
|
|
2258
|
+
const node = context.createBufferSource();
|
|
2259
|
+
node.buffer = buffer;
|
|
2260
|
+
if (node.playbackRate) {
|
|
2261
|
+
node.playbackRate.value = rate;
|
|
2262
|
+
}
|
|
2263
|
+
node.connect(outputNode ?? context.destination);
|
|
2264
|
+
node.onended = () => {
|
|
2265
|
+
sourceNodes.delete(node);
|
|
2266
|
+
node.disconnect?.();
|
|
2267
|
+
setState({
|
|
2268
|
+
activeSourceCount: sourceNodes.size,
|
|
2269
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
2270
|
+
});
|
|
2271
|
+
maybeResolveInterrupt();
|
|
2272
|
+
};
|
|
2273
|
+
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
2274
|
+
const startAt = Math.max(earliestStart, queueEndTime);
|
|
2275
|
+
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
2276
|
+
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
2277
|
+
gapCount += 1;
|
|
2278
|
+
totalGapMs += gapMs;
|
|
2279
|
+
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
2280
|
+
}
|
|
2281
|
+
hasScheduledAnyChunk = true;
|
|
2282
|
+
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
2283
|
+
sampleConcurrency();
|
|
2284
|
+
queueEndTime = startAt + buffer.duration / rate;
|
|
2285
|
+
sourceNodes.add(node);
|
|
2286
|
+
setState({
|
|
2287
|
+
activeSourceCount: sourceNodes.size,
|
|
2288
|
+
isPlaying: true
|
|
2289
|
+
});
|
|
2290
|
+
node.start(startAt);
|
|
2291
|
+
};
|
|
2292
|
+
const scheduleChunk = async (chunk) => {
|
|
2293
|
+
const context = await ensureAudioContext();
|
|
2294
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
2295
|
+
if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
|
|
2296
|
+
stretcher?.reset();
|
|
2297
|
+
scheduleBuffer(context, buffer, playbackRate);
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
const channels = Math.max(1, chunk.format.channels);
|
|
2301
|
+
const input = [];
|
|
2302
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2303
|
+
input.push(buffer.getChannelData(channelIndex));
|
|
2304
|
+
}
|
|
2305
|
+
stretcher ??= createTimeStretcher();
|
|
2306
|
+
const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
|
|
2307
|
+
const outLength = stretched[0]?.length ?? 0;
|
|
2308
|
+
if (outLength === 0) {
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
|
|
2312
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2313
|
+
const channelOut = stretched[channelIndex];
|
|
2314
|
+
if (!channelOut)
|
|
2315
|
+
continue;
|
|
2316
|
+
outBuffer.getChannelData(channelIndex).set(channelOut);
|
|
2317
|
+
}
|
|
2318
|
+
scheduleBuffer(context, outBuffer, 1);
|
|
2319
|
+
};
|
|
2320
|
+
const stopQueuedPlayback = (options2) => {
|
|
2321
|
+
for (const node of [...sourceNodes]) {
|
|
2322
|
+
node.stop?.();
|
|
2323
|
+
}
|
|
2324
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
2325
|
+
if (options2?.forceClear) {
|
|
2326
|
+
for (const node of sourceNodes) {
|
|
2327
|
+
node.disconnect?.();
|
|
2328
|
+
}
|
|
2329
|
+
sourceNodes.clear();
|
|
2330
|
+
maybeResolveInterrupt();
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
const sync = async () => {
|
|
2334
|
+
if (!state.isActive) {
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
2338
|
+
if (nextChunks.length === 0) {
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
try {
|
|
2342
|
+
clearError();
|
|
2343
|
+
for (const chunk of nextChunks) {
|
|
2344
|
+
await scheduleChunk(chunk);
|
|
2345
|
+
}
|
|
2346
|
+
setState({
|
|
2347
|
+
processedChunkCount: source.assistantAudio.length,
|
|
2348
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
2349
|
+
});
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
errorCount += 1;
|
|
2352
|
+
setState({
|
|
2353
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
const queueSync = () => {
|
|
2358
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
2359
|
+
return syncPromise;
|
|
2360
|
+
};
|
|
2361
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
2362
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
2363
|
+
player.start();
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
if (state.isActive) {
|
|
2367
|
+
queueSync();
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
const player = {
|
|
2371
|
+
get activeSourceCount() {
|
|
2372
|
+
return state.activeSourceCount;
|
|
1094
2373
|
},
|
|
1095
|
-
|
|
1096
|
-
|
|
2374
|
+
close: async () => {
|
|
2375
|
+
markPlayerInactive();
|
|
2376
|
+
unsubscribeSource();
|
|
2377
|
+
stopQueuedPlayback({ forceClear: true });
|
|
2378
|
+
clearInterruptTimer();
|
|
2379
|
+
resolveInterruptPromise?.();
|
|
2380
|
+
resolveInterruptPromise = null;
|
|
2381
|
+
interruptPromise = null;
|
|
2382
|
+
interruptStartedAt = null;
|
|
2383
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
2384
|
+
await audioContext.close();
|
|
2385
|
+
}
|
|
2386
|
+
audioContext = null;
|
|
2387
|
+
outputNode?.disconnect?.();
|
|
2388
|
+
outputNode = null;
|
|
2389
|
+
analyserNode?.disconnect?.();
|
|
2390
|
+
analyserNode = null;
|
|
2391
|
+
analyserBuffer = null;
|
|
2392
|
+
queueEndTime = 0;
|
|
2393
|
+
setState({
|
|
2394
|
+
activeSourceCount: 0,
|
|
2395
|
+
isActive: false,
|
|
2396
|
+
isPlaying: false
|
|
2397
|
+
});
|
|
1097
2398
|
},
|
|
1098
|
-
get
|
|
1099
|
-
return state.
|
|
2399
|
+
get error() {
|
|
2400
|
+
return state.error;
|
|
1100
2401
|
},
|
|
1101
|
-
|
|
1102
|
-
|
|
2402
|
+
getOutputLevel: () => {
|
|
2403
|
+
if (!analyserNode || !analyserBuffer) {
|
|
2404
|
+
return 0;
|
|
2405
|
+
}
|
|
2406
|
+
analyserNode.getByteTimeDomainData(analyserBuffer);
|
|
2407
|
+
let sumSquares = 0;
|
|
2408
|
+
for (const sample of analyserBuffer) {
|
|
2409
|
+
const centered = (sample - PCM_BYTE_MIDPOINT) / PCM_BYTE_MIDPOINT;
|
|
2410
|
+
sumSquares += centered * centered;
|
|
2411
|
+
}
|
|
2412
|
+
return Math.sqrt(sumSquares / analyserBuffer.length);
|
|
2413
|
+
},
|
|
2414
|
+
getSnapshot: () => state,
|
|
2415
|
+
getIntegritySummary: () => {
|
|
2416
|
+
const chunksReceived = source.assistantAudio.length;
|
|
2417
|
+
const chunksScheduled = state.processedChunkCount;
|
|
2418
|
+
return {
|
|
2419
|
+
chunksReceived,
|
|
2420
|
+
chunksScheduled,
|
|
2421
|
+
errorCount,
|
|
2422
|
+
gapCount,
|
|
2423
|
+
maxConcurrentPlayers: observedPeakConcurrency,
|
|
2424
|
+
maxGapMs: Math.round(maxGapMs),
|
|
2425
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
|
|
2426
|
+
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
2427
|
+
totalGapMs: Math.round(totalGapMs)
|
|
2428
|
+
};
|
|
2429
|
+
},
|
|
2430
|
+
interrupt: async () => {
|
|
2431
|
+
const startedAt = Date.now();
|
|
2432
|
+
const context = await ensureAudioContext();
|
|
2433
|
+
interruptStartedAt = startedAt;
|
|
2434
|
+
muteOutputGain(context);
|
|
2435
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
2436
|
+
setState({
|
|
2437
|
+
isActive: false,
|
|
2438
|
+
isPlaying: sourceNodes.size > 0,
|
|
2439
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
2440
|
+
});
|
|
2441
|
+
if (sourceNodes.size === 0) {
|
|
2442
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (!interruptPromise) {
|
|
2446
|
+
interruptPromise = new Promise((resolve) => {
|
|
2447
|
+
resolveInterruptPromise = resolve;
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
clearInterruptTimer();
|
|
2451
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
2452
|
+
for (const node of sourceNodes) {
|
|
2453
|
+
node.disconnect?.();
|
|
2454
|
+
}
|
|
2455
|
+
sourceNodes.clear();
|
|
2456
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
2457
|
+
}, 250);
|
|
2458
|
+
stopQueuedPlayback();
|
|
2459
|
+
await interruptPromise;
|
|
2460
|
+
},
|
|
2461
|
+
get isActive() {
|
|
2462
|
+
return state.isActive;
|
|
2463
|
+
},
|
|
2464
|
+
get isPlaying() {
|
|
2465
|
+
return state.isPlaying;
|
|
2466
|
+
},
|
|
2467
|
+
get lastInterruptLatencyMs() {
|
|
2468
|
+
return state.lastInterruptLatencyMs;
|
|
2469
|
+
},
|
|
2470
|
+
get lastPlaybackStopLatencyMs() {
|
|
2471
|
+
return state.lastPlaybackStopLatencyMs;
|
|
2472
|
+
},
|
|
2473
|
+
pause: async () => {
|
|
2474
|
+
if (!audioContext) {
|
|
2475
|
+
setState({
|
|
2476
|
+
activeSourceCount: 0,
|
|
2477
|
+
isActive: false,
|
|
2478
|
+
isPlaying: false
|
|
2479
|
+
});
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
await audioContext.suspend();
|
|
2483
|
+
markPlayerInactive();
|
|
2484
|
+
setState({
|
|
2485
|
+
activeSourceCount: sourceNodes.size,
|
|
2486
|
+
isActive: false,
|
|
2487
|
+
isPlaying: false
|
|
2488
|
+
});
|
|
2489
|
+
},
|
|
2490
|
+
get playbackRate() {
|
|
2491
|
+
return playbackRate;
|
|
2492
|
+
},
|
|
2493
|
+
get processedChunkCount() {
|
|
2494
|
+
return state.processedChunkCount;
|
|
2495
|
+
},
|
|
2496
|
+
get queuedChunkCount() {
|
|
2497
|
+
return state.queuedChunkCount;
|
|
2498
|
+
},
|
|
2499
|
+
setPlaybackRate: (nextRate) => {
|
|
2500
|
+
playbackRate = clampPlaybackRate(nextRate);
|
|
2501
|
+
},
|
|
2502
|
+
setVolume: (nextVolume) => {
|
|
2503
|
+
volume = clampVolume(nextVolume);
|
|
2504
|
+
applyOutputGain(audioContext);
|
|
2505
|
+
},
|
|
2506
|
+
start: async () => {
|
|
2507
|
+
try {
|
|
2508
|
+
clearError();
|
|
2509
|
+
const context = await ensureAudioContext();
|
|
2510
|
+
applyOutputGain(context);
|
|
2511
|
+
if (context.state === "suspended") {
|
|
2512
|
+
await context.resume();
|
|
2513
|
+
}
|
|
2514
|
+
markPlayerActive();
|
|
2515
|
+
setState({
|
|
2516
|
+
activeSourceCount: sourceNodes.size,
|
|
2517
|
+
isActive: true,
|
|
2518
|
+
isPlaying: context.state === "running"
|
|
2519
|
+
});
|
|
2520
|
+
await queueSync();
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
errorCount += 1;
|
|
2523
|
+
markPlayerInactive();
|
|
2524
|
+
setState({
|
|
2525
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2526
|
+
isActive: false,
|
|
2527
|
+
isPlaying: false
|
|
2528
|
+
});
|
|
2529
|
+
throw error;
|
|
2530
|
+
}
|
|
2531
|
+
},
|
|
2532
|
+
subscribe: (subscriber) => {
|
|
2533
|
+
subscribers.add(subscriber);
|
|
2534
|
+
return () => {
|
|
2535
|
+
subscribers.delete(subscriber);
|
|
2536
|
+
};
|
|
2537
|
+
},
|
|
2538
|
+
get volume() {
|
|
2539
|
+
return volume;
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
return player;
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
// src/client/bargeInMonitor.ts
|
|
2546
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
2547
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
2548
|
+
var summarize = (events, thresholdMs) => {
|
|
2549
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
2550
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
2551
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
2552
|
+
const passed = stopped.length - failed;
|
|
2553
|
+
return {
|
|
2554
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
2555
|
+
events: [...events],
|
|
2556
|
+
failed,
|
|
2557
|
+
lastEvent: events.at(-1),
|
|
2558
|
+
passed,
|
|
2559
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
2560
|
+
thresholdMs,
|
|
2561
|
+
total: stopped.length
|
|
2562
|
+
};
|
|
2563
|
+
};
|
|
2564
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
2565
|
+
const listeners = new Set;
|
|
2566
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
2567
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2568
|
+
const events = [];
|
|
2569
|
+
const emit = () => {
|
|
2570
|
+
for (const listener of listeners) {
|
|
2571
|
+
listener();
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
const postEvent = (event) => {
|
|
2575
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
fetchImpl(options.path, {
|
|
2579
|
+
body: JSON.stringify(event),
|
|
2580
|
+
headers: {
|
|
2581
|
+
"Content-Type": "application/json"
|
|
2582
|
+
},
|
|
2583
|
+
method: "POST"
|
|
2584
|
+
}).catch(() => {});
|
|
2585
|
+
};
|
|
2586
|
+
const record = (status, input) => {
|
|
2587
|
+
const event = {
|
|
2588
|
+
at: Date.now(),
|
|
2589
|
+
id: createEventId(),
|
|
2590
|
+
latencyMs: input.latencyMs,
|
|
2591
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
2592
|
+
reason: input.reason,
|
|
2593
|
+
sessionId: input.sessionId,
|
|
2594
|
+
status,
|
|
2595
|
+
thresholdMs
|
|
2596
|
+
};
|
|
2597
|
+
events.push(event);
|
|
2598
|
+
postEvent(event);
|
|
2599
|
+
emit();
|
|
2600
|
+
return event;
|
|
2601
|
+
};
|
|
2602
|
+
return {
|
|
2603
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
2604
|
+
recordRequested: (input) => record("requested", input),
|
|
2605
|
+
recordSkipped: (input) => record("skipped", input),
|
|
2606
|
+
recordStopped: (input) => record("stopped", input),
|
|
2607
|
+
subscribe: (subscriber) => {
|
|
2608
|
+
listeners.add(subscriber);
|
|
2609
|
+
return () => {
|
|
2610
|
+
listeners.delete(subscriber);
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
};
|
|
2614
|
+
};
|
|
2615
|
+
|
|
2616
|
+
// src/client/duplex.ts
|
|
2617
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
2618
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
2619
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
2620
|
+
let lastPartial = controller.partial;
|
|
2621
|
+
const interruptIfPlaying = (reason) => {
|
|
2622
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
2623
|
+
options.monitor?.recordSkipped({
|
|
2624
|
+
reason,
|
|
2625
|
+
sessionId: controller.sessionId
|
|
2626
|
+
});
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
options.monitor?.recordRequested({
|
|
2630
|
+
reason,
|
|
2631
|
+
sessionId: controller.sessionId
|
|
2632
|
+
});
|
|
2633
|
+
player.interrupt().then(() => {
|
|
2634
|
+
options.monitor?.recordStopped({
|
|
2635
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
2636
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
2637
|
+
reason,
|
|
2638
|
+
sessionId: controller.sessionId
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
};
|
|
2642
|
+
const unsubscribe = controller.subscribe(() => {
|
|
2643
|
+
if (options.interruptOnPartial === false) {
|
|
2644
|
+
lastPartial = controller.partial;
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
if (!lastPartial && controller.partial) {
|
|
2648
|
+
interruptIfPlaying("partial-transcript");
|
|
2649
|
+
}
|
|
2650
|
+
lastPartial = controller.partial;
|
|
2651
|
+
});
|
|
2652
|
+
return {
|
|
2653
|
+
close: () => {
|
|
2654
|
+
unsubscribe();
|
|
2655
|
+
},
|
|
2656
|
+
handleLevel: (level) => {
|
|
2657
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
2658
|
+
interruptIfPlaying("input-level");
|
|
2659
|
+
}
|
|
2660
|
+
},
|
|
2661
|
+
sendAudio: (audio) => {
|
|
2662
|
+
interruptIfPlaying("manual-audio");
|
|
2663
|
+
controller.sendAudio(audio);
|
|
1103
2664
|
}
|
|
1104
2665
|
};
|
|
1105
2666
|
};
|
|
@@ -1126,8 +2687,7 @@ var DEFAULT_GUIDED_PROMPTS = [
|
|
|
1126
2687
|
"Now describe what you are trying to do or test.",
|
|
1127
2688
|
"Finish with any detail that feels blocked, risky, or unclear."
|
|
1128
2689
|
];
|
|
1129
|
-
var clamp = (value, min,
|
|
1130
|
-
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2690
|
+
var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
|
|
1131
2691
|
var readErrorField = (value, key) => {
|
|
1132
2692
|
const candidate = value[key];
|
|
1133
2693
|
if (typeof candidate === "string" && candidate.trim()) {
|
|
@@ -1160,6 +2720,17 @@ var formatErrorMessage = (error) => {
|
|
|
1160
2720
|
}
|
|
1161
2721
|
return "Unexpected error";
|
|
1162
2722
|
};
|
|
2723
|
+
var formatReconnectState = (reconnect) => {
|
|
2724
|
+
const pieces = [reconnect.status];
|
|
2725
|
+
if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
|
|
2726
|
+
pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
|
|
2727
|
+
}
|
|
2728
|
+
if (reconnect.nextAttemptAt) {
|
|
2729
|
+
const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
|
|
2730
|
+
pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
|
|
2731
|
+
}
|
|
2732
|
+
return pieces.join(" · ");
|
|
2733
|
+
};
|
|
1163
2734
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1164
2735
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1165
2736
|
const next = levels.slice(-(count - 1));
|
|
@@ -1216,6 +2787,20 @@ var parsePromptList = (value) => {
|
|
|
1216
2787
|
} catch {}
|
|
1217
2788
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1218
2789
|
};
|
|
2790
|
+
var parseOptionalNumber = (value) => {
|
|
2791
|
+
if (!value) {
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
const parsed = Number(value);
|
|
2795
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2796
|
+
};
|
|
2797
|
+
var resolveElement2 = (root, selector, ctor) => {
|
|
2798
|
+
if (!selector) {
|
|
2799
|
+
return null;
|
|
2800
|
+
}
|
|
2801
|
+
const value = document.querySelector(selector);
|
|
2802
|
+
return value instanceof ctor ? value : null;
|
|
2803
|
+
};
|
|
1219
2804
|
var requireElement = (root, selector, ctor, name) => {
|
|
1220
2805
|
const value = selector ? document.querySelector(selector) : null;
|
|
1221
2806
|
if (value instanceof ctor) {
|
|
@@ -1266,11 +2851,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1266
2851
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1267
2852
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1268
2853
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
2854
|
+
const reconnectReportPath = root.dataset.voiceReconnectReportPath;
|
|
2855
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
2856
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
2857
|
+
path: bargeInPath,
|
|
2858
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
2859
|
+
}) : null;
|
|
2860
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
2861
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1269
2862
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1270
2863
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1271
2864
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1272
2865
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1273
2866
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
2867
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1274
2868
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1275
2869
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1276
2870
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1279,35 +2873,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1279
2873
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1280
2874
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1281
2875
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
2876
|
+
let activeMode = null;
|
|
2877
|
+
let hasStartedModes = {
|
|
2878
|
+
general: false,
|
|
2879
|
+
guided: false
|
|
2880
|
+
};
|
|
2881
|
+
let isCapturing = false;
|
|
2882
|
+
let micError = null;
|
|
2883
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
2884
|
+
let guidedBargeInBinding = null;
|
|
2885
|
+
let generalBargeInBinding = null;
|
|
1282
2886
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1283
2887
|
capture: {
|
|
2888
|
+
onAudio: (audio, sendAudio) => {
|
|
2889
|
+
if (guidedBargeInBinding) {
|
|
2890
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
sendAudio(audio);
|
|
2894
|
+
},
|
|
1284
2895
|
onLevel: (level) => {
|
|
2896
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1285
2897
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1286
2898
|
renderWave();
|
|
1287
2899
|
}
|
|
1288
2900
|
},
|
|
2901
|
+
connection: {
|
|
2902
|
+
reconnectReportPath
|
|
2903
|
+
},
|
|
1289
2904
|
preset: "guided-intake"
|
|
1290
2905
|
});
|
|
1291
2906
|
const generalVoice = createVoiceController(generalPath, {
|
|
1292
2907
|
capture: {
|
|
2908
|
+
onAudio: (audio, sendAudio) => {
|
|
2909
|
+
if (generalBargeInBinding) {
|
|
2910
|
+
generalBargeInBinding.sendAudio(audio);
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
sendAudio(audio);
|
|
2914
|
+
},
|
|
1293
2915
|
onLevel: (level) => {
|
|
2916
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1294
2917
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1295
2918
|
renderWave();
|
|
1296
2919
|
}
|
|
1297
2920
|
},
|
|
2921
|
+
connection: {
|
|
2922
|
+
reconnectReportPath
|
|
2923
|
+
},
|
|
1298
2924
|
preset: "dictation"
|
|
1299
2925
|
});
|
|
1300
2926
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1301
2927
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2928
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
2929
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
2930
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
2931
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2932
|
+
monitor: bargeInMonitor ?? undefined
|
|
2933
|
+
});
|
|
2934
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
2935
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2936
|
+
monitor: bargeInMonitor ?? undefined
|
|
2937
|
+
});
|
|
1310
2938
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
2939
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1311
2940
|
const renderWave = () => {
|
|
1312
2941
|
const path = createVoiceWavePath(waveLevels);
|
|
1313
2942
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1319,9 +2948,12 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1319
2948
|
const render = () => {
|
|
1320
2949
|
const voice = currentVoice();
|
|
1321
2950
|
const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
|
|
1322
|
-
const status = voice
|
|
2951
|
+
const { status } = voice;
|
|
1323
2952
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1324
2953
|
errorStatus.textContent = micError || voice.error || "None";
|
|
2954
|
+
if (reconnectStatus) {
|
|
2955
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2956
|
+
}
|
|
1325
2957
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1326
2958
|
promptStatus.textContent = resolvePromptMessage({
|
|
1327
2959
|
guidedPrompts,
|
|
@@ -1385,8 +3017,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1385
3017
|
render();
|
|
1386
3018
|
}
|
|
1387
3019
|
};
|
|
1388
|
-
guidedVoice.subscribe(
|
|
1389
|
-
|
|
3020
|
+
guidedVoice.subscribe(() => {
|
|
3021
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
3022
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
3023
|
+
}
|
|
3024
|
+
render();
|
|
3025
|
+
});
|
|
3026
|
+
generalVoice.subscribe(() => {
|
|
3027
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
3028
|
+
generalAudioPlayer.start().catch(() => {});
|
|
3029
|
+
}
|
|
3030
|
+
render();
|
|
3031
|
+
});
|
|
1390
3032
|
startGuidedButton.addEventListener("click", () => {
|
|
1391
3033
|
startMode("guided");
|
|
1392
3034
|
});
|
|
@@ -1396,9 +3038,16 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1396
3038
|
stopButton.addEventListener("click", () => {
|
|
1397
3039
|
stopMic();
|
|
1398
3040
|
});
|
|
3041
|
+
root.addEventListener("absolute-voice-simulate-disconnect", () => {
|
|
3042
|
+
currentVoice().simulateDisconnect();
|
|
3043
|
+
});
|
|
1399
3044
|
window.addEventListener("beforeunload", () => {
|
|
1400
3045
|
guidedVoice.stopRecording();
|
|
1401
3046
|
generalVoice.stopRecording();
|
|
3047
|
+
guidedBargeInBinding?.close();
|
|
3048
|
+
generalBargeInBinding?.close();
|
|
3049
|
+
guidedAudioPlayer.close();
|
|
3050
|
+
generalAudioPlayer.close();
|
|
1402
3051
|
stopGuidedBinding();
|
|
1403
3052
|
stopGeneralBinding();
|
|
1404
3053
|
guidedVoice.close();
|