@absolutejs/voice 0.0.22-beta.65 → 0.0.22-beta.651
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 +4281 -472
- 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 +1494 -107
- package/dist/client/htmxDashboardRenderers.d.ts +71 -0
- package/dist/client/index.d.ts +109 -25
- package/dist/client/index.js +10811 -532
- 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 +1727 -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 +1838 -0
- package/dist/embed/voice-widget.js +10 -0
- package/dist/index.d.ts +379 -80
- package/dist/index.js +48148 -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 +13610 -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 +7249 -1315
- 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 +8517 -1053
- 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 +12896 -1700
- 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,264 @@ 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
|
|
499
|
+
var WS_UNAUTHORIZED = 4401;
|
|
500
|
+
var WS_FORBIDDEN = 4403;
|
|
501
|
+
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
234
502
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
235
|
-
var
|
|
503
|
+
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
504
|
+
var RECONNECT_BASE_DELAY_MS = 500;
|
|
505
|
+
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
506
|
+
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
236
507
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
237
508
|
var noop = () => {};
|
|
238
509
|
var noopUnsubscribe = () => noop;
|
|
239
510
|
var NOOP_CONNECTION = {
|
|
240
511
|
callControl: noop,
|
|
241
512
|
close: noop,
|
|
513
|
+
disconnect: noop,
|
|
242
514
|
endTurn: noop,
|
|
515
|
+
send: noop,
|
|
516
|
+
sendAudio: noop,
|
|
517
|
+
simulateDisconnect: noop,
|
|
518
|
+
subscribe: noopUnsubscribe,
|
|
243
519
|
getReadyState: () => WS_CLOSED,
|
|
244
520
|
getScenarioId: () => "",
|
|
245
521
|
getSessionId: () => "",
|
|
246
|
-
|
|
247
|
-
sendAudio: noop,
|
|
248
|
-
start: () => {},
|
|
249
|
-
subscribe: noopUnsubscribe
|
|
522
|
+
start: () => {}
|
|
250
523
|
};
|
|
251
524
|
var createSessionId = () => crypto.randomUUID();
|
|
252
|
-
var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
525
|
+
var buildWsUrl = (path, sessionId, scenarioId, query) => {
|
|
253
526
|
const { hostname, port, protocol } = window.location;
|
|
254
527
|
const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
|
|
255
528
|
const portSuffix = port ? `:${port}` : "";
|
|
@@ -258,6 +531,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
|
258
531
|
if (scenarioId) {
|
|
259
532
|
url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
|
|
260
533
|
}
|
|
534
|
+
for (const [key, value] of Object.entries(query)) {
|
|
535
|
+
if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
|
|
536
|
+
url.searchParams.set(key, value);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
261
539
|
return url.toString();
|
|
262
540
|
};
|
|
263
541
|
var isVoiceServerMessage = (value) => {
|
|
@@ -269,10 +547,12 @@ var isVoiceServerMessage = (value) => {
|
|
|
269
547
|
case "assistant":
|
|
270
548
|
case "call_lifecycle":
|
|
271
549
|
case "complete":
|
|
550
|
+
case "connection":
|
|
272
551
|
case "error":
|
|
273
552
|
case "final":
|
|
274
553
|
case "partial":
|
|
275
554
|
case "pong":
|
|
555
|
+
case "replay":
|
|
276
556
|
case "session":
|
|
277
557
|
case "turn":
|
|
278
558
|
return true;
|
|
@@ -298,17 +578,24 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
298
578
|
const listeners = new Set;
|
|
299
579
|
const shouldReconnect = options.reconnect !== false;
|
|
300
580
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
581
|
+
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
301
582
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
583
|
+
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
584
|
+
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
302
585
|
const state = {
|
|
303
586
|
isConnected: false,
|
|
304
587
|
pendingMessages: [],
|
|
305
588
|
scenarioId: options.scenarioId ?? null,
|
|
306
589
|
pingInterval: null,
|
|
307
590
|
reconnectAttempts: 0,
|
|
591
|
+
reconnectResetTimeout: null,
|
|
308
592
|
reconnectTimeout: null,
|
|
309
593
|
sessionId: options.sessionId ?? createSessionId(),
|
|
310
594
|
ws: null
|
|
311
595
|
};
|
|
596
|
+
const emitConnection = (reconnect) => {
|
|
597
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
598
|
+
};
|
|
312
599
|
const clearTimers = () => {
|
|
313
600
|
if (state.pingInterval) {
|
|
314
601
|
clearInterval(state.pingInterval);
|
|
@@ -318,6 +605,10 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
318
605
|
clearTimeout(state.reconnectTimeout);
|
|
319
606
|
state.reconnectTimeout = null;
|
|
320
607
|
}
|
|
608
|
+
if (state.reconnectResetTimeout) {
|
|
609
|
+
clearTimeout(state.reconnectResetTimeout);
|
|
610
|
+
state.reconnectResetTimeout = null;
|
|
611
|
+
}
|
|
321
612
|
};
|
|
322
613
|
const flushPendingMessages = () => {
|
|
323
614
|
if (state.ws?.readyState !== WS_OPEN) {
|
|
@@ -332,20 +623,61 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
332
623
|
};
|
|
333
624
|
const scheduleReconnect = () => {
|
|
334
625
|
state.reconnectAttempts += 1;
|
|
626
|
+
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
627
|
+
const nextAttemptAt = Date.now() + delayMs;
|
|
628
|
+
emitConnection({
|
|
629
|
+
reconnect: {
|
|
630
|
+
attempts: state.reconnectAttempts,
|
|
631
|
+
lastDisconnectAt: Date.now(),
|
|
632
|
+
maxAttempts: maxReconnectAttempts,
|
|
633
|
+
nextAttemptAt,
|
|
634
|
+
status: "reconnecting"
|
|
635
|
+
},
|
|
636
|
+
type: "connection"
|
|
637
|
+
});
|
|
335
638
|
state.reconnectTimeout = setTimeout(() => {
|
|
336
639
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
640
|
+
emitConnection({
|
|
641
|
+
reconnect: {
|
|
642
|
+
attempts: state.reconnectAttempts,
|
|
643
|
+
maxAttempts: maxReconnectAttempts,
|
|
644
|
+
status: "exhausted"
|
|
645
|
+
},
|
|
646
|
+
type: "connection"
|
|
647
|
+
});
|
|
337
648
|
return;
|
|
338
649
|
}
|
|
339
650
|
connect();
|
|
340
|
-
},
|
|
651
|
+
}, delayMs);
|
|
341
652
|
};
|
|
342
653
|
const connect = () => {
|
|
343
|
-
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
|
|
654
|
+
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
344
655
|
ws.binaryType = "arraybuffer";
|
|
345
656
|
ws.onopen = () => {
|
|
657
|
+
if (state.ws !== ws) {
|
|
658
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
346
662
|
state.isConnected = true;
|
|
347
|
-
state.reconnectAttempts = 0;
|
|
348
663
|
flushPendingMessages();
|
|
664
|
+
if (wasReconnecting) {
|
|
665
|
+
emitConnection({
|
|
666
|
+
reconnect: {
|
|
667
|
+
attempts: state.reconnectAttempts,
|
|
668
|
+
lastResumedAt: Date.now(),
|
|
669
|
+
maxAttempts: maxReconnectAttempts,
|
|
670
|
+
status: "resumed"
|
|
671
|
+
},
|
|
672
|
+
type: "connection"
|
|
673
|
+
});
|
|
674
|
+
state.reconnectResetTimeout = setTimeout(() => {
|
|
675
|
+
if (state.ws === ws && ws.readyState === WS_OPEN) {
|
|
676
|
+
state.reconnectAttempts = 0;
|
|
677
|
+
}
|
|
678
|
+
state.reconnectResetTimeout = null;
|
|
679
|
+
}, reconnectResetAfterMs);
|
|
680
|
+
}
|
|
349
681
|
listeners.forEach((listener) => listener({
|
|
350
682
|
scenarioId: state.scenarioId ?? undefined,
|
|
351
683
|
sessionId: state.sessionId,
|
|
@@ -359,6 +691,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
359
691
|
}, pingInterval);
|
|
360
692
|
};
|
|
361
693
|
ws.onmessage = (event) => {
|
|
694
|
+
if (state.ws !== ws)
|
|
695
|
+
return;
|
|
362
696
|
const parsed = parseServerMessage(event);
|
|
363
697
|
if (!parsed) {
|
|
364
698
|
return;
|
|
@@ -370,11 +704,24 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
370
704
|
listeners.forEach((listener) => listener(parsed));
|
|
371
705
|
};
|
|
372
706
|
ws.onclose = (event) => {
|
|
707
|
+
if (state.ws !== ws)
|
|
708
|
+
return;
|
|
709
|
+
state.ws = null;
|
|
373
710
|
state.isConnected = false;
|
|
374
711
|
clearTimers();
|
|
375
|
-
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
|
|
712
|
+
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
376
713
|
if (reconnectable) {
|
|
377
714
|
scheduleReconnect();
|
|
715
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
716
|
+
emitConnection({
|
|
717
|
+
reconnect: {
|
|
718
|
+
attempts: state.reconnectAttempts,
|
|
719
|
+
lastDisconnectAt: Date.now(),
|
|
720
|
+
maxAttempts: maxReconnectAttempts,
|
|
721
|
+
status: "exhausted"
|
|
722
|
+
},
|
|
723
|
+
type: "connection"
|
|
724
|
+
});
|
|
378
725
|
}
|
|
379
726
|
};
|
|
380
727
|
state.ws = ws;
|
|
@@ -397,9 +744,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
397
744
|
state.scenarioId = input.scenarioId;
|
|
398
745
|
}
|
|
399
746
|
send({
|
|
400
|
-
|
|
747
|
+
scenarioId: state.scenarioId ?? undefined,
|
|
401
748
|
sessionId: state.sessionId,
|
|
402
|
-
|
|
749
|
+
type: "start"
|
|
403
750
|
});
|
|
404
751
|
};
|
|
405
752
|
const sendAudio = (audio) => {
|
|
@@ -414,15 +761,34 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
414
761
|
type: "call_control"
|
|
415
762
|
});
|
|
416
763
|
};
|
|
417
|
-
const close = () => {
|
|
764
|
+
const close = (reason = "client-close") => {
|
|
765
|
+
clearTimers();
|
|
766
|
+
if (state.ws) {
|
|
767
|
+
const ws = state.ws;
|
|
768
|
+
state.ws = null;
|
|
769
|
+
if (ws.readyState === WS_OPEN) {
|
|
770
|
+
ws.send(JSON.stringify({ reason, type: "close" }));
|
|
771
|
+
}
|
|
772
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
773
|
+
}
|
|
774
|
+
state.isConnected = false;
|
|
775
|
+
listeners.clear();
|
|
776
|
+
};
|
|
777
|
+
const disconnect = () => {
|
|
418
778
|
clearTimers();
|
|
419
779
|
if (state.ws) {
|
|
420
|
-
state.ws
|
|
780
|
+
const ws = state.ws;
|
|
421
781
|
state.ws = null;
|
|
782
|
+
ws.close(WS_NORMAL_CLOSURE);
|
|
422
783
|
}
|
|
423
784
|
state.isConnected = false;
|
|
424
785
|
listeners.clear();
|
|
425
786
|
};
|
|
787
|
+
const simulateDisconnect = () => {
|
|
788
|
+
if (state.ws?.readyState === WS_OPEN) {
|
|
789
|
+
state.ws.close(4000, "absolutejs-voice-reconnect-proof");
|
|
790
|
+
}
|
|
791
|
+
};
|
|
426
792
|
const subscribe = (callback) => {
|
|
427
793
|
listeners.add(callback);
|
|
428
794
|
return () => {
|
|
@@ -433,32 +799,64 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
433
799
|
return {
|
|
434
800
|
callControl,
|
|
435
801
|
close,
|
|
802
|
+
disconnect,
|
|
436
803
|
endTurn,
|
|
437
|
-
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
438
|
-
getScenarioId: () => state.scenarioId ?? "",
|
|
439
|
-
getSessionId: () => state.sessionId,
|
|
440
804
|
send,
|
|
441
805
|
sendAudio,
|
|
806
|
+
simulateDisconnect,
|
|
442
807
|
start,
|
|
443
|
-
subscribe
|
|
808
|
+
subscribe,
|
|
809
|
+
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
810
|
+
getScenarioId: () => state.scenarioId ?? "",
|
|
811
|
+
getSessionId: () => state.sessionId
|
|
444
812
|
};
|
|
445
813
|
};
|
|
446
814
|
|
|
447
815
|
// src/client/store.ts
|
|
816
|
+
var createInitialReconnectState = () => ({
|
|
817
|
+
attempts: 0,
|
|
818
|
+
maxAttempts: 0,
|
|
819
|
+
status: "idle"
|
|
820
|
+
});
|
|
821
|
+
var appendSegmentText = (accumulated, next) => {
|
|
822
|
+
const nextText = next.trim().replace(/\s+/g, " ");
|
|
823
|
+
if (!nextText)
|
|
824
|
+
return accumulated;
|
|
825
|
+
if (!accumulated)
|
|
826
|
+
return nextText;
|
|
827
|
+
if (accumulated === nextText || accumulated.endsWith(nextText)) {
|
|
828
|
+
return accumulated;
|
|
829
|
+
}
|
|
830
|
+
if (nextText.includes(accumulated))
|
|
831
|
+
return nextText;
|
|
832
|
+
return `${accumulated} ${nextText}`;
|
|
833
|
+
};
|
|
834
|
+
var joinPartial = (finalized, interim) => {
|
|
835
|
+
const interimText = interim.trim().replace(/\s+/g, " ");
|
|
836
|
+
if (!finalized)
|
|
837
|
+
return interimText;
|
|
838
|
+
if (!interimText || finalized.endsWith(interimText))
|
|
839
|
+
return finalized;
|
|
840
|
+
return `${finalized} ${interimText}`;
|
|
841
|
+
};
|
|
448
842
|
var createInitialState = () => ({
|
|
449
843
|
assistantAudio: [],
|
|
844
|
+
assistantStreamingText: "",
|
|
450
845
|
assistantTexts: [],
|
|
451
846
|
call: null,
|
|
452
847
|
error: null,
|
|
453
848
|
isConnected: false,
|
|
454
|
-
scenarioId: null,
|
|
455
849
|
partial: "",
|
|
850
|
+
reconnect: createInitialReconnectState(),
|
|
851
|
+
scenarioId: null,
|
|
456
852
|
sessionId: null,
|
|
853
|
+
sessionMetadata: null,
|
|
457
854
|
status: "idle",
|
|
458
855
|
turns: []
|
|
459
856
|
});
|
|
460
857
|
var createVoiceStreamStore = () => {
|
|
461
858
|
let state = createInitialState();
|
|
859
|
+
let turnFinalText = "";
|
|
462
860
|
const subscribers = new Set;
|
|
463
861
|
const notify = () => {
|
|
464
862
|
subscribers.forEach((subscriber) => subscriber());
|
|
@@ -482,9 +880,16 @@ var createVoiceStreamStore = () => {
|
|
|
482
880
|
case "assistant":
|
|
483
881
|
state = {
|
|
484
882
|
...state,
|
|
883
|
+
assistantStreamingText: "",
|
|
485
884
|
assistantTexts: [...state.assistantTexts, action.text]
|
|
486
885
|
};
|
|
487
886
|
break;
|
|
887
|
+
case "assistant_delta":
|
|
888
|
+
state = {
|
|
889
|
+
...state,
|
|
890
|
+
assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
|
|
891
|
+
};
|
|
892
|
+
break;
|
|
488
893
|
case "complete":
|
|
489
894
|
state = {
|
|
490
895
|
...state,
|
|
@@ -509,7 +914,19 @@ var createVoiceStreamStore = () => {
|
|
|
509
914
|
case "connected":
|
|
510
915
|
state = {
|
|
511
916
|
...state,
|
|
512
|
-
isConnected: true
|
|
917
|
+
isConnected: true,
|
|
918
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
919
|
+
...state.reconnect,
|
|
920
|
+
lastResumedAt: Date.now(),
|
|
921
|
+
nextAttemptAt: undefined,
|
|
922
|
+
status: "resumed"
|
|
923
|
+
} : state.reconnect
|
|
924
|
+
};
|
|
925
|
+
break;
|
|
926
|
+
case "connection":
|
|
927
|
+
state = {
|
|
928
|
+
...state,
|
|
929
|
+
reconnect: action.reconnect
|
|
513
930
|
};
|
|
514
931
|
break;
|
|
515
932
|
case "disconnected":
|
|
@@ -525,16 +942,39 @@ var createVoiceStreamStore = () => {
|
|
|
525
942
|
};
|
|
526
943
|
break;
|
|
527
944
|
case "final":
|
|
945
|
+
turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
|
|
528
946
|
state = {
|
|
529
947
|
...state,
|
|
530
|
-
partial:
|
|
531
|
-
turns: state.turns.map((turn) => turn)
|
|
948
|
+
partial: turnFinalText
|
|
532
949
|
};
|
|
533
950
|
break;
|
|
534
951
|
case "partial":
|
|
535
952
|
state = {
|
|
536
953
|
...state,
|
|
537
|
-
partial: action.transcript.text
|
|
954
|
+
partial: joinPartial(turnFinalText, action.transcript.text)
|
|
955
|
+
};
|
|
956
|
+
break;
|
|
957
|
+
case "replay":
|
|
958
|
+
turnFinalText = action.partial;
|
|
959
|
+
state = {
|
|
960
|
+
...state,
|
|
961
|
+
assistantStreamingText: "",
|
|
962
|
+
assistantTexts: [...action.assistantTexts],
|
|
963
|
+
call: action.call ?? null,
|
|
964
|
+
error: null,
|
|
965
|
+
isConnected: action.status === "active",
|
|
966
|
+
partial: action.partial,
|
|
967
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
968
|
+
...state.reconnect,
|
|
969
|
+
lastResumedAt: Date.now(),
|
|
970
|
+
nextAttemptAt: undefined,
|
|
971
|
+
status: "resumed"
|
|
972
|
+
} : state.reconnect,
|
|
973
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
974
|
+
sessionId: action.sessionId,
|
|
975
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
976
|
+
status: action.status,
|
|
977
|
+
turns: [...action.turns]
|
|
538
978
|
};
|
|
539
979
|
break;
|
|
540
980
|
case "session":
|
|
@@ -544,10 +984,12 @@ var createVoiceStreamStore = () => {
|
|
|
544
984
|
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
545
985
|
isConnected: action.status === "active",
|
|
546
986
|
sessionId: action.sessionId,
|
|
987
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
547
988
|
status: action.status
|
|
548
989
|
};
|
|
549
990
|
break;
|
|
550
991
|
case "turn":
|
|
992
|
+
turnFinalText = "";
|
|
551
993
|
state = {
|
|
552
994
|
...state,
|
|
553
995
|
partial: "",
|
|
@@ -574,30 +1016,81 @@ var createVoiceStreamStore = () => {
|
|
|
574
1016
|
var createVoiceStream = (path, options = {}) => {
|
|
575
1017
|
const connection = createVoiceConnection(path, options);
|
|
576
1018
|
const store = createVoiceStreamStore();
|
|
1019
|
+
const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
|
|
1020
|
+
...options.browserMedia,
|
|
1021
|
+
getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
|
|
1022
|
+
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1023
|
+
}) : null;
|
|
577
1024
|
const subscribers = new Set;
|
|
578
1025
|
const start = (input) => Promise.resolve().then(() => {
|
|
579
1026
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
580
1027
|
return;
|
|
581
1028
|
}
|
|
582
1029
|
connection.start(input);
|
|
1030
|
+
browserMediaReporter?.start();
|
|
583
1031
|
});
|
|
584
1032
|
const notify = () => {
|
|
585
1033
|
subscribers.forEach((subscriber) => subscriber());
|
|
586
1034
|
};
|
|
1035
|
+
const reportReconnect = () => {
|
|
1036
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const snapshot = store.getSnapshot();
|
|
1040
|
+
const body = JSON.stringify({
|
|
1041
|
+
at: Date.now(),
|
|
1042
|
+
reconnect: snapshot.reconnect,
|
|
1043
|
+
scenarioId: snapshot.scenarioId,
|
|
1044
|
+
sessionId: connection.getSessionId(),
|
|
1045
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
1046
|
+
});
|
|
1047
|
+
fetch(options.reconnectReportPath, {
|
|
1048
|
+
body,
|
|
1049
|
+
headers: {
|
|
1050
|
+
"Content-Type": "application/json"
|
|
1051
|
+
},
|
|
1052
|
+
keepalive: true,
|
|
1053
|
+
method: "POST"
|
|
1054
|
+
}).catch(() => {});
|
|
1055
|
+
};
|
|
587
1056
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
588
1057
|
const action = serverMessageToAction(message);
|
|
589
1058
|
if (action) {
|
|
590
1059
|
store.dispatch(action);
|
|
1060
|
+
if (message.type === "connection") {
|
|
1061
|
+
reportReconnect();
|
|
1062
|
+
}
|
|
591
1063
|
notify();
|
|
592
1064
|
}
|
|
593
1065
|
});
|
|
594
1066
|
return {
|
|
1067
|
+
start,
|
|
1068
|
+
get assistantAudio() {
|
|
1069
|
+
return store.getSnapshot().assistantAudio;
|
|
1070
|
+
},
|
|
1071
|
+
get assistantTexts() {
|
|
1072
|
+
return store.getSnapshot().assistantTexts;
|
|
1073
|
+
},
|
|
1074
|
+
get assistantStreamingText() {
|
|
1075
|
+
return store.getSnapshot().assistantStreamingText;
|
|
1076
|
+
},
|
|
1077
|
+
get call() {
|
|
1078
|
+
return store.getSnapshot().call;
|
|
1079
|
+
},
|
|
595
1080
|
callControl(message) {
|
|
596
1081
|
connection.callControl(message);
|
|
597
1082
|
},
|
|
598
|
-
close() {
|
|
1083
|
+
close(reason) {
|
|
599
1084
|
unsubscribeConnection();
|
|
600
|
-
|
|
1085
|
+
browserMediaReporter?.close();
|
|
1086
|
+
connection.close(reason);
|
|
1087
|
+
store.dispatch({ type: "disconnected" });
|
|
1088
|
+
notify();
|
|
1089
|
+
},
|
|
1090
|
+
disconnect() {
|
|
1091
|
+
unsubscribeConnection();
|
|
1092
|
+
browserMediaReporter?.close();
|
|
1093
|
+
connection.disconnect();
|
|
601
1094
|
store.dispatch({ type: "disconnected" });
|
|
602
1095
|
notify();
|
|
603
1096
|
},
|
|
@@ -616,44 +1109,43 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
616
1109
|
get isConnected() {
|
|
617
1110
|
return store.getSnapshot().isConnected;
|
|
618
1111
|
},
|
|
619
|
-
get scenarioId() {
|
|
620
|
-
return store.getSnapshot().scenarioId;
|
|
621
|
-
},
|
|
622
|
-
start,
|
|
623
1112
|
get partial() {
|
|
624
1113
|
return store.getSnapshot().partial;
|
|
625
1114
|
},
|
|
626
|
-
get
|
|
627
|
-
return
|
|
1115
|
+
get reconnect() {
|
|
1116
|
+
return store.getSnapshot().reconnect;
|
|
628
1117
|
},
|
|
629
|
-
get
|
|
630
|
-
return store.getSnapshot().
|
|
1118
|
+
get scenarioId() {
|
|
1119
|
+
return store.getSnapshot().scenarioId;
|
|
631
1120
|
},
|
|
632
|
-
|
|
633
|
-
|
|
1121
|
+
sendAudio(audio) {
|
|
1122
|
+
connection.sendAudio(audio);
|
|
634
1123
|
},
|
|
635
|
-
get
|
|
636
|
-
return
|
|
1124
|
+
get sessionId() {
|
|
1125
|
+
return connection.getSessionId();
|
|
637
1126
|
},
|
|
638
|
-
get
|
|
639
|
-
return store.getSnapshot().
|
|
1127
|
+
get sessionMetadata() {
|
|
1128
|
+
return store.getSnapshot().sessionMetadata;
|
|
640
1129
|
},
|
|
641
|
-
|
|
642
|
-
|
|
1130
|
+
simulateDisconnect() {
|
|
1131
|
+
connection.simulateDisconnect();
|
|
643
1132
|
},
|
|
644
|
-
|
|
645
|
-
|
|
1133
|
+
get status() {
|
|
1134
|
+
return store.getSnapshot().status;
|
|
646
1135
|
},
|
|
647
1136
|
subscribe(subscriber) {
|
|
648
1137
|
subscribers.add(subscriber);
|
|
649
1138
|
return () => {
|
|
650
1139
|
subscribers.delete(subscriber);
|
|
651
1140
|
};
|
|
1141
|
+
},
|
|
1142
|
+
get turns() {
|
|
1143
|
+
return store.getSnapshot().turns;
|
|
652
1144
|
}
|
|
653
1145
|
};
|
|
654
1146
|
};
|
|
655
1147
|
|
|
656
|
-
// src/audioConditioning.ts
|
|
1148
|
+
// src/core/audioConditioning.ts
|
|
657
1149
|
var DEFAULT_TARGET_LEVEL = 0.08;
|
|
658
1150
|
var DEFAULT_MAX_GAIN = 3;
|
|
659
1151
|
var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
|
|
@@ -671,34 +1163,37 @@ var resolveAudioConditioningConfig = (config) => {
|
|
|
671
1163
|
};
|
|
672
1164
|
};
|
|
673
1165
|
|
|
674
|
-
// src/turnProfiles.ts
|
|
1166
|
+
// src/core/turnProfiles.ts
|
|
675
1167
|
var TURN_PROFILE_DEFAULTS = {
|
|
676
1168
|
balanced: {
|
|
677
1169
|
qualityProfile: "general",
|
|
1170
|
+
minSilenceMs: 400,
|
|
678
1171
|
silenceMs: 1400,
|
|
679
1172
|
speechThreshold: 0.012,
|
|
680
1173
|
transcriptStabilityMs: 1000
|
|
681
1174
|
},
|
|
682
1175
|
fast: {
|
|
683
1176
|
qualityProfile: "general",
|
|
1177
|
+
minSilenceMs: 300,
|
|
684
1178
|
silenceMs: 700,
|
|
685
1179
|
speechThreshold: 0.015,
|
|
686
1180
|
transcriptStabilityMs: 450
|
|
687
1181
|
},
|
|
688
1182
|
"long-form": {
|
|
689
1183
|
qualityProfile: "general",
|
|
1184
|
+
minSilenceMs: 600,
|
|
690
1185
|
silenceMs: 2200,
|
|
691
1186
|
speechThreshold: 0.01,
|
|
692
1187
|
transcriptStabilityMs: 1500
|
|
693
1188
|
}
|
|
694
1189
|
};
|
|
695
1190
|
var QUALITY_PROFILE_DEFAULTS = {
|
|
696
|
-
general: {},
|
|
697
1191
|
"accent-heavy": {
|
|
698
1192
|
silenceMs: 1200,
|
|
699
1193
|
speechThreshold: 0.01,
|
|
700
1194
|
transcriptStabilityMs: 1200
|
|
701
1195
|
},
|
|
1196
|
+
general: {},
|
|
702
1197
|
"noisy-room": {
|
|
703
1198
|
silenceMs: 2000,
|
|
704
1199
|
speechThreshold: 0.02,
|
|
@@ -717,16 +1212,18 @@ var resolveTurnDetectionConfig = (config) => {
|
|
|
717
1212
|
const qualityProfile = config?.qualityProfile ?? DEFAULT_QUALITY_PROFILE;
|
|
718
1213
|
const preset = TURN_PROFILE_DEFAULTS[profile];
|
|
719
1214
|
const quality = QUALITY_PROFILE_DEFAULTS[qualityProfile];
|
|
1215
|
+
const silenceMs = config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs;
|
|
720
1216
|
return {
|
|
721
1217
|
profile,
|
|
722
1218
|
qualityProfile,
|
|
723
|
-
|
|
1219
|
+
minSilenceMs: Math.min(silenceMs, config?.minSilenceMs ?? quality.minSilenceMs ?? preset.minSilenceMs),
|
|
1220
|
+
silenceMs,
|
|
724
1221
|
speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
|
|
725
1222
|
transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
|
|
726
1223
|
};
|
|
727
1224
|
};
|
|
728
1225
|
|
|
729
|
-
// src/presets.ts
|
|
1226
|
+
// src/core/presets.ts
|
|
730
1227
|
var PRESET_INPUTS = {
|
|
731
1228
|
chat: {
|
|
732
1229
|
audioConditioning: {
|
|
@@ -747,8 +1244,8 @@ var PRESET_INPUTS = {
|
|
|
747
1244
|
},
|
|
748
1245
|
sttLifecycle: "continuous",
|
|
749
1246
|
turnDetection: {
|
|
750
|
-
|
|
751
|
-
|
|
1247
|
+
profile: "balanced",
|
|
1248
|
+
qualityProfile: "short-command"
|
|
752
1249
|
}
|
|
753
1250
|
},
|
|
754
1251
|
default: {
|
|
@@ -763,8 +1260,8 @@ var PRESET_INPUTS = {
|
|
|
763
1260
|
},
|
|
764
1261
|
sttLifecycle: "continuous",
|
|
765
1262
|
turnDetection: {
|
|
766
|
-
|
|
767
|
-
|
|
1263
|
+
profile: "fast",
|
|
1264
|
+
qualityProfile: "general"
|
|
768
1265
|
}
|
|
769
1266
|
},
|
|
770
1267
|
dictation: {
|
|
@@ -786,8 +1283,8 @@ var PRESET_INPUTS = {
|
|
|
786
1283
|
},
|
|
787
1284
|
sttLifecycle: "continuous",
|
|
788
1285
|
turnDetection: {
|
|
789
|
-
|
|
790
|
-
|
|
1286
|
+
profile: "long-form",
|
|
1287
|
+
qualityProfile: "accent-heavy"
|
|
791
1288
|
}
|
|
792
1289
|
},
|
|
793
1290
|
"guided-intake": {
|
|
@@ -809,8 +1306,8 @@ var PRESET_INPUTS = {
|
|
|
809
1306
|
},
|
|
810
1307
|
sttLifecycle: "turn-scoped",
|
|
811
1308
|
turnDetection: {
|
|
812
|
-
|
|
813
|
-
|
|
1309
|
+
profile: "long-form",
|
|
1310
|
+
qualityProfile: "accent-heavy"
|
|
814
1311
|
}
|
|
815
1312
|
},
|
|
816
1313
|
"noisy-room": {
|
|
@@ -832,8 +1329,8 @@ var PRESET_INPUTS = {
|
|
|
832
1329
|
},
|
|
833
1330
|
sttLifecycle: "continuous",
|
|
834
1331
|
turnDetection: {
|
|
835
|
-
qualityProfile: "noisy-room",
|
|
836
1332
|
profile: "long-form",
|
|
1333
|
+
qualityProfile: "noisy-room",
|
|
837
1334
|
silenceMs: 2100,
|
|
838
1335
|
speechThreshold: 0.02,
|
|
839
1336
|
transcriptStabilityMs: 1650
|
|
@@ -858,8 +1355,8 @@ var PRESET_INPUTS = {
|
|
|
858
1355
|
},
|
|
859
1356
|
sttLifecycle: "continuous",
|
|
860
1357
|
turnDetection: {
|
|
861
|
-
qualityProfile: "noisy-room",
|
|
862
1358
|
profile: "long-form",
|
|
1359
|
+
qualityProfile: "noisy-room",
|
|
863
1360
|
silenceMs: 660,
|
|
864
1361
|
speechThreshold: 0.012,
|
|
865
1362
|
transcriptStabilityMs: 300
|
|
@@ -884,8 +1381,8 @@ var PRESET_INPUTS = {
|
|
|
884
1381
|
},
|
|
885
1382
|
sttLifecycle: "continuous",
|
|
886
1383
|
turnDetection: {
|
|
887
|
-
qualityProfile: "noisy-room",
|
|
888
1384
|
profile: "long-form",
|
|
1385
|
+
qualityProfile: "noisy-room",
|
|
889
1386
|
silenceMs: 620,
|
|
890
1387
|
speechThreshold: 0.012,
|
|
891
1388
|
transcriptStabilityMs: 280
|
|
@@ -910,8 +1407,8 @@ var PRESET_INPUTS = {
|
|
|
910
1407
|
},
|
|
911
1408
|
sttLifecycle: "continuous",
|
|
912
1409
|
turnDetection: {
|
|
913
|
-
|
|
914
|
-
|
|
1410
|
+
profile: "long-form",
|
|
1411
|
+
qualityProfile: "noisy-room"
|
|
915
1412
|
}
|
|
916
1413
|
}
|
|
917
1414
|
};
|
|
@@ -935,14 +1432,17 @@ var resolveVoiceRuntimePreset = (name = "default") => {
|
|
|
935
1432
|
// src/client/controller.ts
|
|
936
1433
|
var createInitialState2 = (stream) => ({
|
|
937
1434
|
assistantAudio: [...stream.assistantAudio],
|
|
1435
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
938
1436
|
assistantTexts: [...stream.assistantTexts],
|
|
939
1437
|
call: stream.call,
|
|
940
1438
|
error: stream.error,
|
|
941
1439
|
isConnected: stream.isConnected,
|
|
942
1440
|
isRecording: false,
|
|
943
1441
|
partial: stream.partial,
|
|
1442
|
+
reconnect: stream.reconnect,
|
|
944
1443
|
recordingError: null,
|
|
945
1444
|
sessionId: stream.sessionId,
|
|
1445
|
+
sessionMetadata: stream.sessionMetadata,
|
|
946
1446
|
scenarioId: stream.scenarioId,
|
|
947
1447
|
status: stream.status,
|
|
948
1448
|
turns: [...stream.turns]
|
|
@@ -965,12 +1465,15 @@ var createVoiceController = (path, options = {}) => {
|
|
|
965
1465
|
state = {
|
|
966
1466
|
...state,
|
|
967
1467
|
assistantAudio: [...stream.assistantAudio],
|
|
1468
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
968
1469
|
assistantTexts: [...stream.assistantTexts],
|
|
969
1470
|
call: stream.call,
|
|
970
1471
|
error: stream.error,
|
|
971
1472
|
isConnected: stream.isConnected,
|
|
972
1473
|
partial: stream.partial,
|
|
1474
|
+
reconnect: stream.reconnect,
|
|
973
1475
|
sessionId: stream.sessionId,
|
|
1476
|
+
sessionMetadata: stream.sessionMetadata,
|
|
974
1477
|
scenarioId: stream.scenarioId,
|
|
975
1478
|
status: stream.status,
|
|
976
1479
|
turns: [...stream.turns]
|
|
@@ -994,8 +1497,15 @@ var createVoiceController = (path, options = {}) => {
|
|
|
994
1497
|
capture = createMicrophoneCapture({
|
|
995
1498
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
996
1499
|
onLevel: options.capture?.onLevel,
|
|
997
|
-
onAudio: (audio) =>
|
|
998
|
-
|
|
1500
|
+
onAudio: (audio) => {
|
|
1501
|
+
if (options.capture?.onAudio) {
|
|
1502
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
stream.sendAudio(audio);
|
|
1506
|
+
},
|
|
1507
|
+
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
|
|
1508
|
+
...options.capture?.stream ? { stream: options.capture.stream } : {}
|
|
999
1509
|
});
|
|
1000
1510
|
return capture;
|
|
1001
1511
|
};
|
|
@@ -1025,6 +1535,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1025
1535
|
};
|
|
1026
1536
|
notify();
|
|
1027
1537
|
} catch (error) {
|
|
1538
|
+
capture?.stop();
|
|
1028
1539
|
capture = null;
|
|
1029
1540
|
state = {
|
|
1030
1541
|
...state,
|
|
@@ -1035,17 +1546,37 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1035
1546
|
throw error;
|
|
1036
1547
|
}
|
|
1037
1548
|
};
|
|
1038
|
-
const close = () => {
|
|
1549
|
+
const close = (reason) => {
|
|
1039
1550
|
unsubscribeStream();
|
|
1040
1551
|
stopRecording();
|
|
1041
|
-
stream.close();
|
|
1552
|
+
stream.close(reason);
|
|
1553
|
+
};
|
|
1554
|
+
const disconnect = () => {
|
|
1555
|
+
unsubscribeStream();
|
|
1556
|
+
stopRecording();
|
|
1557
|
+
stream.disconnect();
|
|
1042
1558
|
};
|
|
1043
1559
|
return {
|
|
1560
|
+
close,
|
|
1561
|
+
disconnect,
|
|
1562
|
+
startRecording,
|
|
1563
|
+
stopRecording,
|
|
1564
|
+
get assistantAudio() {
|
|
1565
|
+
return state.assistantAudio;
|
|
1566
|
+
},
|
|
1567
|
+
get assistantTexts() {
|
|
1568
|
+
return state.assistantTexts;
|
|
1569
|
+
},
|
|
1570
|
+
get assistantStreamingText() {
|
|
1571
|
+
return state.assistantStreamingText;
|
|
1572
|
+
},
|
|
1044
1573
|
bindHTMX(bindingOptions) {
|
|
1045
1574
|
return bindVoiceHTMX(stream, bindingOptions);
|
|
1046
1575
|
},
|
|
1576
|
+
get call() {
|
|
1577
|
+
return state.call;
|
|
1578
|
+
},
|
|
1047
1579
|
callControl: (message) => stream.callControl(message),
|
|
1048
|
-
close,
|
|
1049
1580
|
endTurn: () => stream.endTurn(),
|
|
1050
1581
|
get error() {
|
|
1051
1582
|
return state.error;
|
|
@@ -1061,21 +1592,26 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1061
1592
|
get partial() {
|
|
1062
1593
|
return state.partial;
|
|
1063
1594
|
},
|
|
1595
|
+
get reconnect() {
|
|
1596
|
+
return state.reconnect;
|
|
1597
|
+
},
|
|
1064
1598
|
get recordingError() {
|
|
1065
1599
|
return state.recordingError;
|
|
1066
1600
|
},
|
|
1601
|
+
get scenarioId() {
|
|
1602
|
+
return state.scenarioId;
|
|
1603
|
+
},
|
|
1067
1604
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1068
1605
|
get sessionId() {
|
|
1069
1606
|
return state.sessionId;
|
|
1070
1607
|
},
|
|
1071
|
-
get
|
|
1072
|
-
return state.
|
|
1608
|
+
get sessionMetadata() {
|
|
1609
|
+
return state.sessionMetadata;
|
|
1073
1610
|
},
|
|
1074
|
-
|
|
1611
|
+
simulateDisconnect: () => stream.simulateDisconnect(),
|
|
1075
1612
|
get status() {
|
|
1076
1613
|
return state.status;
|
|
1077
1614
|
},
|
|
1078
|
-
stopRecording,
|
|
1079
1615
|
subscribe: (subscriber) => {
|
|
1080
1616
|
subscribers.add(subscriber);
|
|
1081
1617
|
return () => {
|
|
@@ -1091,15 +1627,778 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1091
1627
|
},
|
|
1092
1628
|
get turns() {
|
|
1093
1629
|
return state.turns;
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
// src/client/timeStretch.ts
|
|
1635
|
+
var HOP_MS = 10;
|
|
1636
|
+
var SEEK_MS = 5;
|
|
1637
|
+
var ENERGY_EPSILON = 0.000001;
|
|
1638
|
+
var HALF = 0.5;
|
|
1639
|
+
var MS_PER_SECOND = 1000;
|
|
1640
|
+
var makeHann = (length) => {
|
|
1641
|
+
const weights = new Float32Array(length);
|
|
1642
|
+
for (let index = 0;index < length; index += 1) {
|
|
1643
|
+
weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
|
|
1644
|
+
}
|
|
1645
|
+
return weights;
|
|
1646
|
+
};
|
|
1647
|
+
var correlationScore = (base, start, ref, length) => {
|
|
1648
|
+
let dot = 0;
|
|
1649
|
+
let energy = 0;
|
|
1650
|
+
for (let index = 0;index < length; index += 1) {
|
|
1651
|
+
const sample = base[start + index] ?? 0;
|
|
1652
|
+
dot += sample * (ref[index] ?? 0);
|
|
1653
|
+
energy += sample * sample;
|
|
1654
|
+
}
|
|
1655
|
+
return dot / Math.sqrt(energy + ENERGY_EPSILON);
|
|
1656
|
+
};
|
|
1657
|
+
var overlapAddGrain = (src, off, tail, weights, hop) => {
|
|
1658
|
+
const out = new Float32Array(hop);
|
|
1659
|
+
const nextTail = new Float32Array(hop);
|
|
1660
|
+
for (let index = 0;index < hop; index += 1) {
|
|
1661
|
+
out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
|
|
1662
|
+
nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
|
|
1663
|
+
}
|
|
1664
|
+
return { nextTail, out };
|
|
1665
|
+
};
|
|
1666
|
+
var createTimeStretcher = () => {
|
|
1667
|
+
let sampleRate = 0;
|
|
1668
|
+
let channelCount = 0;
|
|
1669
|
+
let hop = 0;
|
|
1670
|
+
let frameLen = 0;
|
|
1671
|
+
let seek = 0;
|
|
1672
|
+
let weights = new Float32Array(0);
|
|
1673
|
+
let buffers = [];
|
|
1674
|
+
let inputStart = 0;
|
|
1675
|
+
let analysisPos = 0;
|
|
1676
|
+
let olaTail = [];
|
|
1677
|
+
let naturalRef = null;
|
|
1678
|
+
const init = (rate, channels) => {
|
|
1679
|
+
sampleRate = rate;
|
|
1680
|
+
channelCount = channels;
|
|
1681
|
+
hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
|
|
1682
|
+
frameLen = hop * 2;
|
|
1683
|
+
seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
|
|
1684
|
+
weights = makeHann(frameLen);
|
|
1685
|
+
buffers = Array.from({ length: channels }, () => new Float32Array(0));
|
|
1686
|
+
olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
|
|
1687
|
+
inputStart = 0;
|
|
1688
|
+
analysisPos = seek;
|
|
1689
|
+
naturalRef = null;
|
|
1690
|
+
};
|
|
1691
|
+
const reset = () => {
|
|
1692
|
+
buffers = buffers.map(() => new Float32Array(0));
|
|
1693
|
+
olaTail = olaTail.map(() => new Float32Array(hop));
|
|
1694
|
+
inputStart = 0;
|
|
1695
|
+
analysisPos = seek;
|
|
1696
|
+
naturalRef = null;
|
|
1697
|
+
};
|
|
1698
|
+
const append = (input) => {
|
|
1699
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1700
|
+
const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
|
|
1701
|
+
const existing = buffers[channel] ?? new Float32Array(0);
|
|
1702
|
+
const merged = new Float32Array(existing.length + incoming.length);
|
|
1703
|
+
merged.set(existing, 0);
|
|
1704
|
+
merged.set(incoming, existing.length);
|
|
1705
|
+
buffers[channel] = merged;
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
|
|
1709
|
+
const compact = () => {
|
|
1710
|
+
const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
|
|
1711
|
+
if (keepFrom <= inputStart)
|
|
1712
|
+
return;
|
|
1713
|
+
const drop = keepFrom - inputStart;
|
|
1714
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1715
|
+
buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
|
|
1716
|
+
}
|
|
1717
|
+
inputStart = keepFrom;
|
|
1718
|
+
};
|
|
1719
|
+
const bestOffset = (center) => {
|
|
1720
|
+
if (!naturalRef)
|
|
1721
|
+
return 0;
|
|
1722
|
+
const [base] = buffers;
|
|
1723
|
+
if (!base)
|
|
1724
|
+
return 0;
|
|
1725
|
+
let bestDelta = 0;
|
|
1726
|
+
let bestScore = -Infinity;
|
|
1727
|
+
for (let delta = -seek;delta <= seek; delta += 1) {
|
|
1728
|
+
const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
|
|
1729
|
+
if (score <= bestScore)
|
|
1730
|
+
continue;
|
|
1731
|
+
bestScore = score;
|
|
1732
|
+
bestDelta = delta;
|
|
1733
|
+
}
|
|
1734
|
+
return bestDelta;
|
|
1735
|
+
};
|
|
1736
|
+
const process = (input, speed, rate) => {
|
|
1737
|
+
const channels = Math.max(1, input.length);
|
|
1738
|
+
if (sampleRate !== rate || channelCount !== channels)
|
|
1739
|
+
init(rate, channels);
|
|
1740
|
+
append(input);
|
|
1741
|
+
const analysisHop = hop * speed;
|
|
1742
|
+
const segments = Array.from({ length: channelCount }, () => []);
|
|
1743
|
+
const emitGrain = (pos) => {
|
|
1744
|
+
const off = pos - inputStart;
|
|
1745
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1746
|
+
const src = buffers[channel];
|
|
1747
|
+
const tail = olaTail[channel];
|
|
1748
|
+
if (!src || !tail)
|
|
1749
|
+
continue;
|
|
1750
|
+
const grain = overlapAddGrain(src, off, tail, weights, hop);
|
|
1751
|
+
olaTail[channel] = grain.nextTail;
|
|
1752
|
+
segments[channel]?.push(grain.out);
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
const captureRef = (pos) => {
|
|
1756
|
+
const ref = new Float32Array(frameLen);
|
|
1757
|
+
const refOff = pos + hop - inputStart;
|
|
1758
|
+
const [base] = buffers;
|
|
1759
|
+
if (base)
|
|
1760
|
+
ref.set(base.subarray(refOff, refOff + frameLen));
|
|
1761
|
+
naturalRef = ref;
|
|
1762
|
+
};
|
|
1763
|
+
const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
|
|
1764
|
+
while (canEmit()) {
|
|
1765
|
+
const center = Math.round(analysisPos);
|
|
1766
|
+
const pos = center + bestOffset(center);
|
|
1767
|
+
emitGrain(pos);
|
|
1768
|
+
captureRef(pos);
|
|
1769
|
+
analysisPos += analysisHop;
|
|
1770
|
+
}
|
|
1771
|
+
compact();
|
|
1772
|
+
return segments.map((channelSegments) => {
|
|
1773
|
+
const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
|
|
1774
|
+
const merged = new Float32Array(total);
|
|
1775
|
+
let offset = 0;
|
|
1776
|
+
for (const seg of channelSegments) {
|
|
1777
|
+
merged.set(seg, offset);
|
|
1778
|
+
offset += seg.length;
|
|
1779
|
+
}
|
|
1780
|
+
return merged;
|
|
1781
|
+
});
|
|
1782
|
+
};
|
|
1783
|
+
return { process, reset };
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
// src/client/audioPlayer.ts
|
|
1787
|
+
var activeAudioPlayers = new Set;
|
|
1788
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1789
|
+
var DEFAULT_VOLUME = 1;
|
|
1790
|
+
var DEFAULT_PLAYBACK_RATE = 1;
|
|
1791
|
+
var MIN_PLAYBACK_RATE = 0.5;
|
|
1792
|
+
var MAX_PLAYBACK_RATE = 2;
|
|
1793
|
+
var STRETCH_BYPASS_EPSILON = 0.01;
|
|
1794
|
+
var ANALYSER_FFT_SIZE = 256;
|
|
1795
|
+
var PCM_BYTE_MIDPOINT = 128;
|
|
1796
|
+
var GAP_EPSILON_S = 0.03;
|
|
1797
|
+
var createInitialState3 = () => ({
|
|
1798
|
+
activeSourceCount: 0,
|
|
1799
|
+
error: null,
|
|
1800
|
+
isActive: false,
|
|
1801
|
+
isPlaying: false,
|
|
1802
|
+
lastInterruptLatencyMs: undefined,
|
|
1803
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
1804
|
+
processedChunkCount: 0,
|
|
1805
|
+
queuedChunkCount: 0
|
|
1806
|
+
});
|
|
1807
|
+
var getAudioContextCtor = () => {
|
|
1808
|
+
if (typeof window === "undefined") {
|
|
1809
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
1810
|
+
}
|
|
1811
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
1812
|
+
};
|
|
1813
|
+
var clampVolume = (volume) => {
|
|
1814
|
+
if (typeof volume !== "number" || !Number.isFinite(volume)) {
|
|
1815
|
+
return DEFAULT_VOLUME;
|
|
1816
|
+
}
|
|
1817
|
+
return Math.min(1, Math.max(0, volume));
|
|
1818
|
+
};
|
|
1819
|
+
var clampPlaybackRate = (rate) => {
|
|
1820
|
+
if (typeof rate !== "number" || !Number.isFinite(rate)) {
|
|
1821
|
+
return DEFAULT_PLAYBACK_RATE;
|
|
1822
|
+
}
|
|
1823
|
+
return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
|
|
1824
|
+
};
|
|
1825
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
1826
|
+
const { format } = chunk;
|
|
1827
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
1828
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
1829
|
+
}
|
|
1830
|
+
const bytes = chunk.chunk;
|
|
1831
|
+
const channels = Math.max(1, format.channels);
|
|
1832
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
1833
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
1834
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
1835
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1836
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1837
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
1838
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
1839
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
1840
|
+
const sampleOffset = sampleIndex * 2;
|
|
1841
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
1842
|
+
channelData[frameIndex] = 0;
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return audioBuffer;
|
|
1849
|
+
};
|
|
1850
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
1851
|
+
const subscribers = new Set;
|
|
1852
|
+
const sourceNodes = new Set;
|
|
1853
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
1854
|
+
let state = createInitialState3();
|
|
1855
|
+
let audioContext = null;
|
|
1856
|
+
let outputNode = null;
|
|
1857
|
+
let analyserNode = null;
|
|
1858
|
+
let analyserBuffer = null;
|
|
1859
|
+
let volume = clampVolume(options.volume);
|
|
1860
|
+
let playbackRate = clampPlaybackRate(options.playbackRate);
|
|
1861
|
+
let stretcher = null;
|
|
1862
|
+
let queueEndTime = 0;
|
|
1863
|
+
let syncPromise = Promise.resolve();
|
|
1864
|
+
let interruptStartedAt = null;
|
|
1865
|
+
let interruptPromise = null;
|
|
1866
|
+
let resolveInterruptPromise = null;
|
|
1867
|
+
let interruptFallbackTimer = null;
|
|
1868
|
+
let concurrencyCounted = false;
|
|
1869
|
+
let observedPeakConcurrency = 0;
|
|
1870
|
+
let scheduledDurationMs = 0;
|
|
1871
|
+
let gapCount = 0;
|
|
1872
|
+
let totalGapMs = 0;
|
|
1873
|
+
let maxGapMs = 0;
|
|
1874
|
+
let errorCount = 0;
|
|
1875
|
+
let hasScheduledAnyChunk = false;
|
|
1876
|
+
const sampleConcurrency = () => {
|
|
1877
|
+
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
1878
|
+
observedPeakConcurrency = activeAudioPlayers.size;
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
const markPlayerActive = () => {
|
|
1882
|
+
if (concurrencyCounted) {
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
concurrencyCounted = true;
|
|
1886
|
+
activeAudioPlayers.add(sampleConcurrency);
|
|
1887
|
+
for (const sample of activeAudioPlayers) {
|
|
1888
|
+
sample();
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
const markPlayerInactive = () => {
|
|
1892
|
+
if (!concurrencyCounted) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
concurrencyCounted = false;
|
|
1896
|
+
activeAudioPlayers.delete(sampleConcurrency);
|
|
1897
|
+
};
|
|
1898
|
+
const notify = () => {
|
|
1899
|
+
for (const subscriber of subscribers) {
|
|
1900
|
+
subscriber();
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
const setState = (next) => {
|
|
1904
|
+
state = {
|
|
1905
|
+
...state,
|
|
1906
|
+
...next
|
|
1907
|
+
};
|
|
1908
|
+
notify();
|
|
1909
|
+
};
|
|
1910
|
+
const clearError = () => {
|
|
1911
|
+
if (state.error !== null) {
|
|
1912
|
+
setState({ error: null });
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
const clearInterruptTimer = () => {
|
|
1916
|
+
if (interruptFallbackTimer !== null) {
|
|
1917
|
+
clearTimeout(interruptFallbackTimer);
|
|
1918
|
+
interruptFallbackTimer = null;
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
const resolveInterrupt = (latencyMs) => {
|
|
1922
|
+
clearInterruptTimer();
|
|
1923
|
+
interruptStartedAt = null;
|
|
1924
|
+
stretcher?.reset();
|
|
1925
|
+
setState({
|
|
1926
|
+
activeSourceCount: sourceNodes.size,
|
|
1927
|
+
isPlaying: false,
|
|
1928
|
+
lastInterruptLatencyMs: latencyMs,
|
|
1929
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
1930
|
+
});
|
|
1931
|
+
resolveInterruptPromise?.();
|
|
1932
|
+
resolveInterruptPromise = null;
|
|
1933
|
+
interruptPromise = null;
|
|
1934
|
+
};
|
|
1935
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
1936
|
+
if (!context) {
|
|
1937
|
+
return 0;
|
|
1938
|
+
}
|
|
1939
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
1940
|
+
};
|
|
1941
|
+
const applyOutputGain = (context) => {
|
|
1942
|
+
if (!outputNode) {
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
const gainValue = volume;
|
|
1946
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1947
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
outputNode.gain.value = gainValue;
|
|
1951
|
+
};
|
|
1952
|
+
const muteOutputGain = (context) => {
|
|
1953
|
+
if (!outputNode) {
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const gainValue = 0;
|
|
1957
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1958
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
outputNode.gain.value = gainValue;
|
|
1962
|
+
};
|
|
1963
|
+
const maybeResolveInterrupt = () => {
|
|
1964
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
1968
|
+
};
|
|
1969
|
+
const ensureAudioContext = async () => {
|
|
1970
|
+
if (audioContext) {
|
|
1971
|
+
return audioContext;
|
|
1972
|
+
}
|
|
1973
|
+
if (options.createAudioContext) {
|
|
1974
|
+
audioContext = options.createAudioContext();
|
|
1975
|
+
} else {
|
|
1976
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
1977
|
+
if (!AudioContextCtor) {
|
|
1978
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
1979
|
+
}
|
|
1980
|
+
audioContext = new AudioContextCtor;
|
|
1981
|
+
}
|
|
1982
|
+
if (audioContext.createGain) {
|
|
1983
|
+
outputNode = audioContext.createGain();
|
|
1984
|
+
outputNode.connect?.(audioContext.destination);
|
|
1985
|
+
if (audioContext.createAnalyser) {
|
|
1986
|
+
analyserNode = audioContext.createAnalyser();
|
|
1987
|
+
analyserNode.fftSize = ANALYSER_FFT_SIZE;
|
|
1988
|
+
analyserBuffer = new Uint8Array(analyserNode.fftSize);
|
|
1989
|
+
outputNode.connect?.(analyserNode);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
queueEndTime = audioContext.currentTime;
|
|
1993
|
+
return audioContext;
|
|
1994
|
+
};
|
|
1995
|
+
const scheduleBuffer = (context, buffer, rate) => {
|
|
1996
|
+
const node = context.createBufferSource();
|
|
1997
|
+
node.buffer = buffer;
|
|
1998
|
+
if (node.playbackRate) {
|
|
1999
|
+
node.playbackRate.value = rate;
|
|
2000
|
+
}
|
|
2001
|
+
node.connect(outputNode ?? context.destination);
|
|
2002
|
+
node.onended = () => {
|
|
2003
|
+
sourceNodes.delete(node);
|
|
2004
|
+
node.disconnect?.();
|
|
2005
|
+
setState({
|
|
2006
|
+
activeSourceCount: sourceNodes.size,
|
|
2007
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
2008
|
+
});
|
|
2009
|
+
maybeResolveInterrupt();
|
|
2010
|
+
};
|
|
2011
|
+
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
2012
|
+
const startAt = Math.max(earliestStart, queueEndTime);
|
|
2013
|
+
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
2014
|
+
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
2015
|
+
gapCount += 1;
|
|
2016
|
+
totalGapMs += gapMs;
|
|
2017
|
+
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
2018
|
+
}
|
|
2019
|
+
hasScheduledAnyChunk = true;
|
|
2020
|
+
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
2021
|
+
sampleConcurrency();
|
|
2022
|
+
queueEndTime = startAt + buffer.duration / rate;
|
|
2023
|
+
sourceNodes.add(node);
|
|
2024
|
+
setState({
|
|
2025
|
+
activeSourceCount: sourceNodes.size,
|
|
2026
|
+
isPlaying: true
|
|
2027
|
+
});
|
|
2028
|
+
node.start(startAt);
|
|
2029
|
+
};
|
|
2030
|
+
const scheduleChunk = async (chunk) => {
|
|
2031
|
+
const context = await ensureAudioContext();
|
|
2032
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
2033
|
+
if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
|
|
2034
|
+
stretcher?.reset();
|
|
2035
|
+
scheduleBuffer(context, buffer, playbackRate);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const channels = Math.max(1, chunk.format.channels);
|
|
2039
|
+
const input = [];
|
|
2040
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2041
|
+
input.push(buffer.getChannelData(channelIndex));
|
|
2042
|
+
}
|
|
2043
|
+
stretcher ??= createTimeStretcher();
|
|
2044
|
+
const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
|
|
2045
|
+
const outLength = stretched[0]?.length ?? 0;
|
|
2046
|
+
if (outLength === 0) {
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
|
|
2050
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
2051
|
+
const channelOut = stretched[channelIndex];
|
|
2052
|
+
if (!channelOut)
|
|
2053
|
+
continue;
|
|
2054
|
+
outBuffer.getChannelData(channelIndex).set(channelOut);
|
|
2055
|
+
}
|
|
2056
|
+
scheduleBuffer(context, outBuffer, 1);
|
|
2057
|
+
};
|
|
2058
|
+
const stopQueuedPlayback = (options2) => {
|
|
2059
|
+
for (const node of [...sourceNodes]) {
|
|
2060
|
+
node.stop?.();
|
|
2061
|
+
}
|
|
2062
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
2063
|
+
if (options2?.forceClear) {
|
|
2064
|
+
for (const node of sourceNodes) {
|
|
2065
|
+
node.disconnect?.();
|
|
2066
|
+
}
|
|
2067
|
+
sourceNodes.clear();
|
|
2068
|
+
maybeResolveInterrupt();
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
const sync = async () => {
|
|
2072
|
+
if (!state.isActive) {
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
2076
|
+
if (nextChunks.length === 0) {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
clearError();
|
|
2081
|
+
for (const chunk of nextChunks) {
|
|
2082
|
+
await scheduleChunk(chunk);
|
|
2083
|
+
}
|
|
2084
|
+
setState({
|
|
2085
|
+
processedChunkCount: source.assistantAudio.length,
|
|
2086
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
2087
|
+
});
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
errorCount += 1;
|
|
2090
|
+
setState({
|
|
2091
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
const queueSync = () => {
|
|
2096
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
2097
|
+
return syncPromise;
|
|
2098
|
+
};
|
|
2099
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
2100
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
2101
|
+
player.start();
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (state.isActive) {
|
|
2105
|
+
queueSync();
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
const player = {
|
|
2109
|
+
get activeSourceCount() {
|
|
2110
|
+
return state.activeSourceCount;
|
|
1094
2111
|
},
|
|
1095
|
-
|
|
1096
|
-
|
|
2112
|
+
close: async () => {
|
|
2113
|
+
markPlayerInactive();
|
|
2114
|
+
unsubscribeSource();
|
|
2115
|
+
stopQueuedPlayback({ forceClear: true });
|
|
2116
|
+
clearInterruptTimer();
|
|
2117
|
+
resolveInterruptPromise?.();
|
|
2118
|
+
resolveInterruptPromise = null;
|
|
2119
|
+
interruptPromise = null;
|
|
2120
|
+
interruptStartedAt = null;
|
|
2121
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
2122
|
+
await audioContext.close();
|
|
2123
|
+
}
|
|
2124
|
+
audioContext = null;
|
|
2125
|
+
outputNode?.disconnect?.();
|
|
2126
|
+
outputNode = null;
|
|
2127
|
+
analyserNode?.disconnect?.();
|
|
2128
|
+
analyserNode = null;
|
|
2129
|
+
analyserBuffer = null;
|
|
2130
|
+
queueEndTime = 0;
|
|
2131
|
+
setState({
|
|
2132
|
+
activeSourceCount: 0,
|
|
2133
|
+
isActive: false,
|
|
2134
|
+
isPlaying: false
|
|
2135
|
+
});
|
|
1097
2136
|
},
|
|
1098
|
-
get
|
|
1099
|
-
return state.
|
|
2137
|
+
get error() {
|
|
2138
|
+
return state.error;
|
|
1100
2139
|
},
|
|
1101
|
-
|
|
1102
|
-
|
|
2140
|
+
getOutputLevel: () => {
|
|
2141
|
+
if (!analyserNode || !analyserBuffer) {
|
|
2142
|
+
return 0;
|
|
2143
|
+
}
|
|
2144
|
+
analyserNode.getByteTimeDomainData(analyserBuffer);
|
|
2145
|
+
let sumSquares = 0;
|
|
2146
|
+
for (const sample of analyserBuffer) {
|
|
2147
|
+
const centered = (sample - PCM_BYTE_MIDPOINT) / PCM_BYTE_MIDPOINT;
|
|
2148
|
+
sumSquares += centered * centered;
|
|
2149
|
+
}
|
|
2150
|
+
return Math.sqrt(sumSquares / analyserBuffer.length);
|
|
2151
|
+
},
|
|
2152
|
+
getSnapshot: () => state,
|
|
2153
|
+
getIntegritySummary: () => {
|
|
2154
|
+
const chunksReceived = source.assistantAudio.length;
|
|
2155
|
+
const chunksScheduled = state.processedChunkCount;
|
|
2156
|
+
return {
|
|
2157
|
+
chunksReceived,
|
|
2158
|
+
chunksScheduled,
|
|
2159
|
+
errorCount,
|
|
2160
|
+
gapCount,
|
|
2161
|
+
maxConcurrentPlayers: observedPeakConcurrency,
|
|
2162
|
+
maxGapMs: Math.round(maxGapMs),
|
|
2163
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
|
|
2164
|
+
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
2165
|
+
totalGapMs: Math.round(totalGapMs)
|
|
2166
|
+
};
|
|
2167
|
+
},
|
|
2168
|
+
interrupt: async () => {
|
|
2169
|
+
const startedAt = Date.now();
|
|
2170
|
+
const context = await ensureAudioContext();
|
|
2171
|
+
interruptStartedAt = startedAt;
|
|
2172
|
+
muteOutputGain(context);
|
|
2173
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
2174
|
+
setState({
|
|
2175
|
+
isActive: false,
|
|
2176
|
+
isPlaying: sourceNodes.size > 0,
|
|
2177
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
2178
|
+
});
|
|
2179
|
+
if (sourceNodes.size === 0) {
|
|
2180
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (!interruptPromise) {
|
|
2184
|
+
interruptPromise = new Promise((resolve) => {
|
|
2185
|
+
resolveInterruptPromise = resolve;
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
clearInterruptTimer();
|
|
2189
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
2190
|
+
for (const node of sourceNodes) {
|
|
2191
|
+
node.disconnect?.();
|
|
2192
|
+
}
|
|
2193
|
+
sourceNodes.clear();
|
|
2194
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
2195
|
+
}, 250);
|
|
2196
|
+
stopQueuedPlayback();
|
|
2197
|
+
await interruptPromise;
|
|
2198
|
+
},
|
|
2199
|
+
get isActive() {
|
|
2200
|
+
return state.isActive;
|
|
2201
|
+
},
|
|
2202
|
+
get isPlaying() {
|
|
2203
|
+
return state.isPlaying;
|
|
2204
|
+
},
|
|
2205
|
+
get lastInterruptLatencyMs() {
|
|
2206
|
+
return state.lastInterruptLatencyMs;
|
|
2207
|
+
},
|
|
2208
|
+
get lastPlaybackStopLatencyMs() {
|
|
2209
|
+
return state.lastPlaybackStopLatencyMs;
|
|
2210
|
+
},
|
|
2211
|
+
pause: async () => {
|
|
2212
|
+
if (!audioContext) {
|
|
2213
|
+
setState({
|
|
2214
|
+
activeSourceCount: 0,
|
|
2215
|
+
isActive: false,
|
|
2216
|
+
isPlaying: false
|
|
2217
|
+
});
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
await audioContext.suspend();
|
|
2221
|
+
markPlayerInactive();
|
|
2222
|
+
setState({
|
|
2223
|
+
activeSourceCount: sourceNodes.size,
|
|
2224
|
+
isActive: false,
|
|
2225
|
+
isPlaying: false
|
|
2226
|
+
});
|
|
2227
|
+
},
|
|
2228
|
+
get playbackRate() {
|
|
2229
|
+
return playbackRate;
|
|
2230
|
+
},
|
|
2231
|
+
get processedChunkCount() {
|
|
2232
|
+
return state.processedChunkCount;
|
|
2233
|
+
},
|
|
2234
|
+
get queuedChunkCount() {
|
|
2235
|
+
return state.queuedChunkCount;
|
|
2236
|
+
},
|
|
2237
|
+
setPlaybackRate: (nextRate) => {
|
|
2238
|
+
playbackRate = clampPlaybackRate(nextRate);
|
|
2239
|
+
},
|
|
2240
|
+
setVolume: (nextVolume) => {
|
|
2241
|
+
volume = clampVolume(nextVolume);
|
|
2242
|
+
applyOutputGain(audioContext);
|
|
2243
|
+
},
|
|
2244
|
+
start: async () => {
|
|
2245
|
+
try {
|
|
2246
|
+
clearError();
|
|
2247
|
+
const context = await ensureAudioContext();
|
|
2248
|
+
applyOutputGain(context);
|
|
2249
|
+
if (context.state === "suspended") {
|
|
2250
|
+
await context.resume();
|
|
2251
|
+
}
|
|
2252
|
+
markPlayerActive();
|
|
2253
|
+
setState({
|
|
2254
|
+
activeSourceCount: sourceNodes.size,
|
|
2255
|
+
isActive: true,
|
|
2256
|
+
isPlaying: context.state === "running"
|
|
2257
|
+
});
|
|
2258
|
+
await queueSync();
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
errorCount += 1;
|
|
2261
|
+
markPlayerInactive();
|
|
2262
|
+
setState({
|
|
2263
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2264
|
+
isActive: false,
|
|
2265
|
+
isPlaying: false
|
|
2266
|
+
});
|
|
2267
|
+
throw error;
|
|
2268
|
+
}
|
|
2269
|
+
},
|
|
2270
|
+
subscribe: (subscriber) => {
|
|
2271
|
+
subscribers.add(subscriber);
|
|
2272
|
+
return () => {
|
|
2273
|
+
subscribers.delete(subscriber);
|
|
2274
|
+
};
|
|
2275
|
+
},
|
|
2276
|
+
get volume() {
|
|
2277
|
+
return volume;
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
return player;
|
|
2281
|
+
};
|
|
2282
|
+
|
|
2283
|
+
// src/client/bargeInMonitor.ts
|
|
2284
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
2285
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
2286
|
+
var summarize = (events, thresholdMs) => {
|
|
2287
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
2288
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
2289
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
2290
|
+
const passed = stopped.length - failed;
|
|
2291
|
+
return {
|
|
2292
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
2293
|
+
events: [...events],
|
|
2294
|
+
failed,
|
|
2295
|
+
lastEvent: events.at(-1),
|
|
2296
|
+
passed,
|
|
2297
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
2298
|
+
thresholdMs,
|
|
2299
|
+
total: stopped.length
|
|
2300
|
+
};
|
|
2301
|
+
};
|
|
2302
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
2303
|
+
const listeners = new Set;
|
|
2304
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
2305
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2306
|
+
const events = [];
|
|
2307
|
+
const emit = () => {
|
|
2308
|
+
for (const listener of listeners) {
|
|
2309
|
+
listener();
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
const postEvent = (event) => {
|
|
2313
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
fetchImpl(options.path, {
|
|
2317
|
+
body: JSON.stringify(event),
|
|
2318
|
+
headers: {
|
|
2319
|
+
"Content-Type": "application/json"
|
|
2320
|
+
},
|
|
2321
|
+
method: "POST"
|
|
2322
|
+
}).catch(() => {});
|
|
2323
|
+
};
|
|
2324
|
+
const record = (status, input) => {
|
|
2325
|
+
const event = {
|
|
2326
|
+
at: Date.now(),
|
|
2327
|
+
id: createEventId(),
|
|
2328
|
+
latencyMs: input.latencyMs,
|
|
2329
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
2330
|
+
reason: input.reason,
|
|
2331
|
+
sessionId: input.sessionId,
|
|
2332
|
+
status,
|
|
2333
|
+
thresholdMs
|
|
2334
|
+
};
|
|
2335
|
+
events.push(event);
|
|
2336
|
+
postEvent(event);
|
|
2337
|
+
emit();
|
|
2338
|
+
return event;
|
|
2339
|
+
};
|
|
2340
|
+
return {
|
|
2341
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
2342
|
+
recordRequested: (input) => record("requested", input),
|
|
2343
|
+
recordSkipped: (input) => record("skipped", input),
|
|
2344
|
+
recordStopped: (input) => record("stopped", input),
|
|
2345
|
+
subscribe: (subscriber) => {
|
|
2346
|
+
listeners.add(subscriber);
|
|
2347
|
+
return () => {
|
|
2348
|
+
listeners.delete(subscriber);
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
// src/client/duplex.ts
|
|
2355
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
2356
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
2357
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
2358
|
+
let lastPartial = controller.partial;
|
|
2359
|
+
const interruptIfPlaying = (reason) => {
|
|
2360
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
2361
|
+
options.monitor?.recordSkipped({
|
|
2362
|
+
reason,
|
|
2363
|
+
sessionId: controller.sessionId
|
|
2364
|
+
});
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
options.monitor?.recordRequested({
|
|
2368
|
+
reason,
|
|
2369
|
+
sessionId: controller.sessionId
|
|
2370
|
+
});
|
|
2371
|
+
player.interrupt().then(() => {
|
|
2372
|
+
options.monitor?.recordStopped({
|
|
2373
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
2374
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
2375
|
+
reason,
|
|
2376
|
+
sessionId: controller.sessionId
|
|
2377
|
+
});
|
|
2378
|
+
});
|
|
2379
|
+
};
|
|
2380
|
+
const unsubscribe = controller.subscribe(() => {
|
|
2381
|
+
if (options.interruptOnPartial === false) {
|
|
2382
|
+
lastPartial = controller.partial;
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
if (!lastPartial && controller.partial) {
|
|
2386
|
+
interruptIfPlaying("partial-transcript");
|
|
2387
|
+
}
|
|
2388
|
+
lastPartial = controller.partial;
|
|
2389
|
+
});
|
|
2390
|
+
return {
|
|
2391
|
+
close: () => {
|
|
2392
|
+
unsubscribe();
|
|
2393
|
+
},
|
|
2394
|
+
handleLevel: (level) => {
|
|
2395
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
2396
|
+
interruptIfPlaying("input-level");
|
|
2397
|
+
}
|
|
2398
|
+
},
|
|
2399
|
+
sendAudio: (audio) => {
|
|
2400
|
+
interruptIfPlaying("manual-audio");
|
|
2401
|
+
controller.sendAudio(audio);
|
|
1103
2402
|
}
|
|
1104
2403
|
};
|
|
1105
2404
|
};
|
|
@@ -1126,8 +2425,7 @@ var DEFAULT_GUIDED_PROMPTS = [
|
|
|
1126
2425
|
"Now describe what you are trying to do or test.",
|
|
1127
2426
|
"Finish with any detail that feels blocked, risky, or unclear."
|
|
1128
2427
|
];
|
|
1129
|
-
var clamp = (value, min,
|
|
1130
|
-
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2428
|
+
var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
|
|
1131
2429
|
var readErrorField = (value, key) => {
|
|
1132
2430
|
const candidate = value[key];
|
|
1133
2431
|
if (typeof candidate === "string" && candidate.trim()) {
|
|
@@ -1160,6 +2458,17 @@ var formatErrorMessage = (error) => {
|
|
|
1160
2458
|
}
|
|
1161
2459
|
return "Unexpected error";
|
|
1162
2460
|
};
|
|
2461
|
+
var formatReconnectState = (reconnect) => {
|
|
2462
|
+
const pieces = [reconnect.status];
|
|
2463
|
+
if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
|
|
2464
|
+
pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
|
|
2465
|
+
}
|
|
2466
|
+
if (reconnect.nextAttemptAt) {
|
|
2467
|
+
const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
|
|
2468
|
+
pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
|
|
2469
|
+
}
|
|
2470
|
+
return pieces.join(" · ");
|
|
2471
|
+
};
|
|
1163
2472
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1164
2473
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1165
2474
|
const next = levels.slice(-(count - 1));
|
|
@@ -1216,6 +2525,20 @@ var parsePromptList = (value) => {
|
|
|
1216
2525
|
} catch {}
|
|
1217
2526
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1218
2527
|
};
|
|
2528
|
+
var parseOptionalNumber = (value) => {
|
|
2529
|
+
if (!value) {
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
const parsed = Number(value);
|
|
2533
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2534
|
+
};
|
|
2535
|
+
var resolveElement2 = (root, selector, ctor) => {
|
|
2536
|
+
if (!selector) {
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
const value = document.querySelector(selector);
|
|
2540
|
+
return value instanceof ctor ? value : null;
|
|
2541
|
+
};
|
|
1219
2542
|
var requireElement = (root, selector, ctor, name) => {
|
|
1220
2543
|
const value = selector ? document.querySelector(selector) : null;
|
|
1221
2544
|
if (value instanceof ctor) {
|
|
@@ -1266,11 +2589,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1266
2589
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1267
2590
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1268
2591
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
2592
|
+
const reconnectReportPath = root.dataset.voiceReconnectReportPath;
|
|
2593
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
2594
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
2595
|
+
path: bargeInPath,
|
|
2596
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
2597
|
+
}) : null;
|
|
2598
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
2599
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1269
2600
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1270
2601
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1271
2602
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1272
2603
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1273
2604
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
2605
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1274
2606
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1275
2607
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1276
2608
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1279,35 +2611,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1279
2611
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1280
2612
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1281
2613
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
2614
|
+
let activeMode = null;
|
|
2615
|
+
let hasStartedModes = {
|
|
2616
|
+
general: false,
|
|
2617
|
+
guided: false
|
|
2618
|
+
};
|
|
2619
|
+
let isCapturing = false;
|
|
2620
|
+
let micError = null;
|
|
2621
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
2622
|
+
let guidedBargeInBinding = null;
|
|
2623
|
+
let generalBargeInBinding = null;
|
|
1282
2624
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1283
2625
|
capture: {
|
|
2626
|
+
onAudio: (audio, sendAudio) => {
|
|
2627
|
+
if (guidedBargeInBinding) {
|
|
2628
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
sendAudio(audio);
|
|
2632
|
+
},
|
|
1284
2633
|
onLevel: (level) => {
|
|
2634
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1285
2635
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1286
2636
|
renderWave();
|
|
1287
2637
|
}
|
|
1288
2638
|
},
|
|
2639
|
+
connection: {
|
|
2640
|
+
reconnectReportPath
|
|
2641
|
+
},
|
|
1289
2642
|
preset: "guided-intake"
|
|
1290
2643
|
});
|
|
1291
2644
|
const generalVoice = createVoiceController(generalPath, {
|
|
1292
2645
|
capture: {
|
|
2646
|
+
onAudio: (audio, sendAudio) => {
|
|
2647
|
+
if (generalBargeInBinding) {
|
|
2648
|
+
generalBargeInBinding.sendAudio(audio);
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
sendAudio(audio);
|
|
2652
|
+
},
|
|
1293
2653
|
onLevel: (level) => {
|
|
2654
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1294
2655
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1295
2656
|
renderWave();
|
|
1296
2657
|
}
|
|
1297
2658
|
},
|
|
2659
|
+
connection: {
|
|
2660
|
+
reconnectReportPath
|
|
2661
|
+
},
|
|
1298
2662
|
preset: "dictation"
|
|
1299
2663
|
});
|
|
1300
2664
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1301
2665
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2666
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
2667
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
2668
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
2669
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2670
|
+
monitor: bargeInMonitor ?? undefined
|
|
2671
|
+
});
|
|
2672
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
2673
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2674
|
+
monitor: bargeInMonitor ?? undefined
|
|
2675
|
+
});
|
|
1310
2676
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
2677
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1311
2678
|
const renderWave = () => {
|
|
1312
2679
|
const path = createVoiceWavePath(waveLevels);
|
|
1313
2680
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1319,9 +2686,12 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1319
2686
|
const render = () => {
|
|
1320
2687
|
const voice = currentVoice();
|
|
1321
2688
|
const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
|
|
1322
|
-
const status = voice
|
|
2689
|
+
const { status } = voice;
|
|
1323
2690
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1324
2691
|
errorStatus.textContent = micError || voice.error || "None";
|
|
2692
|
+
if (reconnectStatus) {
|
|
2693
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2694
|
+
}
|
|
1325
2695
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1326
2696
|
promptStatus.textContent = resolvePromptMessage({
|
|
1327
2697
|
guidedPrompts,
|
|
@@ -1385,8 +2755,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1385
2755
|
render();
|
|
1386
2756
|
}
|
|
1387
2757
|
};
|
|
1388
|
-
guidedVoice.subscribe(
|
|
1389
|
-
|
|
2758
|
+
guidedVoice.subscribe(() => {
|
|
2759
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
2760
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
2761
|
+
}
|
|
2762
|
+
render();
|
|
2763
|
+
});
|
|
2764
|
+
generalVoice.subscribe(() => {
|
|
2765
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
2766
|
+
generalAudioPlayer.start().catch(() => {});
|
|
2767
|
+
}
|
|
2768
|
+
render();
|
|
2769
|
+
});
|
|
1390
2770
|
startGuidedButton.addEventListener("click", () => {
|
|
1391
2771
|
startMode("guided");
|
|
1392
2772
|
});
|
|
@@ -1396,9 +2776,16 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1396
2776
|
stopButton.addEventListener("click", () => {
|
|
1397
2777
|
stopMic();
|
|
1398
2778
|
});
|
|
2779
|
+
root.addEventListener("absolute-voice-simulate-disconnect", () => {
|
|
2780
|
+
currentVoice().simulateDisconnect();
|
|
2781
|
+
});
|
|
1399
2782
|
window.addEventListener("beforeunload", () => {
|
|
1400
2783
|
guidedVoice.stopRecording();
|
|
1401
2784
|
generalVoice.stopRecording();
|
|
2785
|
+
guidedBargeInBinding?.close();
|
|
2786
|
+
generalBargeInBinding?.close();
|
|
2787
|
+
guidedAudioPlayer.close();
|
|
2788
|
+
generalAudioPlayer.close();
|
|
1402
2789
|
stopGuidedBinding();
|
|
1403
2790
|
stopGeneralBinding();
|
|
1404
2791
|
guidedVoice.close();
|