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