@absolutejs/voice 0.0.22-beta.58 → 0.0.22-beta.581
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 +4543 -1181
- package/dist/angular/index.d.ts +36 -5
- package/dist/angular/index.js +4229 -393
- 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 +11 -0
- package/dist/angular/voice-session-observability.service.d.ts +12 -0
- package/dist/angular/voice-session-snapshot.service.d.ts +13 -0
- package/dist/angular/voice-stream.service.d.ts +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 +5 -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 +4 -3
- 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 +1279 -77
- package/dist/client/htmxDashboardRenderers.d.ts +71 -0
- package/dist/client/index.d.ts +107 -15
- package/dist/client/index.js +10742 -254
- 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 +33 -0
- package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
- package/dist/client/providerStatus.d.ts +5 -3
- package/dist/client/providerStatusWidget.d.ts +32 -0
- package/dist/client/reactiveSource.d.ts +8 -0
- package/dist/client/readinessFailures.d.ts +21 -0
- package/dist/client/readinessFailuresWidget.d.ts +42 -0
- package/dist/client/reconnectProfileEvidence.d.ts +21 -0
- package/dist/client/reconnectProfileEvidenceWidget.d.ts +39 -0
- package/dist/client/replayTimeline.d.ts +26 -0
- package/dist/client/routingStatus.d.ts +21 -0
- package/dist/client/routingStatusWidget.d.ts +32 -0
- package/dist/client/sessionObservability.d.ts +21 -0
- package/dist/client/sessionObservabilityWidget.d.ts +31 -0
- package/dist/client/sessionSnapshot.d.ts +23 -0
- package/dist/client/sessionSnapshotWidget.d.ts +33 -0
- package/dist/client/store.d.ts +1 -1
- package/dist/client/timeStretch.d.ts +5 -0
- package/dist/client/traceTimeline.d.ts +21 -0
- package/dist/client/traceTimelineWidget.d.ts +36 -0
- package/dist/client/turnLatency.d.ts +24 -0
- package/dist/client/turnLatencyWidget.d.ts +33 -0
- package/dist/client/turnQuality.d.ts +21 -0
- package/dist/client/turnQualityWidget.d.ts +32 -0
- package/dist/client/voiceWidgetView.d.ts +48 -0
- package/dist/client/workflowStatus.d.ts +5 -3
- package/dist/{agent.d.ts → core/agent.d.ts} +83 -6
- package/dist/core/agentPerformanceReport.d.ts +40 -0
- package/dist/core/agentSquadContract.d.ts +98 -0
- package/dist/core/agentState.d.ts +12 -0
- package/dist/core/agentTools.d.ts +132 -0
- package/dist/core/aiScorecard.d.ts +32 -0
- package/dist/core/aiVoiceModel.d.ts +15 -0
- package/dist/core/amdDetector.d.ts +25 -0
- package/dist/{assistant.d.ts → core/assistant.d.ts} +14 -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 +24 -0
- package/dist/core/bargeInRoutes.d.ts +56 -0
- package/dist/core/bookingFlow.d.ts +43 -0
- package/dist/core/browserCallProfiles.d.ts +120 -0
- package/dist/core/browserMediaRoutes.d.ts +62 -0
- package/dist/core/cachedTTS.d.ts +54 -0
- package/dist/core/calendarAdapter.d.ts +47 -0
- package/dist/core/calendarSlots.d.ts +35 -0
- package/dist/core/callDebugger.d.ts +66 -0
- package/dist/core/callDisposition.d.ts +38 -0
- package/dist/core/callQuota.d.ts +54 -0
- package/dist/core/callScorecard.d.ts +53 -0
- package/dist/core/callerCRMLinker.d.ts +29 -0
- package/dist/core/callerMemory.d.ts +37 -0
- package/dist/core/callingWindow.d.ts +26 -0
- package/dist/core/campaign.d.ts +795 -0
- package/dist/core/campaignControls.d.ts +37 -0
- package/dist/core/campaignDialers.d.ts +111 -0
- package/dist/core/campaignTemplate.d.ts +16 -0
- package/dist/core/competitiveCoverage.d.ts +141 -0
- package/dist/core/conversationSimulator.d.ts +73 -0
- package/dist/{correction.d.ts → core/correction.d.ts} +5 -4
- package/dist/core/costAccounting.d.ts +76 -0
- package/dist/core/costPredictor.d.ts +74 -0
- package/dist/core/crmCallLogger.d.ts +37 -0
- package/dist/core/crmContract.d.ts +70 -0
- package/dist/core/dataControl.d.ts +180 -0
- package/dist/core/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/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} +52 -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/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} +2 -2
- package/dist/core/postCallAnalysis.d.ts +98 -0
- package/dist/core/postCallSurvey.d.ts +41 -0
- package/dist/{postgresStore.d.ts → core/postgresStore.d.ts} +20 -9
- package/dist/{presets.d.ts → core/presets.d.ts} +3 -3
- package/dist/core/productionReadiness.d.ts +757 -0
- package/dist/core/profileSwitchRecommendation.d.ts +350 -0
- package/dist/core/promptInjectionGuard.d.ts +30 -0
- package/dist/core/proofAssertions.d.ts +32 -0
- package/dist/core/proofPack.d.ts +211 -0
- package/dist/core/proofRunner.d.ts +79 -0
- package/dist/core/proofTrends.d.ts +966 -0
- package/dist/{providerAdapters.d.ts → core/providerAdapters.d.ts} +5 -5
- package/dist/core/providerCapabilities.d.ts +92 -0
- 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 +27 -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/core/toolContract.d.ts +161 -0
- package/dist/core/toolRuntime.d.ts +50 -0
- package/dist/{trace.d.ts → core/trace.d.ts} +61 -22
- package/dist/core/traceDeliveryRoutes.d.ts +86 -0
- package/dist/core/traceTimeline.d.ts +97 -0
- package/dist/core/transcriptAnnotator.d.ts +41 -0
- package/dist/{turnDetection.d.ts → core/turnDetection.d.ts} +1 -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/{types.d.ts → core/types.d.ts} +573 -81
- 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/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 +1741 -0
- package/dist/embed/voice-widget.js +10 -0
- package/dist/index.d.ts +373 -74
- package/dist/index.js +46504 -7507
- package/dist/internal/evidence.d.ts +10 -0
- package/dist/internal/html.d.ts +6 -0
- package/dist/internal/status.d.ts +7 -0
- package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
- package/dist/react/VoiceCallDebuggerLaunch.d.ts +6 -0
- package/dist/react/VoiceCallPlayer.d.ts +11 -0
- package/dist/react/VoiceCostDashboard.d.ts +10 -0
- package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
- package/dist/react/VoiceLiveAgentConsole.d.ts +11 -0
- package/dist/react/VoiceLiveCallViewer.d.ts +9 -0
- package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
- package/dist/react/VoiceOpsStatus.d.ts +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 +5 -0
- package/dist/react/VoiceProviderStatus.d.ts +6 -0
- package/dist/react/VoiceReadinessFailures.d.ts +6 -0
- package/dist/react/VoiceReconnectProfileEvidence.d.ts +6 -0
- package/dist/react/VoiceReplayTimeline.d.ts +6 -0
- package/dist/react/VoiceRoutingStatus.d.ts +6 -0
- package/dist/react/VoiceSessionObservability.d.ts +6 -0
- package/dist/react/VoiceSessionSnapshot.d.ts +6 -0
- package/dist/react/VoiceTraceTimeline.d.ts +6 -0
- package/dist/react/VoiceTurnLatency.d.ts +6 -0
- package/dist/react/VoiceTurnQuality.d.ts +6 -0
- package/dist/react/VoiceWidget.d.ts +13 -0
- package/dist/react/index.d.ts +80 -6
- package/dist/react/index.js +12960 -323
- 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 +10 -0
- 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 +8 -0
- 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 +11 -0
- package/dist/svelte/createVoiceProviderStatus.d.ts +5 -3
- package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
- package/dist/svelte/createVoiceReconnectProfileEvidence.d.ts +7 -0
- package/dist/svelte/createVoiceReplayTimeline.d.ts +13 -0
- package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
- package/dist/svelte/createVoiceSessionObservability.d.ts +10 -0
- package/dist/svelte/createVoiceSessionSnapshot.d.ts +11 -0
- package/dist/svelte/createVoiceStream.d.ts +1 -1
- package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
- package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
- package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
- package/dist/svelte/createVoiceWidget.d.ts +19 -0
- package/dist/svelte/createVoiceWorkflowStatus.d.ts +2 -2
- package/dist/svelte/index.d.ts +37 -6
- package/dist/svelte/index.js +7083 -753
- package/dist/telephony/contract.d.ts +61 -0
- package/dist/telephony/matrix.d.ts +97 -0
- package/dist/telephony/plivo.d.ts +303 -0
- package/dist/telephony/response.d.ts +1 -1
- package/dist/telephony/security.d.ts +182 -0
- package/dist/telephony/telnyx.d.ts +291 -0
- package/dist/telephony/twilio.d.ts +152 -15
- package/dist/testing/accuracy.d.ts +1 -1
- package/dist/testing/benchmark.d.ts +15 -15
- package/dist/testing/corrected.d.ts +9 -9
- package/dist/testing/duplex.d.ts +5 -5
- package/dist/testing/fixtures.d.ts +4 -4
- package/dist/testing/index.d.ts +13 -13
- package/dist/testing/index.js +6839 -948
- package/dist/testing/ioProviderSimulator.d.ts +5 -5
- package/dist/testing/providerSimulator.d.ts +5 -5
- package/dist/testing/review.d.ts +8 -8
- package/dist/testing/sessionBenchmark.d.ts +13 -13
- package/dist/testing/stt.d.ts +3 -3
- package/dist/testing/telephony.d.ts +27 -2
- package/dist/testing/tts.d.ts +2 -2
- package/dist/vue/VoiceCallDebuggerLaunch.d.ts +72 -0
- package/dist/vue/VoiceCallPlayer.d.ts +40 -0
- package/dist/vue/VoiceCostDashboard.d.ts +57 -0
- package/dist/vue/VoiceDeliveryRuntime.d.ts +34 -0
- package/dist/vue/VoiceLiveAgentConsole.d.ts +50 -0
- package/dist/vue/VoiceLiveCallViewer.d.ts +35 -0
- package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
- package/dist/vue/VoiceOpsStatus.d.ts +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 +88 -0
- package/dist/vue/VoiceProviderStatus.d.ts +55 -0
- package/dist/vue/VoiceReadinessFailures.d.ts +25 -0
- package/dist/vue/VoiceReconnectProfileEvidence.d.ts +25 -0
- package/dist/vue/VoiceReplayTimeline.d.ts +17 -0
- package/dist/vue/VoiceRoutingStatus.d.ts +55 -0
- package/dist/vue/VoiceSessionObservability.d.ts +27 -0
- package/dist/vue/VoiceSessionSnapshot.d.ts +72 -0
- package/dist/vue/VoiceTurnLatency.d.ts +73 -0
- package/dist/vue/VoiceTurnQuality.d.ts +55 -0
- package/dist/vue/VoiceWidget.d.ts +77 -0
- package/dist/vue/index.d.ts +49 -6
- package/dist/vue/index.js +12265 -417
- package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/vue/useVoiceCallDebugger.d.ts +10 -0
- package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
- package/dist/vue/useVoiceController.d.ts +10 -7
- package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
- package/dist/vue/useVoiceLiveOps.d.ts +9 -0
- package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
- package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
- package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
- package/dist/vue/useVoiceProfileComparison.d.ts +9 -0
- package/dist/vue/useVoiceProofTrends.d.ts +9 -0
- package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
- package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
- package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
- package/dist/vue/useVoiceProviderStatus.d.ts +3 -3
- package/dist/vue/useVoiceReadinessFailures.d.ts +959 -0
- package/dist/vue/useVoiceReconnectProfileEvidence.d.ts +9 -0
- package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/vue/useVoiceSessionObservability.d.ts +9 -0
- package/dist/vue/useVoiceSessionSnapshot.d.ts +10 -0
- package/dist/vue/useVoiceStream.d.ts +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 +155 -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 -92
- 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/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,19 +104,25 @@ var createMicrophoneCapture = (options) => {
|
|
|
101
104
|
let processorNode = null;
|
|
102
105
|
let mediaStream = null;
|
|
103
106
|
const start = async () => {
|
|
104
|
-
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
|
|
107
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
105
108
|
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
106
109
|
}
|
|
107
110
|
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
108
111
|
if (!AudioContextCtor) {
|
|
109
112
|
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
110
113
|
}
|
|
111
|
-
mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
114
|
+
mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
112
115
|
audio: {
|
|
113
|
-
|
|
116
|
+
autoGainControl: true,
|
|
117
|
+
channelCount: options.channelCount ?? 1,
|
|
118
|
+
echoCancellation: true,
|
|
119
|
+
noiseSuppression: true
|
|
114
120
|
}
|
|
115
121
|
});
|
|
116
122
|
audioContext = new AudioContextCtor;
|
|
123
|
+
if (audioContext.state === "suspended") {
|
|
124
|
+
await audioContext.resume();
|
|
125
|
+
}
|
|
117
126
|
sourceNode = audioContext.createMediaStreamSource(mediaStream);
|
|
118
127
|
processorNode = audioContext.createScriptProcessor(4096, 1, 1);
|
|
119
128
|
processorNode.onaudioprocess = (event) => {
|
|
@@ -181,13 +190,25 @@ var serverMessageToAction = (message) => {
|
|
|
181
190
|
case "assistant":
|
|
182
191
|
return {
|
|
183
192
|
text: message.text,
|
|
193
|
+
turnId: message.turnId,
|
|
184
194
|
type: "assistant"
|
|
185
195
|
};
|
|
196
|
+
case "assistant_delta":
|
|
197
|
+
return {
|
|
198
|
+
delta: message.delta,
|
|
199
|
+
turnId: message.turnId,
|
|
200
|
+
type: "assistant_delta"
|
|
201
|
+
};
|
|
186
202
|
case "complete":
|
|
187
203
|
return {
|
|
188
204
|
sessionId: message.sessionId,
|
|
189
205
|
type: "complete"
|
|
190
206
|
};
|
|
207
|
+
case "connection":
|
|
208
|
+
return {
|
|
209
|
+
reconnect: message.reconnect,
|
|
210
|
+
type: "connection"
|
|
211
|
+
};
|
|
191
212
|
case "call_lifecycle":
|
|
192
213
|
return {
|
|
193
214
|
event: message.event,
|
|
@@ -209,9 +230,22 @@ var serverMessageToAction = (message) => {
|
|
|
209
230
|
transcript: message.transcript,
|
|
210
231
|
type: "partial"
|
|
211
232
|
};
|
|
233
|
+
case "replay":
|
|
234
|
+
return {
|
|
235
|
+
assistantTexts: message.assistantTexts,
|
|
236
|
+
call: message.call,
|
|
237
|
+
partial: message.partial,
|
|
238
|
+
scenarioId: message.scenarioId,
|
|
239
|
+
sessionId: message.sessionId,
|
|
240
|
+
sessionMetadata: message.sessionMetadata,
|
|
241
|
+
status: message.status,
|
|
242
|
+
turns: message.turns,
|
|
243
|
+
type: "replay"
|
|
244
|
+
};
|
|
212
245
|
case "session":
|
|
213
246
|
return {
|
|
214
247
|
sessionId: message.sessionId,
|
|
248
|
+
sessionMetadata: message.sessionMetadata,
|
|
215
249
|
scenarioId: message.scenarioId,
|
|
216
250
|
status: message.status,
|
|
217
251
|
type: "session"
|
|
@@ -226,6 +260,233 @@ var serverMessageToAction = (message) => {
|
|
|
226
260
|
}
|
|
227
261
|
};
|
|
228
262
|
|
|
263
|
+
// node_modules/@absolutejs/media/dist/index.js
|
|
264
|
+
var TAU = Math.PI * 2;
|
|
265
|
+
var pushIssue = (issues, severity, code, message) => {
|
|
266
|
+
issues.push({ code, message, severity });
|
|
267
|
+
};
|
|
268
|
+
var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
|
|
269
|
+
var max = (values) => values.length === 0 ? undefined : Math.max(...values);
|
|
270
|
+
var numericStat = (stat, key) => {
|
|
271
|
+
const value = stat[key];
|
|
272
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
273
|
+
};
|
|
274
|
+
var booleanStat = (stat, key) => {
|
|
275
|
+
const value = stat[key];
|
|
276
|
+
return typeof value === "boolean" ? value : undefined;
|
|
277
|
+
};
|
|
278
|
+
var stringStat = (stat, key) => {
|
|
279
|
+
const value = stat[key];
|
|
280
|
+
return typeof value === "string" ? value : undefined;
|
|
281
|
+
};
|
|
282
|
+
var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
|
|
283
|
+
var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
284
|
+
var normalizeWebRTCStat = (stat) => {
|
|
285
|
+
const sample = {};
|
|
286
|
+
for (const [key, value] of Object.entries(stat)) {
|
|
287
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
288
|
+
sample[key] = value;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return sample;
|
|
292
|
+
};
|
|
293
|
+
var buildMediaWebRTCStatsReport = (input = {}) => {
|
|
294
|
+
const stats = input.stats ?? [];
|
|
295
|
+
const issues = [];
|
|
296
|
+
const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
297
|
+
const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
298
|
+
const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
|
|
299
|
+
const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
|
|
300
|
+
const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
|
|
301
|
+
const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
|
|
302
|
+
const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
|
|
303
|
+
const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
|
|
304
|
+
const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
|
|
305
|
+
const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
|
|
306
|
+
const packetLossDenominator = inboundPackets + packetsLost;
|
|
307
|
+
const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
|
|
308
|
+
const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
|
|
309
|
+
const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
|
|
310
|
+
const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
|
|
311
|
+
const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
|
|
312
|
+
const jitterBufferDelayMs = max(inbound.map((stat) => {
|
|
313
|
+
const delay = numericStat(stat, "jitterBufferDelay");
|
|
314
|
+
const emitted = numericStat(stat, "jitterBufferEmittedCount");
|
|
315
|
+
return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
|
|
316
|
+
}).filter((value) => value !== undefined));
|
|
317
|
+
const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
|
|
318
|
+
if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
|
|
319
|
+
pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
|
|
320
|
+
}
|
|
321
|
+
if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
|
|
322
|
+
pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
|
|
323
|
+
}
|
|
324
|
+
if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
|
|
325
|
+
pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
|
|
326
|
+
}
|
|
327
|
+
if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
|
|
328
|
+
pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
|
|
329
|
+
}
|
|
330
|
+
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
331
|
+
pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
activeCandidatePairs,
|
|
335
|
+
audioLevelAverage: average(audioLevels),
|
|
336
|
+
bytesReceived,
|
|
337
|
+
bytesSent,
|
|
338
|
+
checkedAt: Date.now(),
|
|
339
|
+
endedAudioTracks,
|
|
340
|
+
inboundPackets,
|
|
341
|
+
issues,
|
|
342
|
+
jitterBufferDelayMs,
|
|
343
|
+
jitterMs,
|
|
344
|
+
liveAudioTracks,
|
|
345
|
+
outboundPackets,
|
|
346
|
+
packetLossRatio,
|
|
347
|
+
packetsLost,
|
|
348
|
+
roundTripTimeMs,
|
|
349
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
350
|
+
totalStats: stats.length
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
var collectMediaWebRTCStats = async (input) => {
|
|
354
|
+
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
355
|
+
return [...report.values()].map(normalizeWebRTCStat);
|
|
356
|
+
};
|
|
357
|
+
var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
|
|
358
|
+
const stats = input.stats ?? [];
|
|
359
|
+
const previousStats = input.previousStats ?? [];
|
|
360
|
+
const issues = [];
|
|
361
|
+
const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
|
|
362
|
+
const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
|
|
363
|
+
const streams = audioRtp.map((stat) => {
|
|
364
|
+
const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
|
|
365
|
+
const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
|
|
366
|
+
const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
|
|
367
|
+
const previous = previousByKey.get(statKey(stat));
|
|
368
|
+
const currentPackets = numericStat(stat, packetsKey);
|
|
369
|
+
const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
|
|
370
|
+
const currentBytes = numericStat(stat, bytesKey);
|
|
371
|
+
const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
|
|
372
|
+
const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
|
|
373
|
+
return {
|
|
374
|
+
bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
|
|
375
|
+
currentPackets,
|
|
376
|
+
direction,
|
|
377
|
+
id: statKey(stat),
|
|
378
|
+
packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
|
|
379
|
+
previousPackets,
|
|
380
|
+
timeDeltaMs
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
const inbound = streams.filter((stream) => stream.direction === "inbound");
|
|
384
|
+
const outbound = streams.filter((stream) => stream.direction === "outbound");
|
|
385
|
+
const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
|
|
386
|
+
const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
387
|
+
const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
388
|
+
if (input.requireInboundAudio && inbound.length === 0) {
|
|
389
|
+
pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
|
|
390
|
+
}
|
|
391
|
+
if (input.requireOutboundAudio && outbound.length === 0) {
|
|
392
|
+
pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
|
|
393
|
+
}
|
|
394
|
+
if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
|
|
395
|
+
pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
|
|
396
|
+
}
|
|
397
|
+
if (stalledInboundStreams > 0) {
|
|
398
|
+
pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
|
|
399
|
+
}
|
|
400
|
+
if (stalledOutboundStreams > 0) {
|
|
401
|
+
pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
checkedAt: Date.now(),
|
|
405
|
+
inboundAudioStreams: inbound.length,
|
|
406
|
+
issues,
|
|
407
|
+
maxObservedGapMs,
|
|
408
|
+
outboundAudioStreams: outbound.length,
|
|
409
|
+
stalledInboundStreams,
|
|
410
|
+
stalledOutboundStreams,
|
|
411
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
412
|
+
streams,
|
|
413
|
+
totalStats: stats.length
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/client/browserMedia.ts
|
|
418
|
+
var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
|
|
419
|
+
var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
|
|
420
|
+
var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
|
|
421
|
+
var postBrowserMediaReport = async (payload, options) => {
|
|
422
|
+
const requestFetch = options.fetch ?? globalThis.fetch;
|
|
423
|
+
if (!requestFetch) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
|
|
427
|
+
body: JSON.stringify(payload),
|
|
428
|
+
headers: {
|
|
429
|
+
"Content-Type": "application/json"
|
|
430
|
+
},
|
|
431
|
+
keepalive: true,
|
|
432
|
+
method: "POST"
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
var createVoiceBrowserMediaReporter = (options) => {
|
|
436
|
+
let interval = null;
|
|
437
|
+
let previousStats = [];
|
|
438
|
+
const reportOnce = async () => {
|
|
439
|
+
const peerConnection = await resolvePeerConnection(options);
|
|
440
|
+
if (!peerConnection) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const stats = await collectMediaWebRTCStats({ peerConnection });
|
|
444
|
+
const report = buildMediaWebRTCStatsReport({
|
|
445
|
+
...options,
|
|
446
|
+
stats
|
|
447
|
+
});
|
|
448
|
+
const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
|
|
449
|
+
...options.continuity,
|
|
450
|
+
previousStats,
|
|
451
|
+
stats
|
|
452
|
+
});
|
|
453
|
+
const payload = {
|
|
454
|
+
at: Date.now(),
|
|
455
|
+
continuity,
|
|
456
|
+
report,
|
|
457
|
+
scenarioId: options.getScenarioId?.() ?? null,
|
|
458
|
+
sessionId: options.getSessionId?.() ?? null
|
|
459
|
+
};
|
|
460
|
+
previousStats = stats;
|
|
461
|
+
options.onReport?.(payload);
|
|
462
|
+
await postBrowserMediaReport(payload, options);
|
|
463
|
+
return payload;
|
|
464
|
+
};
|
|
465
|
+
const run = () => {
|
|
466
|
+
reportOnce().catch((error) => {
|
|
467
|
+
options.onError?.(error);
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
const stop = () => {
|
|
471
|
+
if (interval) {
|
|
472
|
+
clearInterval(interval);
|
|
473
|
+
interval = null;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
return {
|
|
477
|
+
close: stop,
|
|
478
|
+
reportOnce,
|
|
479
|
+
stop,
|
|
480
|
+
start: () => {
|
|
481
|
+
if (interval) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
run();
|
|
485
|
+
interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
|
|
229
490
|
// src/client/connection.ts
|
|
230
491
|
var WS_OPEN = 1;
|
|
231
492
|
var WS_CLOSED = 3;
|
|
@@ -240,13 +501,14 @@ var NOOP_CONNECTION = {
|
|
|
240
501
|
callControl: noop,
|
|
241
502
|
close: noop,
|
|
242
503
|
endTurn: noop,
|
|
504
|
+
send: noop,
|
|
505
|
+
sendAudio: noop,
|
|
506
|
+
simulateDisconnect: noop,
|
|
507
|
+
subscribe: noopUnsubscribe,
|
|
243
508
|
getReadyState: () => WS_CLOSED,
|
|
244
509
|
getScenarioId: () => "",
|
|
245
510
|
getSessionId: () => "",
|
|
246
|
-
|
|
247
|
-
sendAudio: noop,
|
|
248
|
-
start: () => {},
|
|
249
|
-
subscribe: noopUnsubscribe
|
|
511
|
+
start: () => {}
|
|
250
512
|
};
|
|
251
513
|
var createSessionId = () => crypto.randomUUID();
|
|
252
514
|
var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
@@ -269,10 +531,12 @@ var isVoiceServerMessage = (value) => {
|
|
|
269
531
|
case "assistant":
|
|
270
532
|
case "call_lifecycle":
|
|
271
533
|
case "complete":
|
|
534
|
+
case "connection":
|
|
272
535
|
case "error":
|
|
273
536
|
case "final":
|
|
274
537
|
case "partial":
|
|
275
538
|
case "pong":
|
|
539
|
+
case "replay":
|
|
276
540
|
case "session":
|
|
277
541
|
case "turn":
|
|
278
542
|
return true;
|
|
@@ -309,6 +573,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
309
573
|
sessionId: options.sessionId ?? createSessionId(),
|
|
310
574
|
ws: null
|
|
311
575
|
};
|
|
576
|
+
const emitConnection = (reconnect) => {
|
|
577
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
578
|
+
};
|
|
312
579
|
const clearTimers = () => {
|
|
313
580
|
if (state.pingInterval) {
|
|
314
581
|
clearInterval(state.pingInterval);
|
|
@@ -331,9 +598,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
331
598
|
}
|
|
332
599
|
};
|
|
333
600
|
const scheduleReconnect = () => {
|
|
601
|
+
const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
|
|
334
602
|
state.reconnectAttempts += 1;
|
|
603
|
+
emitConnection({
|
|
604
|
+
reconnect: {
|
|
605
|
+
attempts: state.reconnectAttempts,
|
|
606
|
+
lastDisconnectAt: Date.now(),
|
|
607
|
+
maxAttempts: maxReconnectAttempts,
|
|
608
|
+
nextAttemptAt,
|
|
609
|
+
status: "reconnecting"
|
|
610
|
+
},
|
|
611
|
+
type: "connection"
|
|
612
|
+
});
|
|
335
613
|
state.reconnectTimeout = setTimeout(() => {
|
|
336
614
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
615
|
+
emitConnection({
|
|
616
|
+
reconnect: {
|
|
617
|
+
attempts: state.reconnectAttempts,
|
|
618
|
+
maxAttempts: maxReconnectAttempts,
|
|
619
|
+
status: "exhausted"
|
|
620
|
+
},
|
|
621
|
+
type: "connection"
|
|
622
|
+
});
|
|
337
623
|
return;
|
|
338
624
|
}
|
|
339
625
|
connect();
|
|
@@ -343,9 +629,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
343
629
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
|
|
344
630
|
ws.binaryType = "arraybuffer";
|
|
345
631
|
ws.onopen = () => {
|
|
632
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
346
633
|
state.isConnected = true;
|
|
347
|
-
state.reconnectAttempts = 0;
|
|
348
634
|
flushPendingMessages();
|
|
635
|
+
if (wasReconnecting) {
|
|
636
|
+
emitConnection({
|
|
637
|
+
reconnect: {
|
|
638
|
+
attempts: state.reconnectAttempts,
|
|
639
|
+
lastResumedAt: Date.now(),
|
|
640
|
+
maxAttempts: maxReconnectAttempts,
|
|
641
|
+
status: "resumed"
|
|
642
|
+
},
|
|
643
|
+
type: "connection"
|
|
644
|
+
});
|
|
645
|
+
state.reconnectAttempts = 0;
|
|
646
|
+
}
|
|
349
647
|
listeners.forEach((listener) => listener({
|
|
350
648
|
scenarioId: state.scenarioId ?? undefined,
|
|
351
649
|
sessionId: state.sessionId,
|
|
@@ -375,6 +673,16 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
375
673
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
|
|
376
674
|
if (reconnectable) {
|
|
377
675
|
scheduleReconnect();
|
|
676
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
677
|
+
emitConnection({
|
|
678
|
+
reconnect: {
|
|
679
|
+
attempts: state.reconnectAttempts,
|
|
680
|
+
lastDisconnectAt: Date.now(),
|
|
681
|
+
maxAttempts: maxReconnectAttempts,
|
|
682
|
+
status: "exhausted"
|
|
683
|
+
},
|
|
684
|
+
type: "connection"
|
|
685
|
+
});
|
|
378
686
|
}
|
|
379
687
|
};
|
|
380
688
|
state.ws = ws;
|
|
@@ -397,9 +705,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
397
705
|
state.scenarioId = input.scenarioId;
|
|
398
706
|
}
|
|
399
707
|
send({
|
|
400
|
-
|
|
708
|
+
scenarioId: state.scenarioId ?? undefined,
|
|
401
709
|
sessionId: state.sessionId,
|
|
402
|
-
|
|
710
|
+
type: "start"
|
|
403
711
|
});
|
|
404
712
|
};
|
|
405
713
|
const sendAudio = (audio) => {
|
|
@@ -423,6 +731,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
423
731
|
state.isConnected = false;
|
|
424
732
|
listeners.clear();
|
|
425
733
|
};
|
|
734
|
+
const simulateDisconnect = () => {
|
|
735
|
+
if (state.ws?.readyState === WS_OPEN) {
|
|
736
|
+
state.ws.close(4000, "absolutejs-voice-reconnect-proof");
|
|
737
|
+
}
|
|
738
|
+
};
|
|
426
739
|
const subscribe = (callback) => {
|
|
427
740
|
listeners.add(callback);
|
|
428
741
|
return () => {
|
|
@@ -434,26 +747,35 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
434
747
|
callControl,
|
|
435
748
|
close,
|
|
436
749
|
endTurn,
|
|
437
|
-
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
438
|
-
getScenarioId: () => state.scenarioId ?? "",
|
|
439
|
-
getSessionId: () => state.sessionId,
|
|
440
750
|
send,
|
|
441
751
|
sendAudio,
|
|
752
|
+
simulateDisconnect,
|
|
442
753
|
start,
|
|
443
|
-
subscribe
|
|
754
|
+
subscribe,
|
|
755
|
+
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
756
|
+
getScenarioId: () => state.scenarioId ?? "",
|
|
757
|
+
getSessionId: () => state.sessionId
|
|
444
758
|
};
|
|
445
759
|
};
|
|
446
760
|
|
|
447
761
|
// src/client/store.ts
|
|
762
|
+
var createInitialReconnectState = () => ({
|
|
763
|
+
attempts: 0,
|
|
764
|
+
maxAttempts: 0,
|
|
765
|
+
status: "idle"
|
|
766
|
+
});
|
|
448
767
|
var createInitialState = () => ({
|
|
449
768
|
assistantAudio: [],
|
|
769
|
+
assistantStreamingText: "",
|
|
450
770
|
assistantTexts: [],
|
|
451
771
|
call: null,
|
|
452
772
|
error: null,
|
|
453
773
|
isConnected: false,
|
|
454
|
-
scenarioId: null,
|
|
455
774
|
partial: "",
|
|
775
|
+
reconnect: createInitialReconnectState(),
|
|
776
|
+
scenarioId: null,
|
|
456
777
|
sessionId: null,
|
|
778
|
+
sessionMetadata: null,
|
|
457
779
|
status: "idle",
|
|
458
780
|
turns: []
|
|
459
781
|
});
|
|
@@ -482,9 +804,16 @@ var createVoiceStreamStore = () => {
|
|
|
482
804
|
case "assistant":
|
|
483
805
|
state = {
|
|
484
806
|
...state,
|
|
807
|
+
assistantStreamingText: "",
|
|
485
808
|
assistantTexts: [...state.assistantTexts, action.text]
|
|
486
809
|
};
|
|
487
810
|
break;
|
|
811
|
+
case "assistant_delta":
|
|
812
|
+
state = {
|
|
813
|
+
...state,
|
|
814
|
+
assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
|
|
815
|
+
};
|
|
816
|
+
break;
|
|
488
817
|
case "complete":
|
|
489
818
|
state = {
|
|
490
819
|
...state,
|
|
@@ -509,7 +838,19 @@ var createVoiceStreamStore = () => {
|
|
|
509
838
|
case "connected":
|
|
510
839
|
state = {
|
|
511
840
|
...state,
|
|
512
|
-
isConnected: true
|
|
841
|
+
isConnected: true,
|
|
842
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
843
|
+
...state.reconnect,
|
|
844
|
+
lastResumedAt: Date.now(),
|
|
845
|
+
nextAttemptAt: undefined,
|
|
846
|
+
status: "resumed"
|
|
847
|
+
} : state.reconnect
|
|
848
|
+
};
|
|
849
|
+
break;
|
|
850
|
+
case "connection":
|
|
851
|
+
state = {
|
|
852
|
+
...state,
|
|
853
|
+
reconnect: action.reconnect
|
|
513
854
|
};
|
|
514
855
|
break;
|
|
515
856
|
case "disconnected":
|
|
@@ -537,6 +878,28 @@ var createVoiceStreamStore = () => {
|
|
|
537
878
|
partial: action.transcript.text
|
|
538
879
|
};
|
|
539
880
|
break;
|
|
881
|
+
case "replay":
|
|
882
|
+
state = {
|
|
883
|
+
...state,
|
|
884
|
+
assistantStreamingText: "",
|
|
885
|
+
assistantTexts: [...action.assistantTexts],
|
|
886
|
+
call: action.call ?? null,
|
|
887
|
+
error: null,
|
|
888
|
+
isConnected: action.status === "active",
|
|
889
|
+
partial: action.partial,
|
|
890
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
891
|
+
...state.reconnect,
|
|
892
|
+
lastResumedAt: Date.now(),
|
|
893
|
+
nextAttemptAt: undefined,
|
|
894
|
+
status: "resumed"
|
|
895
|
+
} : state.reconnect,
|
|
896
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
897
|
+
sessionId: action.sessionId,
|
|
898
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
899
|
+
status: action.status,
|
|
900
|
+
turns: [...action.turns]
|
|
901
|
+
};
|
|
902
|
+
break;
|
|
540
903
|
case "session":
|
|
541
904
|
state = {
|
|
542
905
|
...state,
|
|
@@ -544,6 +907,7 @@ var createVoiceStreamStore = () => {
|
|
|
544
907
|
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
545
908
|
isConnected: action.status === "active",
|
|
546
909
|
sessionId: action.sessionId,
|
|
910
|
+
sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
|
|
547
911
|
status: action.status
|
|
548
912
|
};
|
|
549
913
|
break;
|
|
@@ -574,29 +938,73 @@ var createVoiceStreamStore = () => {
|
|
|
574
938
|
var createVoiceStream = (path, options = {}) => {
|
|
575
939
|
const connection = createVoiceConnection(path, options);
|
|
576
940
|
const store = createVoiceStreamStore();
|
|
941
|
+
const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
|
|
942
|
+
...options.browserMedia,
|
|
943
|
+
getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
|
|
944
|
+
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
945
|
+
}) : null;
|
|
577
946
|
const subscribers = new Set;
|
|
578
947
|
const start = (input) => Promise.resolve().then(() => {
|
|
579
948
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
580
949
|
return;
|
|
581
950
|
}
|
|
582
951
|
connection.start(input);
|
|
952
|
+
browserMediaReporter?.start();
|
|
583
953
|
});
|
|
584
954
|
const notify = () => {
|
|
585
955
|
subscribers.forEach((subscriber) => subscriber());
|
|
586
956
|
};
|
|
957
|
+
const reportReconnect = () => {
|
|
958
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const snapshot = store.getSnapshot();
|
|
962
|
+
const body = JSON.stringify({
|
|
963
|
+
at: Date.now(),
|
|
964
|
+
reconnect: snapshot.reconnect,
|
|
965
|
+
scenarioId: snapshot.scenarioId,
|
|
966
|
+
sessionId: connection.getSessionId(),
|
|
967
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
968
|
+
});
|
|
969
|
+
fetch(options.reconnectReportPath, {
|
|
970
|
+
body,
|
|
971
|
+
headers: {
|
|
972
|
+
"Content-Type": "application/json"
|
|
973
|
+
},
|
|
974
|
+
keepalive: true,
|
|
975
|
+
method: "POST"
|
|
976
|
+
}).catch(() => {});
|
|
977
|
+
};
|
|
587
978
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
588
979
|
const action = serverMessageToAction(message);
|
|
589
980
|
if (action) {
|
|
590
981
|
store.dispatch(action);
|
|
982
|
+
if (message.type === "connection") {
|
|
983
|
+
reportReconnect();
|
|
984
|
+
}
|
|
591
985
|
notify();
|
|
592
986
|
}
|
|
593
987
|
});
|
|
594
988
|
return {
|
|
989
|
+
start,
|
|
990
|
+
get assistantAudio() {
|
|
991
|
+
return store.getSnapshot().assistantAudio;
|
|
992
|
+
},
|
|
993
|
+
get assistantTexts() {
|
|
994
|
+
return store.getSnapshot().assistantTexts;
|
|
995
|
+
},
|
|
996
|
+
get assistantStreamingText() {
|
|
997
|
+
return store.getSnapshot().assistantStreamingText;
|
|
998
|
+
},
|
|
999
|
+
get call() {
|
|
1000
|
+
return store.getSnapshot().call;
|
|
1001
|
+
},
|
|
595
1002
|
callControl(message) {
|
|
596
1003
|
connection.callControl(message);
|
|
597
1004
|
},
|
|
598
1005
|
close() {
|
|
599
1006
|
unsubscribeConnection();
|
|
1007
|
+
browserMediaReporter?.close();
|
|
600
1008
|
connection.close();
|
|
601
1009
|
store.dispatch({ type: "disconnected" });
|
|
602
1010
|
notify();
|
|
@@ -616,44 +1024,43 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
616
1024
|
get isConnected() {
|
|
617
1025
|
return store.getSnapshot().isConnected;
|
|
618
1026
|
},
|
|
619
|
-
get scenarioId() {
|
|
620
|
-
return store.getSnapshot().scenarioId;
|
|
621
|
-
},
|
|
622
|
-
start,
|
|
623
1027
|
get partial() {
|
|
624
1028
|
return store.getSnapshot().partial;
|
|
625
1029
|
},
|
|
626
|
-
get
|
|
627
|
-
return
|
|
1030
|
+
get reconnect() {
|
|
1031
|
+
return store.getSnapshot().reconnect;
|
|
628
1032
|
},
|
|
629
|
-
get
|
|
630
|
-
return store.getSnapshot().
|
|
1033
|
+
get scenarioId() {
|
|
1034
|
+
return store.getSnapshot().scenarioId;
|
|
631
1035
|
},
|
|
632
|
-
|
|
633
|
-
|
|
1036
|
+
sendAudio(audio) {
|
|
1037
|
+
connection.sendAudio(audio);
|
|
634
1038
|
},
|
|
635
|
-
get
|
|
636
|
-
return
|
|
1039
|
+
get sessionId() {
|
|
1040
|
+
return connection.getSessionId();
|
|
637
1041
|
},
|
|
638
|
-
get
|
|
639
|
-
return store.getSnapshot().
|
|
1042
|
+
get sessionMetadata() {
|
|
1043
|
+
return store.getSnapshot().sessionMetadata;
|
|
640
1044
|
},
|
|
641
|
-
|
|
642
|
-
|
|
1045
|
+
simulateDisconnect() {
|
|
1046
|
+
connection.simulateDisconnect();
|
|
643
1047
|
},
|
|
644
|
-
|
|
645
|
-
|
|
1048
|
+
get status() {
|
|
1049
|
+
return store.getSnapshot().status;
|
|
646
1050
|
},
|
|
647
1051
|
subscribe(subscriber) {
|
|
648
1052
|
subscribers.add(subscriber);
|
|
649
1053
|
return () => {
|
|
650
1054
|
subscribers.delete(subscriber);
|
|
651
1055
|
};
|
|
1056
|
+
},
|
|
1057
|
+
get turns() {
|
|
1058
|
+
return store.getSnapshot().turns;
|
|
652
1059
|
}
|
|
653
1060
|
};
|
|
654
1061
|
};
|
|
655
1062
|
|
|
656
|
-
// src/audioConditioning.ts
|
|
1063
|
+
// src/core/audioConditioning.ts
|
|
657
1064
|
var DEFAULT_TARGET_LEVEL = 0.08;
|
|
658
1065
|
var DEFAULT_MAX_GAIN = 3;
|
|
659
1066
|
var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
|
|
@@ -671,7 +1078,7 @@ var resolveAudioConditioningConfig = (config) => {
|
|
|
671
1078
|
};
|
|
672
1079
|
};
|
|
673
1080
|
|
|
674
|
-
// src/turnProfiles.ts
|
|
1081
|
+
// src/core/turnProfiles.ts
|
|
675
1082
|
var TURN_PROFILE_DEFAULTS = {
|
|
676
1083
|
balanced: {
|
|
677
1084
|
qualityProfile: "general",
|
|
@@ -693,12 +1100,12 @@ var TURN_PROFILE_DEFAULTS = {
|
|
|
693
1100
|
}
|
|
694
1101
|
};
|
|
695
1102
|
var QUALITY_PROFILE_DEFAULTS = {
|
|
696
|
-
general: {},
|
|
697
1103
|
"accent-heavy": {
|
|
698
1104
|
silenceMs: 1200,
|
|
699
1105
|
speechThreshold: 0.01,
|
|
700
1106
|
transcriptStabilityMs: 1200
|
|
701
1107
|
},
|
|
1108
|
+
general: {},
|
|
702
1109
|
"noisy-room": {
|
|
703
1110
|
silenceMs: 2000,
|
|
704
1111
|
speechThreshold: 0.02,
|
|
@@ -726,7 +1133,7 @@ var resolveTurnDetectionConfig = (config) => {
|
|
|
726
1133
|
};
|
|
727
1134
|
};
|
|
728
1135
|
|
|
729
|
-
// src/presets.ts
|
|
1136
|
+
// src/core/presets.ts
|
|
730
1137
|
var PRESET_INPUTS = {
|
|
731
1138
|
chat: {
|
|
732
1139
|
audioConditioning: {
|
|
@@ -747,8 +1154,8 @@ var PRESET_INPUTS = {
|
|
|
747
1154
|
},
|
|
748
1155
|
sttLifecycle: "continuous",
|
|
749
1156
|
turnDetection: {
|
|
750
|
-
|
|
751
|
-
|
|
1157
|
+
profile: "balanced",
|
|
1158
|
+
qualityProfile: "short-command"
|
|
752
1159
|
}
|
|
753
1160
|
},
|
|
754
1161
|
default: {
|
|
@@ -763,8 +1170,8 @@ var PRESET_INPUTS = {
|
|
|
763
1170
|
},
|
|
764
1171
|
sttLifecycle: "continuous",
|
|
765
1172
|
turnDetection: {
|
|
766
|
-
|
|
767
|
-
|
|
1173
|
+
profile: "fast",
|
|
1174
|
+
qualityProfile: "general"
|
|
768
1175
|
}
|
|
769
1176
|
},
|
|
770
1177
|
dictation: {
|
|
@@ -786,8 +1193,8 @@ var PRESET_INPUTS = {
|
|
|
786
1193
|
},
|
|
787
1194
|
sttLifecycle: "continuous",
|
|
788
1195
|
turnDetection: {
|
|
789
|
-
|
|
790
|
-
|
|
1196
|
+
profile: "long-form",
|
|
1197
|
+
qualityProfile: "accent-heavy"
|
|
791
1198
|
}
|
|
792
1199
|
},
|
|
793
1200
|
"guided-intake": {
|
|
@@ -809,8 +1216,8 @@ var PRESET_INPUTS = {
|
|
|
809
1216
|
},
|
|
810
1217
|
sttLifecycle: "turn-scoped",
|
|
811
1218
|
turnDetection: {
|
|
812
|
-
|
|
813
|
-
|
|
1219
|
+
profile: "long-form",
|
|
1220
|
+
qualityProfile: "accent-heavy"
|
|
814
1221
|
}
|
|
815
1222
|
},
|
|
816
1223
|
"noisy-room": {
|
|
@@ -832,8 +1239,8 @@ var PRESET_INPUTS = {
|
|
|
832
1239
|
},
|
|
833
1240
|
sttLifecycle: "continuous",
|
|
834
1241
|
turnDetection: {
|
|
835
|
-
qualityProfile: "noisy-room",
|
|
836
1242
|
profile: "long-form",
|
|
1243
|
+
qualityProfile: "noisy-room",
|
|
837
1244
|
silenceMs: 2100,
|
|
838
1245
|
speechThreshold: 0.02,
|
|
839
1246
|
transcriptStabilityMs: 1650
|
|
@@ -858,8 +1265,8 @@ var PRESET_INPUTS = {
|
|
|
858
1265
|
},
|
|
859
1266
|
sttLifecycle: "continuous",
|
|
860
1267
|
turnDetection: {
|
|
861
|
-
qualityProfile: "noisy-room",
|
|
862
1268
|
profile: "long-form",
|
|
1269
|
+
qualityProfile: "noisy-room",
|
|
863
1270
|
silenceMs: 660,
|
|
864
1271
|
speechThreshold: 0.012,
|
|
865
1272
|
transcriptStabilityMs: 300
|
|
@@ -884,8 +1291,8 @@ var PRESET_INPUTS = {
|
|
|
884
1291
|
},
|
|
885
1292
|
sttLifecycle: "continuous",
|
|
886
1293
|
turnDetection: {
|
|
887
|
-
qualityProfile: "noisy-room",
|
|
888
1294
|
profile: "long-form",
|
|
1295
|
+
qualityProfile: "noisy-room",
|
|
889
1296
|
silenceMs: 620,
|
|
890
1297
|
speechThreshold: 0.012,
|
|
891
1298
|
transcriptStabilityMs: 280
|
|
@@ -910,8 +1317,8 @@ var PRESET_INPUTS = {
|
|
|
910
1317
|
},
|
|
911
1318
|
sttLifecycle: "continuous",
|
|
912
1319
|
turnDetection: {
|
|
913
|
-
|
|
914
|
-
|
|
1320
|
+
profile: "long-form",
|
|
1321
|
+
qualityProfile: "noisy-room"
|
|
915
1322
|
}
|
|
916
1323
|
}
|
|
917
1324
|
};
|
|
@@ -935,14 +1342,17 @@ var resolveVoiceRuntimePreset = (name = "default") => {
|
|
|
935
1342
|
// src/client/controller.ts
|
|
936
1343
|
var createInitialState2 = (stream) => ({
|
|
937
1344
|
assistantAudio: [...stream.assistantAudio],
|
|
1345
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
938
1346
|
assistantTexts: [...stream.assistantTexts],
|
|
939
1347
|
call: stream.call,
|
|
940
1348
|
error: stream.error,
|
|
941
1349
|
isConnected: stream.isConnected,
|
|
942
1350
|
isRecording: false,
|
|
943
1351
|
partial: stream.partial,
|
|
1352
|
+
reconnect: stream.reconnect,
|
|
944
1353
|
recordingError: null,
|
|
945
1354
|
sessionId: stream.sessionId,
|
|
1355
|
+
sessionMetadata: stream.sessionMetadata,
|
|
946
1356
|
scenarioId: stream.scenarioId,
|
|
947
1357
|
status: stream.status,
|
|
948
1358
|
turns: [...stream.turns]
|
|
@@ -965,12 +1375,15 @@ var createVoiceController = (path, options = {}) => {
|
|
|
965
1375
|
state = {
|
|
966
1376
|
...state,
|
|
967
1377
|
assistantAudio: [...stream.assistantAudio],
|
|
1378
|
+
assistantStreamingText: stream.assistantStreamingText,
|
|
968
1379
|
assistantTexts: [...stream.assistantTexts],
|
|
969
1380
|
call: stream.call,
|
|
970
1381
|
error: stream.error,
|
|
971
1382
|
isConnected: stream.isConnected,
|
|
972
1383
|
partial: stream.partial,
|
|
1384
|
+
reconnect: stream.reconnect,
|
|
973
1385
|
sessionId: stream.sessionId,
|
|
1386
|
+
sessionMetadata: stream.sessionMetadata,
|
|
974
1387
|
scenarioId: stream.scenarioId,
|
|
975
1388
|
status: stream.status,
|
|
976
1389
|
turns: [...stream.turns]
|
|
@@ -994,8 +1407,15 @@ var createVoiceController = (path, options = {}) => {
|
|
|
994
1407
|
capture = createMicrophoneCapture({
|
|
995
1408
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
996
1409
|
onLevel: options.capture?.onLevel,
|
|
997
|
-
onAudio: (audio) =>
|
|
998
|
-
|
|
1410
|
+
onAudio: (audio) => {
|
|
1411
|
+
if (options.capture?.onAudio) {
|
|
1412
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
stream.sendAudio(audio);
|
|
1416
|
+
},
|
|
1417
|
+
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
|
|
1418
|
+
...options.capture?.stream ? { stream: options.capture.stream } : {}
|
|
999
1419
|
});
|
|
1000
1420
|
return capture;
|
|
1001
1421
|
};
|
|
@@ -1041,11 +1461,25 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1041
1461
|
stream.close();
|
|
1042
1462
|
};
|
|
1043
1463
|
return {
|
|
1464
|
+
close,
|
|
1465
|
+
startRecording,
|
|
1466
|
+
stopRecording,
|
|
1467
|
+
get assistantAudio() {
|
|
1468
|
+
return state.assistantAudio;
|
|
1469
|
+
},
|
|
1470
|
+
get assistantTexts() {
|
|
1471
|
+
return state.assistantTexts;
|
|
1472
|
+
},
|
|
1473
|
+
get assistantStreamingText() {
|
|
1474
|
+
return state.assistantStreamingText;
|
|
1475
|
+
},
|
|
1044
1476
|
bindHTMX(bindingOptions) {
|
|
1045
1477
|
return bindVoiceHTMX(stream, bindingOptions);
|
|
1046
1478
|
},
|
|
1479
|
+
get call() {
|
|
1480
|
+
return state.call;
|
|
1481
|
+
},
|
|
1047
1482
|
callControl: (message) => stream.callControl(message),
|
|
1048
|
-
close,
|
|
1049
1483
|
endTurn: () => stream.endTurn(),
|
|
1050
1484
|
get error() {
|
|
1051
1485
|
return state.error;
|
|
@@ -1061,21 +1495,26 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1061
1495
|
get partial() {
|
|
1062
1496
|
return state.partial;
|
|
1063
1497
|
},
|
|
1498
|
+
get reconnect() {
|
|
1499
|
+
return state.reconnect;
|
|
1500
|
+
},
|
|
1064
1501
|
get recordingError() {
|
|
1065
1502
|
return state.recordingError;
|
|
1066
1503
|
},
|
|
1504
|
+
get scenarioId() {
|
|
1505
|
+
return state.scenarioId;
|
|
1506
|
+
},
|
|
1067
1507
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1068
1508
|
get sessionId() {
|
|
1069
1509
|
return state.sessionId;
|
|
1070
1510
|
},
|
|
1071
|
-
get
|
|
1072
|
-
return state.
|
|
1511
|
+
get sessionMetadata() {
|
|
1512
|
+
return state.sessionMetadata;
|
|
1073
1513
|
},
|
|
1074
|
-
|
|
1514
|
+
simulateDisconnect: () => stream.simulateDisconnect(),
|
|
1075
1515
|
get status() {
|
|
1076
1516
|
return state.status;
|
|
1077
1517
|
},
|
|
1078
|
-
stopRecording,
|
|
1079
1518
|
subscribe: (subscriber) => {
|
|
1080
1519
|
subscribers.add(subscriber);
|
|
1081
1520
|
return () => {
|
|
@@ -1091,15 +1530,690 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1091
1530
|
},
|
|
1092
1531
|
get turns() {
|
|
1093
1532
|
return state.turns;
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
// src/client/timeStretch.ts
|
|
1538
|
+
var HOP_MS = 10;
|
|
1539
|
+
var SEEK_MS = 5;
|
|
1540
|
+
var ENERGY_EPSILON = 0.000001;
|
|
1541
|
+
var HALF = 0.5;
|
|
1542
|
+
var MS_PER_SECOND = 1000;
|
|
1543
|
+
var makeHann = (length) => {
|
|
1544
|
+
const weights = new Float32Array(length);
|
|
1545
|
+
for (let index = 0;index < length; index += 1) {
|
|
1546
|
+
weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
|
|
1547
|
+
}
|
|
1548
|
+
return weights;
|
|
1549
|
+
};
|
|
1550
|
+
var correlationScore = (base, start, ref, length) => {
|
|
1551
|
+
let dot = 0;
|
|
1552
|
+
let energy = 0;
|
|
1553
|
+
for (let index = 0;index < length; index += 1) {
|
|
1554
|
+
const sample = base[start + index] ?? 0;
|
|
1555
|
+
dot += sample * (ref[index] ?? 0);
|
|
1556
|
+
energy += sample * sample;
|
|
1557
|
+
}
|
|
1558
|
+
return dot / Math.sqrt(energy + ENERGY_EPSILON);
|
|
1559
|
+
};
|
|
1560
|
+
var overlapAddGrain = (src, off, tail, weights, hop) => {
|
|
1561
|
+
const out = new Float32Array(hop);
|
|
1562
|
+
const nextTail = new Float32Array(hop);
|
|
1563
|
+
for (let index = 0;index < hop; index += 1) {
|
|
1564
|
+
out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
|
|
1565
|
+
nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
|
|
1566
|
+
}
|
|
1567
|
+
return { nextTail, out };
|
|
1568
|
+
};
|
|
1569
|
+
var createTimeStretcher = () => {
|
|
1570
|
+
let sampleRate = 0;
|
|
1571
|
+
let channelCount = 0;
|
|
1572
|
+
let hop = 0;
|
|
1573
|
+
let frameLen = 0;
|
|
1574
|
+
let seek = 0;
|
|
1575
|
+
let weights = new Float32Array(0);
|
|
1576
|
+
let buffers = [];
|
|
1577
|
+
let inputStart = 0;
|
|
1578
|
+
let analysisPos = 0;
|
|
1579
|
+
let olaTail = [];
|
|
1580
|
+
let naturalRef = null;
|
|
1581
|
+
const init = (rate, channels) => {
|
|
1582
|
+
sampleRate = rate;
|
|
1583
|
+
channelCount = channels;
|
|
1584
|
+
hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
|
|
1585
|
+
frameLen = hop * 2;
|
|
1586
|
+
seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
|
|
1587
|
+
weights = makeHann(frameLen);
|
|
1588
|
+
buffers = Array.from({ length: channels }, () => new Float32Array(0));
|
|
1589
|
+
olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
|
|
1590
|
+
inputStart = 0;
|
|
1591
|
+
analysisPos = seek;
|
|
1592
|
+
naturalRef = null;
|
|
1593
|
+
};
|
|
1594
|
+
const reset = () => {
|
|
1595
|
+
buffers = buffers.map(() => new Float32Array(0));
|
|
1596
|
+
olaTail = olaTail.map(() => new Float32Array(hop));
|
|
1597
|
+
inputStart = 0;
|
|
1598
|
+
analysisPos = seek;
|
|
1599
|
+
naturalRef = null;
|
|
1600
|
+
};
|
|
1601
|
+
const append = (input) => {
|
|
1602
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1603
|
+
const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
|
|
1604
|
+
const existing = buffers[channel] ?? new Float32Array(0);
|
|
1605
|
+
const merged = new Float32Array(existing.length + incoming.length);
|
|
1606
|
+
merged.set(existing, 0);
|
|
1607
|
+
merged.set(incoming, existing.length);
|
|
1608
|
+
buffers[channel] = merged;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
|
|
1612
|
+
const compact = () => {
|
|
1613
|
+
const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
|
|
1614
|
+
if (keepFrom <= inputStart)
|
|
1615
|
+
return;
|
|
1616
|
+
const drop = keepFrom - inputStart;
|
|
1617
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1618
|
+
buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
|
|
1619
|
+
}
|
|
1620
|
+
inputStart = keepFrom;
|
|
1621
|
+
};
|
|
1622
|
+
const bestOffset = (center) => {
|
|
1623
|
+
if (!naturalRef)
|
|
1624
|
+
return 0;
|
|
1625
|
+
const [base] = buffers;
|
|
1626
|
+
if (!base)
|
|
1627
|
+
return 0;
|
|
1628
|
+
let bestDelta = 0;
|
|
1629
|
+
let bestScore = -Infinity;
|
|
1630
|
+
for (let delta = -seek;delta <= seek; delta += 1) {
|
|
1631
|
+
const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
|
|
1632
|
+
if (score <= bestScore)
|
|
1633
|
+
continue;
|
|
1634
|
+
bestScore = score;
|
|
1635
|
+
bestDelta = delta;
|
|
1636
|
+
}
|
|
1637
|
+
return bestDelta;
|
|
1638
|
+
};
|
|
1639
|
+
const process = (input, speed, rate) => {
|
|
1640
|
+
const channels = Math.max(1, input.length);
|
|
1641
|
+
if (sampleRate !== rate || channelCount !== channels)
|
|
1642
|
+
init(rate, channels);
|
|
1643
|
+
append(input);
|
|
1644
|
+
const analysisHop = hop * speed;
|
|
1645
|
+
const segments = Array.from({ length: channelCount }, () => []);
|
|
1646
|
+
const emitGrain = (pos) => {
|
|
1647
|
+
const off = pos - inputStart;
|
|
1648
|
+
for (let channel = 0;channel < channelCount; channel += 1) {
|
|
1649
|
+
const src = buffers[channel];
|
|
1650
|
+
const tail = olaTail[channel];
|
|
1651
|
+
if (!src || !tail)
|
|
1652
|
+
continue;
|
|
1653
|
+
const grain = overlapAddGrain(src, off, tail, weights, hop);
|
|
1654
|
+
olaTail[channel] = grain.nextTail;
|
|
1655
|
+
segments[channel]?.push(grain.out);
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
const captureRef = (pos) => {
|
|
1659
|
+
const ref = new Float32Array(frameLen);
|
|
1660
|
+
const refOff = pos + hop - inputStart;
|
|
1661
|
+
const [base] = buffers;
|
|
1662
|
+
if (base)
|
|
1663
|
+
ref.set(base.subarray(refOff, refOff + frameLen));
|
|
1664
|
+
naturalRef = ref;
|
|
1665
|
+
};
|
|
1666
|
+
const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
|
|
1667
|
+
while (canEmit()) {
|
|
1668
|
+
const center = Math.round(analysisPos);
|
|
1669
|
+
const pos = center + bestOffset(center);
|
|
1670
|
+
emitGrain(pos);
|
|
1671
|
+
captureRef(pos);
|
|
1672
|
+
analysisPos += analysisHop;
|
|
1673
|
+
}
|
|
1674
|
+
compact();
|
|
1675
|
+
return segments.map((channelSegments) => {
|
|
1676
|
+
const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
|
|
1677
|
+
const merged = new Float32Array(total);
|
|
1678
|
+
let offset = 0;
|
|
1679
|
+
for (const seg of channelSegments) {
|
|
1680
|
+
merged.set(seg, offset);
|
|
1681
|
+
offset += seg.length;
|
|
1682
|
+
}
|
|
1683
|
+
return merged;
|
|
1684
|
+
});
|
|
1685
|
+
};
|
|
1686
|
+
return { process, reset };
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
// src/client/audioPlayer.ts
|
|
1690
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1691
|
+
var DEFAULT_VOLUME = 1;
|
|
1692
|
+
var DEFAULT_PLAYBACK_RATE = 1;
|
|
1693
|
+
var MIN_PLAYBACK_RATE = 0.5;
|
|
1694
|
+
var MAX_PLAYBACK_RATE = 2;
|
|
1695
|
+
var STRETCH_BYPASS_EPSILON = 0.01;
|
|
1696
|
+
var createInitialState3 = () => ({
|
|
1697
|
+
activeSourceCount: 0,
|
|
1698
|
+
error: null,
|
|
1699
|
+
isActive: false,
|
|
1700
|
+
isPlaying: false,
|
|
1701
|
+
lastInterruptLatencyMs: undefined,
|
|
1702
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
1703
|
+
processedChunkCount: 0,
|
|
1704
|
+
queuedChunkCount: 0
|
|
1705
|
+
});
|
|
1706
|
+
var getAudioContextCtor = () => {
|
|
1707
|
+
if (typeof window === "undefined") {
|
|
1708
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
1709
|
+
}
|
|
1710
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
1711
|
+
};
|
|
1712
|
+
var clampVolume = (volume) => {
|
|
1713
|
+
if (typeof volume !== "number" || !Number.isFinite(volume)) {
|
|
1714
|
+
return DEFAULT_VOLUME;
|
|
1715
|
+
}
|
|
1716
|
+
return Math.min(1, Math.max(0, volume));
|
|
1717
|
+
};
|
|
1718
|
+
var clampPlaybackRate = (rate) => {
|
|
1719
|
+
if (typeof rate !== "number" || !Number.isFinite(rate)) {
|
|
1720
|
+
return DEFAULT_PLAYBACK_RATE;
|
|
1721
|
+
}
|
|
1722
|
+
return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
|
|
1723
|
+
};
|
|
1724
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
1725
|
+
const { format } = chunk;
|
|
1726
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
1727
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
1728
|
+
}
|
|
1729
|
+
const bytes = chunk.chunk;
|
|
1730
|
+
const channels = Math.max(1, format.channels);
|
|
1731
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
1732
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
1733
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
1734
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1735
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1736
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
1737
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
1738
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
1739
|
+
const sampleOffset = sampleIndex * 2;
|
|
1740
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
1741
|
+
channelData[frameIndex] = 0;
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return audioBuffer;
|
|
1748
|
+
};
|
|
1749
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
1750
|
+
const subscribers = new Set;
|
|
1751
|
+
const sourceNodes = new Set;
|
|
1752
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
1753
|
+
let state = createInitialState3();
|
|
1754
|
+
let audioContext = null;
|
|
1755
|
+
let outputNode = null;
|
|
1756
|
+
let volume = clampVolume(options.volume);
|
|
1757
|
+
let playbackRate = clampPlaybackRate(options.playbackRate);
|
|
1758
|
+
let stretcher = null;
|
|
1759
|
+
let queueEndTime = 0;
|
|
1760
|
+
let syncPromise = Promise.resolve();
|
|
1761
|
+
let interruptStartedAt = null;
|
|
1762
|
+
let interruptPromise = null;
|
|
1763
|
+
let resolveInterruptPromise = null;
|
|
1764
|
+
let interruptFallbackTimer = null;
|
|
1765
|
+
const notify = () => {
|
|
1766
|
+
for (const subscriber of subscribers) {
|
|
1767
|
+
subscriber();
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
const setState = (next) => {
|
|
1771
|
+
state = {
|
|
1772
|
+
...state,
|
|
1773
|
+
...next
|
|
1774
|
+
};
|
|
1775
|
+
notify();
|
|
1776
|
+
};
|
|
1777
|
+
const clearError = () => {
|
|
1778
|
+
if (state.error !== null) {
|
|
1779
|
+
setState({ error: null });
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
const clearInterruptTimer = () => {
|
|
1783
|
+
if (interruptFallbackTimer !== null) {
|
|
1784
|
+
clearTimeout(interruptFallbackTimer);
|
|
1785
|
+
interruptFallbackTimer = null;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
const resolveInterrupt = (latencyMs) => {
|
|
1789
|
+
clearInterruptTimer();
|
|
1790
|
+
interruptStartedAt = null;
|
|
1791
|
+
stretcher?.reset();
|
|
1792
|
+
setState({
|
|
1793
|
+
activeSourceCount: sourceNodes.size,
|
|
1794
|
+
isPlaying: false,
|
|
1795
|
+
lastInterruptLatencyMs: latencyMs,
|
|
1796
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
1797
|
+
});
|
|
1798
|
+
resolveInterruptPromise?.();
|
|
1799
|
+
resolveInterruptPromise = null;
|
|
1800
|
+
interruptPromise = null;
|
|
1801
|
+
};
|
|
1802
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
1803
|
+
if (!context) {
|
|
1804
|
+
return 0;
|
|
1805
|
+
}
|
|
1806
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
1807
|
+
};
|
|
1808
|
+
const applyOutputGain = (context) => {
|
|
1809
|
+
if (!outputNode) {
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const gainValue = volume;
|
|
1813
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1814
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
outputNode.gain.value = gainValue;
|
|
1818
|
+
};
|
|
1819
|
+
const muteOutputGain = (context) => {
|
|
1820
|
+
if (!outputNode) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
const gainValue = 0;
|
|
1824
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1825
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
outputNode.gain.value = gainValue;
|
|
1829
|
+
};
|
|
1830
|
+
const maybeResolveInterrupt = () => {
|
|
1831
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
1835
|
+
};
|
|
1836
|
+
const ensureAudioContext = async () => {
|
|
1837
|
+
if (audioContext) {
|
|
1838
|
+
return audioContext;
|
|
1839
|
+
}
|
|
1840
|
+
if (options.createAudioContext) {
|
|
1841
|
+
audioContext = options.createAudioContext();
|
|
1842
|
+
} else {
|
|
1843
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
1844
|
+
if (!AudioContextCtor) {
|
|
1845
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
1846
|
+
}
|
|
1847
|
+
audioContext = new AudioContextCtor;
|
|
1848
|
+
}
|
|
1849
|
+
if (audioContext.createGain) {
|
|
1850
|
+
outputNode = audioContext.createGain();
|
|
1851
|
+
outputNode.connect?.(audioContext.destination);
|
|
1852
|
+
}
|
|
1853
|
+
queueEndTime = audioContext.currentTime;
|
|
1854
|
+
return audioContext;
|
|
1855
|
+
};
|
|
1856
|
+
const scheduleBuffer = (context, buffer, rate) => {
|
|
1857
|
+
const node = context.createBufferSource();
|
|
1858
|
+
node.buffer = buffer;
|
|
1859
|
+
if (node.playbackRate) {
|
|
1860
|
+
node.playbackRate.value = rate;
|
|
1861
|
+
}
|
|
1862
|
+
node.connect(outputNode ?? context.destination);
|
|
1863
|
+
node.onended = () => {
|
|
1864
|
+
sourceNodes.delete(node);
|
|
1865
|
+
node.disconnect?.();
|
|
1866
|
+
setState({
|
|
1867
|
+
activeSourceCount: sourceNodes.size,
|
|
1868
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
1869
|
+
});
|
|
1870
|
+
maybeResolveInterrupt();
|
|
1871
|
+
};
|
|
1872
|
+
const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
|
|
1873
|
+
queueEndTime = startAt + buffer.duration / rate;
|
|
1874
|
+
sourceNodes.add(node);
|
|
1875
|
+
setState({
|
|
1876
|
+
activeSourceCount: sourceNodes.size,
|
|
1877
|
+
isPlaying: true
|
|
1878
|
+
});
|
|
1879
|
+
node.start(startAt);
|
|
1880
|
+
};
|
|
1881
|
+
const scheduleChunk = async (chunk) => {
|
|
1882
|
+
const context = await ensureAudioContext();
|
|
1883
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
1884
|
+
if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
|
|
1885
|
+
stretcher?.reset();
|
|
1886
|
+
scheduleBuffer(context, buffer, playbackRate);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const channels = Math.max(1, chunk.format.channels);
|
|
1890
|
+
const input = [];
|
|
1891
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1892
|
+
input.push(buffer.getChannelData(channelIndex));
|
|
1893
|
+
}
|
|
1894
|
+
stretcher ??= createTimeStretcher();
|
|
1895
|
+
const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
|
|
1896
|
+
const outLength = stretched[0]?.length ?? 0;
|
|
1897
|
+
if (outLength === 0) {
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
|
|
1901
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1902
|
+
const channelOut = stretched[channelIndex];
|
|
1903
|
+
if (!channelOut)
|
|
1904
|
+
continue;
|
|
1905
|
+
outBuffer.getChannelData(channelIndex).set(channelOut);
|
|
1906
|
+
}
|
|
1907
|
+
scheduleBuffer(context, outBuffer, 1);
|
|
1908
|
+
};
|
|
1909
|
+
const stopQueuedPlayback = (options2) => {
|
|
1910
|
+
for (const node of [...sourceNodes]) {
|
|
1911
|
+
node.stop?.();
|
|
1912
|
+
}
|
|
1913
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
1914
|
+
if (options2?.forceClear) {
|
|
1915
|
+
for (const node of sourceNodes) {
|
|
1916
|
+
node.disconnect?.();
|
|
1917
|
+
}
|
|
1918
|
+
sourceNodes.clear();
|
|
1919
|
+
maybeResolveInterrupt();
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
const sync = async () => {
|
|
1923
|
+
if (!state.isActive) {
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
1927
|
+
if (nextChunks.length === 0) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
try {
|
|
1931
|
+
clearError();
|
|
1932
|
+
for (const chunk of nextChunks) {
|
|
1933
|
+
await scheduleChunk(chunk);
|
|
1934
|
+
}
|
|
1935
|
+
setState({
|
|
1936
|
+
processedChunkCount: source.assistantAudio.length,
|
|
1937
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
1938
|
+
});
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
setState({
|
|
1941
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
const queueSync = () => {
|
|
1946
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
1947
|
+
return syncPromise;
|
|
1948
|
+
};
|
|
1949
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
1950
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
1951
|
+
player.start();
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (state.isActive) {
|
|
1955
|
+
queueSync();
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
const player = {
|
|
1959
|
+
get activeSourceCount() {
|
|
1960
|
+
return state.activeSourceCount;
|
|
1094
1961
|
},
|
|
1095
|
-
|
|
1096
|
-
|
|
1962
|
+
close: async () => {
|
|
1963
|
+
unsubscribeSource();
|
|
1964
|
+
stopQueuedPlayback({ forceClear: true });
|
|
1965
|
+
clearInterruptTimer();
|
|
1966
|
+
resolveInterruptPromise?.();
|
|
1967
|
+
resolveInterruptPromise = null;
|
|
1968
|
+
interruptPromise = null;
|
|
1969
|
+
interruptStartedAt = null;
|
|
1970
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
1971
|
+
await audioContext.close();
|
|
1972
|
+
}
|
|
1973
|
+
audioContext = null;
|
|
1974
|
+
outputNode?.disconnect?.();
|
|
1975
|
+
outputNode = null;
|
|
1976
|
+
queueEndTime = 0;
|
|
1977
|
+
setState({
|
|
1978
|
+
activeSourceCount: 0,
|
|
1979
|
+
isActive: false,
|
|
1980
|
+
isPlaying: false
|
|
1981
|
+
});
|
|
1097
1982
|
},
|
|
1098
|
-
get
|
|
1099
|
-
return state.
|
|
1983
|
+
get error() {
|
|
1984
|
+
return state.error;
|
|
1100
1985
|
},
|
|
1101
|
-
|
|
1102
|
-
|
|
1986
|
+
getSnapshot: () => state,
|
|
1987
|
+
interrupt: async () => {
|
|
1988
|
+
const startedAt = Date.now();
|
|
1989
|
+
const context = await ensureAudioContext();
|
|
1990
|
+
interruptStartedAt = startedAt;
|
|
1991
|
+
muteOutputGain(context);
|
|
1992
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
1993
|
+
setState({
|
|
1994
|
+
isActive: false,
|
|
1995
|
+
isPlaying: sourceNodes.size > 0,
|
|
1996
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
1997
|
+
});
|
|
1998
|
+
if (sourceNodes.size === 0) {
|
|
1999
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (!interruptPromise) {
|
|
2003
|
+
interruptPromise = new Promise((resolve) => {
|
|
2004
|
+
resolveInterruptPromise = resolve;
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
clearInterruptTimer();
|
|
2008
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
2009
|
+
for (const node of sourceNodes) {
|
|
2010
|
+
node.disconnect?.();
|
|
2011
|
+
}
|
|
2012
|
+
sourceNodes.clear();
|
|
2013
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
2014
|
+
}, 250);
|
|
2015
|
+
stopQueuedPlayback();
|
|
2016
|
+
await interruptPromise;
|
|
2017
|
+
},
|
|
2018
|
+
get isActive() {
|
|
2019
|
+
return state.isActive;
|
|
2020
|
+
},
|
|
2021
|
+
get isPlaying() {
|
|
2022
|
+
return state.isPlaying;
|
|
2023
|
+
},
|
|
2024
|
+
get lastInterruptLatencyMs() {
|
|
2025
|
+
return state.lastInterruptLatencyMs;
|
|
2026
|
+
},
|
|
2027
|
+
get lastPlaybackStopLatencyMs() {
|
|
2028
|
+
return state.lastPlaybackStopLatencyMs;
|
|
2029
|
+
},
|
|
2030
|
+
pause: async () => {
|
|
2031
|
+
if (!audioContext) {
|
|
2032
|
+
setState({
|
|
2033
|
+
activeSourceCount: 0,
|
|
2034
|
+
isActive: false,
|
|
2035
|
+
isPlaying: false
|
|
2036
|
+
});
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
await audioContext.suspend();
|
|
2040
|
+
setState({
|
|
2041
|
+
activeSourceCount: sourceNodes.size,
|
|
2042
|
+
isActive: false,
|
|
2043
|
+
isPlaying: false
|
|
2044
|
+
});
|
|
2045
|
+
},
|
|
2046
|
+
get playbackRate() {
|
|
2047
|
+
return playbackRate;
|
|
2048
|
+
},
|
|
2049
|
+
get processedChunkCount() {
|
|
2050
|
+
return state.processedChunkCount;
|
|
2051
|
+
},
|
|
2052
|
+
get queuedChunkCount() {
|
|
2053
|
+
return state.queuedChunkCount;
|
|
2054
|
+
},
|
|
2055
|
+
setPlaybackRate: (nextRate) => {
|
|
2056
|
+
playbackRate = clampPlaybackRate(nextRate);
|
|
2057
|
+
},
|
|
2058
|
+
setVolume: (nextVolume) => {
|
|
2059
|
+
volume = clampVolume(nextVolume);
|
|
2060
|
+
applyOutputGain(audioContext);
|
|
2061
|
+
},
|
|
2062
|
+
start: async () => {
|
|
2063
|
+
try {
|
|
2064
|
+
clearError();
|
|
2065
|
+
const context = await ensureAudioContext();
|
|
2066
|
+
applyOutputGain(context);
|
|
2067
|
+
if (context.state === "suspended") {
|
|
2068
|
+
await context.resume();
|
|
2069
|
+
}
|
|
2070
|
+
setState({
|
|
2071
|
+
activeSourceCount: sourceNodes.size,
|
|
2072
|
+
isActive: true,
|
|
2073
|
+
isPlaying: context.state === "running"
|
|
2074
|
+
});
|
|
2075
|
+
await queueSync();
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
setState({
|
|
2078
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2079
|
+
isActive: false,
|
|
2080
|
+
isPlaying: false
|
|
2081
|
+
});
|
|
2082
|
+
throw error;
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
subscribe: (subscriber) => {
|
|
2086
|
+
subscribers.add(subscriber);
|
|
2087
|
+
return () => {
|
|
2088
|
+
subscribers.delete(subscriber);
|
|
2089
|
+
};
|
|
2090
|
+
},
|
|
2091
|
+
get volume() {
|
|
2092
|
+
return volume;
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
return player;
|
|
2096
|
+
};
|
|
2097
|
+
|
|
2098
|
+
// src/client/bargeInMonitor.ts
|
|
2099
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
2100
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
2101
|
+
var summarize = (events, thresholdMs) => {
|
|
2102
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
2103
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
2104
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
2105
|
+
const passed = stopped.length - failed;
|
|
2106
|
+
return {
|
|
2107
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
2108
|
+
events: [...events],
|
|
2109
|
+
failed,
|
|
2110
|
+
lastEvent: events.at(-1),
|
|
2111
|
+
passed,
|
|
2112
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
2113
|
+
thresholdMs,
|
|
2114
|
+
total: stopped.length
|
|
2115
|
+
};
|
|
2116
|
+
};
|
|
2117
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
2118
|
+
const listeners = new Set;
|
|
2119
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
2120
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2121
|
+
const events = [];
|
|
2122
|
+
const emit = () => {
|
|
2123
|
+
for (const listener of listeners) {
|
|
2124
|
+
listener();
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
const postEvent = (event) => {
|
|
2128
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
fetchImpl(options.path, {
|
|
2132
|
+
body: JSON.stringify(event),
|
|
2133
|
+
headers: {
|
|
2134
|
+
"Content-Type": "application/json"
|
|
2135
|
+
},
|
|
2136
|
+
method: "POST"
|
|
2137
|
+
}).catch(() => {});
|
|
2138
|
+
};
|
|
2139
|
+
const record = (status, input) => {
|
|
2140
|
+
const event = {
|
|
2141
|
+
at: Date.now(),
|
|
2142
|
+
id: createEventId(),
|
|
2143
|
+
latencyMs: input.latencyMs,
|
|
2144
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
2145
|
+
reason: input.reason,
|
|
2146
|
+
sessionId: input.sessionId,
|
|
2147
|
+
status,
|
|
2148
|
+
thresholdMs
|
|
2149
|
+
};
|
|
2150
|
+
events.push(event);
|
|
2151
|
+
postEvent(event);
|
|
2152
|
+
emit();
|
|
2153
|
+
return event;
|
|
2154
|
+
};
|
|
2155
|
+
return {
|
|
2156
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
2157
|
+
recordRequested: (input) => record("requested", input),
|
|
2158
|
+
recordSkipped: (input) => record("skipped", input),
|
|
2159
|
+
recordStopped: (input) => record("stopped", input),
|
|
2160
|
+
subscribe: (subscriber) => {
|
|
2161
|
+
listeners.add(subscriber);
|
|
2162
|
+
return () => {
|
|
2163
|
+
listeners.delete(subscriber);
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
};
|
|
2168
|
+
|
|
2169
|
+
// src/client/duplex.ts
|
|
2170
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
2171
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
2172
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
2173
|
+
let lastPartial = controller.partial;
|
|
2174
|
+
const interruptIfPlaying = (reason) => {
|
|
2175
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
2176
|
+
options.monitor?.recordSkipped({
|
|
2177
|
+
reason,
|
|
2178
|
+
sessionId: controller.sessionId
|
|
2179
|
+
});
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
options.monitor?.recordRequested({
|
|
2183
|
+
reason,
|
|
2184
|
+
sessionId: controller.sessionId
|
|
2185
|
+
});
|
|
2186
|
+
player.interrupt().then(() => {
|
|
2187
|
+
options.monitor?.recordStopped({
|
|
2188
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
2189
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
2190
|
+
reason,
|
|
2191
|
+
sessionId: controller.sessionId
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
};
|
|
2195
|
+
const unsubscribe = controller.subscribe(() => {
|
|
2196
|
+
if (options.interruptOnPartial === false) {
|
|
2197
|
+
lastPartial = controller.partial;
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (!lastPartial && controller.partial) {
|
|
2201
|
+
interruptIfPlaying("partial-transcript");
|
|
2202
|
+
}
|
|
2203
|
+
lastPartial = controller.partial;
|
|
2204
|
+
});
|
|
2205
|
+
return {
|
|
2206
|
+
close: () => {
|
|
2207
|
+
unsubscribe();
|
|
2208
|
+
},
|
|
2209
|
+
handleLevel: (level) => {
|
|
2210
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
2211
|
+
interruptIfPlaying("input-level");
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
sendAudio: (audio) => {
|
|
2215
|
+
interruptIfPlaying("manual-audio");
|
|
2216
|
+
controller.sendAudio(audio);
|
|
1103
2217
|
}
|
|
1104
2218
|
};
|
|
1105
2219
|
};
|
|
@@ -1126,8 +2240,7 @@ var DEFAULT_GUIDED_PROMPTS = [
|
|
|
1126
2240
|
"Now describe what you are trying to do or test.",
|
|
1127
2241
|
"Finish with any detail that feels blocked, risky, or unclear."
|
|
1128
2242
|
];
|
|
1129
|
-
var clamp = (value, min,
|
|
1130
|
-
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2243
|
+
var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
|
|
1131
2244
|
var readErrorField = (value, key) => {
|
|
1132
2245
|
const candidate = value[key];
|
|
1133
2246
|
if (typeof candidate === "string" && candidate.trim()) {
|
|
@@ -1160,6 +2273,17 @@ var formatErrorMessage = (error) => {
|
|
|
1160
2273
|
}
|
|
1161
2274
|
return "Unexpected error";
|
|
1162
2275
|
};
|
|
2276
|
+
var formatReconnectState = (reconnect) => {
|
|
2277
|
+
const pieces = [reconnect.status];
|
|
2278
|
+
if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
|
|
2279
|
+
pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
|
|
2280
|
+
}
|
|
2281
|
+
if (reconnect.nextAttemptAt) {
|
|
2282
|
+
const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
|
|
2283
|
+
pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
|
|
2284
|
+
}
|
|
2285
|
+
return pieces.join(" · ");
|
|
2286
|
+
};
|
|
1163
2287
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1164
2288
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1165
2289
|
const next = levels.slice(-(count - 1));
|
|
@@ -1216,6 +2340,20 @@ var parsePromptList = (value) => {
|
|
|
1216
2340
|
} catch {}
|
|
1217
2341
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1218
2342
|
};
|
|
2343
|
+
var parseOptionalNumber = (value) => {
|
|
2344
|
+
if (!value) {
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const parsed = Number(value);
|
|
2348
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2349
|
+
};
|
|
2350
|
+
var resolveElement2 = (root, selector, ctor) => {
|
|
2351
|
+
if (!selector) {
|
|
2352
|
+
return null;
|
|
2353
|
+
}
|
|
2354
|
+
const value = document.querySelector(selector);
|
|
2355
|
+
return value instanceof ctor ? value : null;
|
|
2356
|
+
};
|
|
1219
2357
|
var requireElement = (root, selector, ctor, name) => {
|
|
1220
2358
|
const value = selector ? document.querySelector(selector) : null;
|
|
1221
2359
|
if (value instanceof ctor) {
|
|
@@ -1266,11 +2404,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1266
2404
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1267
2405
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1268
2406
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
2407
|
+
const reconnectReportPath = root.dataset.voiceReconnectReportPath;
|
|
2408
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
2409
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
2410
|
+
path: bargeInPath,
|
|
2411
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
2412
|
+
}) : null;
|
|
2413
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
2414
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1269
2415
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1270
2416
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1271
2417
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1272
2418
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1273
2419
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
2420
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1274
2421
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1275
2422
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1276
2423
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1279,35 +2426,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1279
2426
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1280
2427
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1281
2428
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
2429
|
+
let activeMode = null;
|
|
2430
|
+
let hasStartedModes = {
|
|
2431
|
+
general: false,
|
|
2432
|
+
guided: false
|
|
2433
|
+
};
|
|
2434
|
+
let isCapturing = false;
|
|
2435
|
+
let micError = null;
|
|
2436
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
2437
|
+
let guidedBargeInBinding = null;
|
|
2438
|
+
let generalBargeInBinding = null;
|
|
1282
2439
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1283
2440
|
capture: {
|
|
2441
|
+
onAudio: (audio, sendAudio) => {
|
|
2442
|
+
if (guidedBargeInBinding) {
|
|
2443
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
sendAudio(audio);
|
|
2447
|
+
},
|
|
1284
2448
|
onLevel: (level) => {
|
|
2449
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1285
2450
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1286
2451
|
renderWave();
|
|
1287
2452
|
}
|
|
1288
2453
|
},
|
|
2454
|
+
connection: {
|
|
2455
|
+
reconnectReportPath
|
|
2456
|
+
},
|
|
1289
2457
|
preset: "guided-intake"
|
|
1290
2458
|
});
|
|
1291
2459
|
const generalVoice = createVoiceController(generalPath, {
|
|
1292
2460
|
capture: {
|
|
2461
|
+
onAudio: (audio, sendAudio) => {
|
|
2462
|
+
if (generalBargeInBinding) {
|
|
2463
|
+
generalBargeInBinding.sendAudio(audio);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
sendAudio(audio);
|
|
2467
|
+
},
|
|
1293
2468
|
onLevel: (level) => {
|
|
2469
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1294
2470
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1295
2471
|
renderWave();
|
|
1296
2472
|
}
|
|
1297
2473
|
},
|
|
2474
|
+
connection: {
|
|
2475
|
+
reconnectReportPath
|
|
2476
|
+
},
|
|
1298
2477
|
preset: "dictation"
|
|
1299
2478
|
});
|
|
1300
2479
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1301
2480
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2481
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
2482
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
2483
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
2484
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2485
|
+
monitor: bargeInMonitor ?? undefined
|
|
2486
|
+
});
|
|
2487
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
2488
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2489
|
+
monitor: bargeInMonitor ?? undefined
|
|
2490
|
+
});
|
|
1310
2491
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
2492
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1311
2493
|
const renderWave = () => {
|
|
1312
2494
|
const path = createVoiceWavePath(waveLevels);
|
|
1313
2495
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1319,9 +2501,12 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1319
2501
|
const render = () => {
|
|
1320
2502
|
const voice = currentVoice();
|
|
1321
2503
|
const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
|
|
1322
|
-
const status = voice
|
|
2504
|
+
const { status } = voice;
|
|
1323
2505
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1324
2506
|
errorStatus.textContent = micError || voice.error || "None";
|
|
2507
|
+
if (reconnectStatus) {
|
|
2508
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2509
|
+
}
|
|
1325
2510
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1326
2511
|
promptStatus.textContent = resolvePromptMessage({
|
|
1327
2512
|
guidedPrompts,
|
|
@@ -1385,8 +2570,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1385
2570
|
render();
|
|
1386
2571
|
}
|
|
1387
2572
|
};
|
|
1388
|
-
guidedVoice.subscribe(
|
|
1389
|
-
|
|
2573
|
+
guidedVoice.subscribe(() => {
|
|
2574
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
2575
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
2576
|
+
}
|
|
2577
|
+
render();
|
|
2578
|
+
});
|
|
2579
|
+
generalVoice.subscribe(() => {
|
|
2580
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
2581
|
+
generalAudioPlayer.start().catch(() => {});
|
|
2582
|
+
}
|
|
2583
|
+
render();
|
|
2584
|
+
});
|
|
1390
2585
|
startGuidedButton.addEventListener("click", () => {
|
|
1391
2586
|
startMode("guided");
|
|
1392
2587
|
});
|
|
@@ -1396,9 +2591,16 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1396
2591
|
stopButton.addEventListener("click", () => {
|
|
1397
2592
|
stopMic();
|
|
1398
2593
|
});
|
|
2594
|
+
root.addEventListener("absolute-voice-simulate-disconnect", () => {
|
|
2595
|
+
currentVoice().simulateDisconnect();
|
|
2596
|
+
});
|
|
1399
2597
|
window.addEventListener("beforeunload", () => {
|
|
1400
2598
|
guidedVoice.stopRecording();
|
|
1401
2599
|
generalVoice.stopRecording();
|
|
2600
|
+
guidedBargeInBinding?.close();
|
|
2601
|
+
generalBargeInBinding?.close();
|
|
2602
|
+
guidedAudioPlayer.close();
|
|
2603
|
+
generalAudioPlayer.close();
|
|
1402
2604
|
stopGuidedBinding();
|
|
1403
2605
|
stopGeneralBinding();
|
|
1404
2606
|
guidedVoice.close();
|