@absolutejs/voice 0.0.22-beta.34 → 0.0.22-beta.340
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/README.md +3361 -55
- package/dist/agent.d.ts +62 -0
- package/dist/agentSquadContract.d.ts +98 -0
- package/dist/angular/index.d.ts +16 -0
- package/dist/angular/index.js +4240 -1128
- package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
- package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
- package/dist/angular/voice-controller.service.d.ts +1 -0
- package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
- package/dist/angular/voice-delivery-runtime.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.component.d.ts +15 -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-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-readiness-failures.service.d.ts +13 -0
- package/dist/angular/voice-routing-status.service.d.ts +11 -0
- package/dist/angular/voice-stream.service.d.ts +1 -0
- 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-workflow-status.service.d.ts +12 -0
- package/dist/audit.d.ts +128 -0
- package/dist/auditDeliveryRoutes.d.ts +85 -0
- package/dist/auditExport.d.ts +34 -0
- package/dist/auditRoutes.d.ts +66 -0
- package/dist/auditSinks.d.ts +151 -0
- package/dist/bargeInRoutes.d.ts +56 -0
- package/dist/browserMediaRoutes.d.ts +62 -0
- package/dist/campaign.d.ts +768 -0
- package/dist/campaignDialers.d.ts +111 -0
- package/dist/client/actions.d.ts +83 -0
- package/dist/client/agentSquadStatus.d.ts +37 -0
- package/dist/client/agentSquadStatusWidget.d.ts +24 -0
- package/dist/client/bargeInMonitor.d.ts +7 -0
- package/dist/client/browserMedia.d.ts +8 -0
- package/dist/client/campaignDialerProof.d.ts +23 -0
- package/dist/client/deliveryRuntime.d.ts +34 -0
- package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
- package/dist/client/duplex.d.ts +1 -1
- package/dist/client/htmxBootstrap.js +937 -14
- package/dist/client/index.d.ts +72 -0
- package/dist/client/index.js +6523 -50
- 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/opsActionCenter.d.ts +54 -0
- package/dist/client/opsActionCenterWidget.d.ts +29 -0
- package/dist/client/opsActionHistory.d.ts +19 -0
- package/dist/client/opsActionHistoryWidget.d.ts +11 -0
- package/dist/client/opsStatus.d.ts +19 -0
- package/dist/client/opsStatusWidget.d.ts +40 -0
- package/dist/client/platformCoverage.d.ts +19 -0
- package/dist/client/platformCoverageWidget.d.ts +37 -0
- package/dist/client/proofTrends.d.ts +19 -0
- package/dist/client/proofTrendsWidget.d.ts +37 -0
- package/dist/client/providerCapabilities.d.ts +19 -0
- package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
- package/dist/client/providerContracts.d.ts +19 -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/providerStatusWidget.d.ts +32 -0
- package/dist/client/readinessFailures.d.ts +19 -0
- package/dist/client/readinessFailuresWidget.d.ts +42 -0
- package/dist/client/routingStatus.d.ts +19 -0
- package/dist/client/routingStatusWidget.d.ts +28 -0
- package/dist/client/traceTimeline.d.ts +19 -0
- package/dist/client/traceTimelineWidget.d.ts +36 -0
- package/dist/client/turnLatency.d.ts +22 -0
- package/dist/client/turnLatencyWidget.d.ts +33 -0
- package/dist/client/turnQuality.d.ts +19 -0
- package/dist/client/turnQualityWidget.d.ts +32 -0
- package/dist/client/workflowStatus.d.ts +19 -0
- package/dist/competitiveCoverage.d.ts +141 -0
- package/dist/dataControl.d.ts +180 -0
- package/dist/deliveryRuntime.d.ts +158 -0
- package/dist/deliverySinkRoutes.d.ts +117 -0
- package/dist/demoReadyRoutes.d.ts +98 -0
- package/dist/diagnosticsRoutes.d.ts +44 -0
- package/dist/evalRoutes.d.ts +219 -0
- package/dist/fileStore.d.ts +14 -2
- package/dist/guardrails.d.ts +128 -0
- package/dist/incidentBundle.d.ts +116 -0
- package/dist/index.d.ts +149 -14
- package/dist/index.js +30457 -5611
- package/dist/latencySlo.d.ts +56 -0
- package/dist/liveLatency.d.ts +78 -0
- package/dist/liveOps.d.ts +190 -0
- package/dist/mediaPipelineRoutes.d.ts +117 -0
- package/dist/modelAdapters.d.ts +54 -2
- package/dist/observabilityExport.d.ts +481 -0
- package/dist/openaiTTS.d.ts +18 -0
- package/dist/operationsRecord.d.ts +351 -0
- package/dist/opsActionAuditRoutes.d.ts +99 -0
- package/dist/opsConsoleRoutes.d.ts +80 -0
- package/dist/opsRecovery.d.ts +137 -0
- package/dist/opsStatus.d.ts +76 -0
- package/dist/opsStatusRoutes.d.ts +33 -0
- package/dist/outcomeContract.d.ts +146 -0
- package/dist/phoneAgent.d.ts +139 -0
- package/dist/phoneAgentProductionSmoke.d.ts +115 -0
- package/dist/platformCoverage.d.ts +91 -0
- package/dist/postCallAnalysis.d.ts +98 -0
- package/dist/postgresStore.d.ts +13 -2
- package/dist/productionReadiness.d.ts +611 -0
- package/dist/proofTrends.d.ts +325 -0
- package/dist/providerAdapters.d.ts +12 -1
- package/dist/providerCapabilities.d.ts +92 -0
- package/dist/providerDecisionTraces.d.ts +130 -0
- package/dist/providerOrchestration.d.ts +109 -0
- package/dist/providerRoutingContract.d.ts +71 -0
- package/dist/providerSlo.d.ts +142 -0
- package/dist/providerStackRecommendations.d.ts +187 -0
- package/dist/qualityRoutes.d.ts +76 -0
- package/dist/queue.d.ts +9 -0
- package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
- package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
- package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
- package/dist/react/VoiceOpsStatus.d.ts +6 -0
- package/dist/react/VoicePlatformCoverage.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/VoiceRoutingStatus.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/index.d.ts +32 -0
- package/dist/react/index.js +6293 -31
- package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
- package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
- package/dist/react/useVoiceController.d.ts +1 -0
- 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/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/useVoiceReadinessFailures.d.ts +8 -0
- package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/react/useVoiceStream.d.ts +1 -0
- 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 +8 -0
- package/dist/readinessProfiles.d.ts +38 -0
- package/dist/realtimeChannel.d.ts +136 -0
- package/dist/realtimeProviderContracts.d.ts +133 -0
- package/dist/reconnectContract.d.ts +88 -0
- package/dist/resilienceRoutes.d.ts +143 -0
- package/dist/sessionReplay.d.ts +12 -0
- package/dist/simulationSuite.d.ts +143 -0
- package/dist/sloCalibration.d.ts +185 -0
- package/dist/sqliteStore.d.ts +13 -2
- package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
- package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
- package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
- package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
- package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
- package/dist/svelte/createVoicePlatformCoverage.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 +4 -2
- package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
- package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
- 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/createVoiceWorkflowStatus.d.ts +8 -0
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.js +5666 -420
- 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/security.d.ts +182 -0
- package/dist/telephony/telnyx.d.ts +291 -0
- package/dist/telephony/twilio.d.ts +136 -2
- package/dist/telephonyMediaRoutes.d.ts +72 -0
- package/dist/telephonyOutcome.d.ts +273 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +5321 -153
- package/dist/testing/ioProviderSimulator.d.ts +41 -0
- package/dist/testing/telephony.d.ts +25 -0
- package/dist/toolContract.d.ts +161 -0
- package/dist/toolRuntime.d.ts +50 -0
- package/dist/trace.d.ts +19 -1
- package/dist/traceDeliveryRoutes.d.ts +86 -0
- package/dist/traceTimeline.d.ts +97 -0
- package/dist/turnLatency.d.ts +95 -0
- package/dist/turnQuality.d.ts +94 -0
- package/dist/types.d.ts +118 -3
- package/dist/voiceMonitoring.d.ts +444 -0
- package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
- package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
- package/dist/vue/VoiceOpsStatus.d.ts +30 -0
- package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
- package/dist/vue/VoiceProofTrends.d.ts +21 -0
- package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
- package/dist/vue/VoiceProviderContracts.d.ts +21 -0
- package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
- package/dist/vue/VoiceProviderStatus.d.ts +51 -0
- package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
- package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
- package/dist/vue/VoiceTurnLatency.d.ts +69 -0
- package/dist/vue/VoiceTurnQuality.d.ts +51 -0
- package/dist/vue/index.d.ts +30 -0
- package/dist/vue/index.js +6062 -56
- package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
- package/dist/vue/useVoiceController.d.ts +2 -1
- 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/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 +1 -1
- package/dist/vue/useVoiceReadinessFailures.d.ts +873 -0
- package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/vue/useVoiceStream.d.ts +2 -1
- 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 +9 -0
- package/dist/workflowContract.d.ts +91 -0
- package/package.json +4 -1
|
@@ -188,6 +188,11 @@ var serverMessageToAction = (message) => {
|
|
|
188
188
|
sessionId: message.sessionId,
|
|
189
189
|
type: "complete"
|
|
190
190
|
};
|
|
191
|
+
case "connection":
|
|
192
|
+
return {
|
|
193
|
+
reconnect: message.reconnect,
|
|
194
|
+
type: "connection"
|
|
195
|
+
};
|
|
191
196
|
case "call_lifecycle":
|
|
192
197
|
return {
|
|
193
198
|
event: message.event,
|
|
@@ -209,6 +214,17 @@ var serverMessageToAction = (message) => {
|
|
|
209
214
|
transcript: message.transcript,
|
|
210
215
|
type: "partial"
|
|
211
216
|
};
|
|
217
|
+
case "replay":
|
|
218
|
+
return {
|
|
219
|
+
assistantTexts: message.assistantTexts,
|
|
220
|
+
call: message.call,
|
|
221
|
+
partial: message.partial,
|
|
222
|
+
scenarioId: message.scenarioId,
|
|
223
|
+
sessionId: message.sessionId,
|
|
224
|
+
status: message.status,
|
|
225
|
+
turns: message.turns,
|
|
226
|
+
type: "replay"
|
|
227
|
+
};
|
|
212
228
|
case "session":
|
|
213
229
|
return {
|
|
214
230
|
sessionId: message.sessionId,
|
|
@@ -226,6 +242,232 @@ var serverMessageToAction = (message) => {
|
|
|
226
242
|
}
|
|
227
243
|
};
|
|
228
244
|
|
|
245
|
+
// node_modules/@absolutejs/media/dist/index.js
|
|
246
|
+
var pushIssue = (issues, severity, code, message) => {
|
|
247
|
+
issues.push({ code, message, severity });
|
|
248
|
+
};
|
|
249
|
+
var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
|
|
250
|
+
var max = (values) => values.length === 0 ? undefined : Math.max(...values);
|
|
251
|
+
var numericStat = (stat, key) => {
|
|
252
|
+
const value = stat[key];
|
|
253
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
254
|
+
};
|
|
255
|
+
var booleanStat = (stat, key) => {
|
|
256
|
+
const value = stat[key];
|
|
257
|
+
return typeof value === "boolean" ? value : undefined;
|
|
258
|
+
};
|
|
259
|
+
var stringStat = (stat, key) => {
|
|
260
|
+
const value = stat[key];
|
|
261
|
+
return typeof value === "string" ? value : undefined;
|
|
262
|
+
};
|
|
263
|
+
var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
|
|
264
|
+
var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
265
|
+
var normalizeWebRTCStat = (stat) => {
|
|
266
|
+
const sample = {};
|
|
267
|
+
for (const [key, value] of Object.entries(stat)) {
|
|
268
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
269
|
+
sample[key] = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return sample;
|
|
273
|
+
};
|
|
274
|
+
var buildMediaWebRTCStatsReport = (input = {}) => {
|
|
275
|
+
const stats = input.stats ?? [];
|
|
276
|
+
const issues = [];
|
|
277
|
+
const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
278
|
+
const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
279
|
+
const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
|
|
280
|
+
const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
|
|
281
|
+
const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
|
|
282
|
+
const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
|
|
283
|
+
const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
|
|
284
|
+
const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
|
|
285
|
+
const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
|
|
286
|
+
const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
|
|
287
|
+
const packetLossDenominator = inboundPackets + packetsLost;
|
|
288
|
+
const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
|
|
289
|
+
const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
|
|
290
|
+
const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
|
|
291
|
+
const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
|
|
292
|
+
const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
|
|
293
|
+
const jitterBufferDelayMs = max(inbound.map((stat) => {
|
|
294
|
+
const delay = numericStat(stat, "jitterBufferDelay");
|
|
295
|
+
const emitted = numericStat(stat, "jitterBufferEmittedCount");
|
|
296
|
+
return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
|
|
297
|
+
}).filter((value) => value !== undefined));
|
|
298
|
+
const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
|
|
299
|
+
if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
|
|
300
|
+
pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
|
|
301
|
+
}
|
|
302
|
+
if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
|
|
303
|
+
pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
|
|
304
|
+
}
|
|
305
|
+
if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
|
|
306
|
+
pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
|
|
307
|
+
}
|
|
308
|
+
if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
|
|
309
|
+
pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
|
|
310
|
+
}
|
|
311
|
+
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
312
|
+
pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
activeCandidatePairs,
|
|
316
|
+
audioLevelAverage: average(audioLevels),
|
|
317
|
+
bytesReceived,
|
|
318
|
+
bytesSent,
|
|
319
|
+
checkedAt: Date.now(),
|
|
320
|
+
endedAudioTracks,
|
|
321
|
+
inboundPackets,
|
|
322
|
+
issues,
|
|
323
|
+
jitterBufferDelayMs,
|
|
324
|
+
jitterMs,
|
|
325
|
+
liveAudioTracks,
|
|
326
|
+
outboundPackets,
|
|
327
|
+
packetLossRatio,
|
|
328
|
+
packetsLost,
|
|
329
|
+
roundTripTimeMs,
|
|
330
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
331
|
+
totalStats: stats.length
|
|
332
|
+
};
|
|
333
|
+
};
|
|
334
|
+
var collectMediaWebRTCStats = async (input) => {
|
|
335
|
+
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
336
|
+
return [...report.values()].map(normalizeWebRTCStat);
|
|
337
|
+
};
|
|
338
|
+
var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
|
|
339
|
+
const stats = input.stats ?? [];
|
|
340
|
+
const previousStats = input.previousStats ?? [];
|
|
341
|
+
const issues = [];
|
|
342
|
+
const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
|
|
343
|
+
const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
|
|
344
|
+
const streams = audioRtp.map((stat) => {
|
|
345
|
+
const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
|
|
346
|
+
const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
|
|
347
|
+
const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
|
|
348
|
+
const previous = previousByKey.get(statKey(stat));
|
|
349
|
+
const currentPackets = numericStat(stat, packetsKey);
|
|
350
|
+
const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
|
|
351
|
+
const currentBytes = numericStat(stat, bytesKey);
|
|
352
|
+
const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
|
|
353
|
+
const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
|
|
354
|
+
return {
|
|
355
|
+
bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
|
|
356
|
+
currentPackets,
|
|
357
|
+
direction,
|
|
358
|
+
id: statKey(stat),
|
|
359
|
+
packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
|
|
360
|
+
previousPackets,
|
|
361
|
+
timeDeltaMs
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
const inbound = streams.filter((stream) => stream.direction === "inbound");
|
|
365
|
+
const outbound = streams.filter((stream) => stream.direction === "outbound");
|
|
366
|
+
const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
|
|
367
|
+
const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
368
|
+
const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
369
|
+
if (input.requireInboundAudio && inbound.length === 0) {
|
|
370
|
+
pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
|
|
371
|
+
}
|
|
372
|
+
if (input.requireOutboundAudio && outbound.length === 0) {
|
|
373
|
+
pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
|
|
374
|
+
}
|
|
375
|
+
if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
|
|
376
|
+
pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
|
|
377
|
+
}
|
|
378
|
+
if (stalledInboundStreams > 0) {
|
|
379
|
+
pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
|
|
380
|
+
}
|
|
381
|
+
if (stalledOutboundStreams > 0) {
|
|
382
|
+
pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
checkedAt: Date.now(),
|
|
386
|
+
inboundAudioStreams: inbound.length,
|
|
387
|
+
issues,
|
|
388
|
+
maxObservedGapMs,
|
|
389
|
+
outboundAudioStreams: outbound.length,
|
|
390
|
+
stalledInboundStreams,
|
|
391
|
+
stalledOutboundStreams,
|
|
392
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
393
|
+
streams,
|
|
394
|
+
totalStats: stats.length
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/client/browserMedia.ts
|
|
399
|
+
var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
|
|
400
|
+
var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
|
|
401
|
+
var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
|
|
402
|
+
var postBrowserMediaReport = async (payload, options) => {
|
|
403
|
+
const requestFetch = options.fetch ?? globalThis.fetch;
|
|
404
|
+
if (!requestFetch) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
|
|
408
|
+
body: JSON.stringify(payload),
|
|
409
|
+
headers: {
|
|
410
|
+
"Content-Type": "application/json"
|
|
411
|
+
},
|
|
412
|
+
keepalive: true,
|
|
413
|
+
method: "POST"
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
var createVoiceBrowserMediaReporter = (options) => {
|
|
417
|
+
let interval = null;
|
|
418
|
+
let previousStats = [];
|
|
419
|
+
const reportOnce = async () => {
|
|
420
|
+
const peerConnection = await resolvePeerConnection(options);
|
|
421
|
+
if (!peerConnection) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const stats = await collectMediaWebRTCStats({ peerConnection });
|
|
425
|
+
const report = buildMediaWebRTCStatsReport({
|
|
426
|
+
...options,
|
|
427
|
+
stats
|
|
428
|
+
});
|
|
429
|
+
const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
|
|
430
|
+
...options.continuity,
|
|
431
|
+
previousStats,
|
|
432
|
+
stats
|
|
433
|
+
});
|
|
434
|
+
const payload = {
|
|
435
|
+
at: Date.now(),
|
|
436
|
+
continuity,
|
|
437
|
+
report,
|
|
438
|
+
scenarioId: options.getScenarioId?.() ?? null,
|
|
439
|
+
sessionId: options.getSessionId?.() ?? null
|
|
440
|
+
};
|
|
441
|
+
previousStats = stats;
|
|
442
|
+
options.onReport?.(payload);
|
|
443
|
+
await postBrowserMediaReport(payload, options);
|
|
444
|
+
return payload;
|
|
445
|
+
};
|
|
446
|
+
const run = () => {
|
|
447
|
+
reportOnce().catch((error) => {
|
|
448
|
+
options.onError?.(error);
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
const stop = () => {
|
|
452
|
+
if (interval) {
|
|
453
|
+
clearInterval(interval);
|
|
454
|
+
interval = null;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
return {
|
|
458
|
+
close: stop,
|
|
459
|
+
reportOnce,
|
|
460
|
+
start: () => {
|
|
461
|
+
if (interval) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
run();
|
|
465
|
+
interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
|
|
466
|
+
},
|
|
467
|
+
stop
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
|
|
229
471
|
// src/client/connection.ts
|
|
230
472
|
var WS_OPEN = 1;
|
|
231
473
|
var WS_CLOSED = 3;
|
|
@@ -269,10 +511,12 @@ var isVoiceServerMessage = (value) => {
|
|
|
269
511
|
case "assistant":
|
|
270
512
|
case "call_lifecycle":
|
|
271
513
|
case "complete":
|
|
514
|
+
case "connection":
|
|
272
515
|
case "error":
|
|
273
516
|
case "final":
|
|
274
517
|
case "partial":
|
|
275
518
|
case "pong":
|
|
519
|
+
case "replay":
|
|
276
520
|
case "session":
|
|
277
521
|
case "turn":
|
|
278
522
|
return true;
|
|
@@ -309,6 +553,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
309
553
|
sessionId: options.sessionId ?? createSessionId(),
|
|
310
554
|
ws: null
|
|
311
555
|
};
|
|
556
|
+
const emitConnection = (reconnect) => {
|
|
557
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
558
|
+
};
|
|
312
559
|
const clearTimers = () => {
|
|
313
560
|
if (state.pingInterval) {
|
|
314
561
|
clearInterval(state.pingInterval);
|
|
@@ -331,9 +578,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
331
578
|
}
|
|
332
579
|
};
|
|
333
580
|
const scheduleReconnect = () => {
|
|
581
|
+
const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
|
|
334
582
|
state.reconnectAttempts += 1;
|
|
583
|
+
emitConnection({
|
|
584
|
+
reconnect: {
|
|
585
|
+
attempts: state.reconnectAttempts,
|
|
586
|
+
lastDisconnectAt: Date.now(),
|
|
587
|
+
maxAttempts: maxReconnectAttempts,
|
|
588
|
+
nextAttemptAt,
|
|
589
|
+
status: "reconnecting"
|
|
590
|
+
},
|
|
591
|
+
type: "connection"
|
|
592
|
+
});
|
|
335
593
|
state.reconnectTimeout = setTimeout(() => {
|
|
336
594
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
595
|
+
emitConnection({
|
|
596
|
+
reconnect: {
|
|
597
|
+
attempts: state.reconnectAttempts,
|
|
598
|
+
maxAttempts: maxReconnectAttempts,
|
|
599
|
+
status: "exhausted"
|
|
600
|
+
},
|
|
601
|
+
type: "connection"
|
|
602
|
+
});
|
|
337
603
|
return;
|
|
338
604
|
}
|
|
339
605
|
connect();
|
|
@@ -343,9 +609,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
343
609
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
|
|
344
610
|
ws.binaryType = "arraybuffer";
|
|
345
611
|
ws.onopen = () => {
|
|
612
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
346
613
|
state.isConnected = true;
|
|
347
|
-
state.reconnectAttempts = 0;
|
|
348
614
|
flushPendingMessages();
|
|
615
|
+
if (wasReconnecting) {
|
|
616
|
+
emitConnection({
|
|
617
|
+
reconnect: {
|
|
618
|
+
attempts: state.reconnectAttempts,
|
|
619
|
+
lastResumedAt: Date.now(),
|
|
620
|
+
maxAttempts: maxReconnectAttempts,
|
|
621
|
+
status: "resumed"
|
|
622
|
+
},
|
|
623
|
+
type: "connection"
|
|
624
|
+
});
|
|
625
|
+
state.reconnectAttempts = 0;
|
|
626
|
+
}
|
|
349
627
|
listeners.forEach((listener) => listener({
|
|
350
628
|
scenarioId: state.scenarioId ?? undefined,
|
|
351
629
|
sessionId: state.sessionId,
|
|
@@ -375,6 +653,16 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
375
653
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
|
|
376
654
|
if (reconnectable) {
|
|
377
655
|
scheduleReconnect();
|
|
656
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
657
|
+
emitConnection({
|
|
658
|
+
reconnect: {
|
|
659
|
+
attempts: state.reconnectAttempts,
|
|
660
|
+
lastDisconnectAt: Date.now(),
|
|
661
|
+
maxAttempts: maxReconnectAttempts,
|
|
662
|
+
status: "exhausted"
|
|
663
|
+
},
|
|
664
|
+
type: "connection"
|
|
665
|
+
});
|
|
378
666
|
}
|
|
379
667
|
};
|
|
380
668
|
state.ws = ws;
|
|
@@ -445,6 +733,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
445
733
|
};
|
|
446
734
|
|
|
447
735
|
// src/client/store.ts
|
|
736
|
+
var createInitialReconnectState = () => ({
|
|
737
|
+
attempts: 0,
|
|
738
|
+
maxAttempts: 0,
|
|
739
|
+
status: "idle"
|
|
740
|
+
});
|
|
448
741
|
var createInitialState = () => ({
|
|
449
742
|
assistantAudio: [],
|
|
450
743
|
assistantTexts: [],
|
|
@@ -453,6 +746,7 @@ var createInitialState = () => ({
|
|
|
453
746
|
isConnected: false,
|
|
454
747
|
scenarioId: null,
|
|
455
748
|
partial: "",
|
|
749
|
+
reconnect: createInitialReconnectState(),
|
|
456
750
|
sessionId: null,
|
|
457
751
|
status: "idle",
|
|
458
752
|
turns: []
|
|
@@ -509,7 +803,19 @@ var createVoiceStreamStore = () => {
|
|
|
509
803
|
case "connected":
|
|
510
804
|
state = {
|
|
511
805
|
...state,
|
|
512
|
-
isConnected: true
|
|
806
|
+
isConnected: true,
|
|
807
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
808
|
+
...state.reconnect,
|
|
809
|
+
lastResumedAt: Date.now(),
|
|
810
|
+
nextAttemptAt: undefined,
|
|
811
|
+
status: "resumed"
|
|
812
|
+
} : state.reconnect
|
|
813
|
+
};
|
|
814
|
+
break;
|
|
815
|
+
case "connection":
|
|
816
|
+
state = {
|
|
817
|
+
...state,
|
|
818
|
+
reconnect: action.reconnect
|
|
513
819
|
};
|
|
514
820
|
break;
|
|
515
821
|
case "disconnected":
|
|
@@ -537,6 +843,26 @@ var createVoiceStreamStore = () => {
|
|
|
537
843
|
partial: action.transcript.text
|
|
538
844
|
};
|
|
539
845
|
break;
|
|
846
|
+
case "replay":
|
|
847
|
+
state = {
|
|
848
|
+
...state,
|
|
849
|
+
assistantTexts: [...action.assistantTexts],
|
|
850
|
+
call: action.call ?? null,
|
|
851
|
+
error: null,
|
|
852
|
+
isConnected: action.status === "active",
|
|
853
|
+
partial: action.partial,
|
|
854
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
855
|
+
...state.reconnect,
|
|
856
|
+
lastResumedAt: Date.now(),
|
|
857
|
+
nextAttemptAt: undefined,
|
|
858
|
+
status: "resumed"
|
|
859
|
+
} : state.reconnect,
|
|
860
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
861
|
+
sessionId: action.sessionId,
|
|
862
|
+
status: action.status,
|
|
863
|
+
turns: [...action.turns]
|
|
864
|
+
};
|
|
865
|
+
break;
|
|
540
866
|
case "session":
|
|
541
867
|
state = {
|
|
542
868
|
...state,
|
|
@@ -574,20 +900,50 @@ var createVoiceStreamStore = () => {
|
|
|
574
900
|
var createVoiceStream = (path, options = {}) => {
|
|
575
901
|
const connection = createVoiceConnection(path, options);
|
|
576
902
|
const store = createVoiceStreamStore();
|
|
903
|
+
const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
|
|
904
|
+
...options.browserMedia,
|
|
905
|
+
getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
|
|
906
|
+
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
907
|
+
}) : null;
|
|
577
908
|
const subscribers = new Set;
|
|
578
909
|
const start = (input) => Promise.resolve().then(() => {
|
|
579
910
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
580
911
|
return;
|
|
581
912
|
}
|
|
582
913
|
connection.start(input);
|
|
914
|
+
browserMediaReporter?.start();
|
|
583
915
|
});
|
|
584
916
|
const notify = () => {
|
|
585
917
|
subscribers.forEach((subscriber) => subscriber());
|
|
586
918
|
};
|
|
919
|
+
const reportReconnect = () => {
|
|
920
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const snapshot = store.getSnapshot();
|
|
924
|
+
const body = JSON.stringify({
|
|
925
|
+
at: Date.now(),
|
|
926
|
+
reconnect: snapshot.reconnect,
|
|
927
|
+
scenarioId: snapshot.scenarioId,
|
|
928
|
+
sessionId: connection.getSessionId(),
|
|
929
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
930
|
+
});
|
|
931
|
+
fetch(options.reconnectReportPath, {
|
|
932
|
+
body,
|
|
933
|
+
headers: {
|
|
934
|
+
"Content-Type": "application/json"
|
|
935
|
+
},
|
|
936
|
+
keepalive: true,
|
|
937
|
+
method: "POST"
|
|
938
|
+
}).catch(() => {});
|
|
939
|
+
};
|
|
587
940
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
588
941
|
const action = serverMessageToAction(message);
|
|
589
942
|
if (action) {
|
|
590
943
|
store.dispatch(action);
|
|
944
|
+
if (message.type === "connection") {
|
|
945
|
+
reportReconnect();
|
|
946
|
+
}
|
|
591
947
|
notify();
|
|
592
948
|
}
|
|
593
949
|
});
|
|
@@ -597,6 +953,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
597
953
|
},
|
|
598
954
|
close() {
|
|
599
955
|
unsubscribeConnection();
|
|
956
|
+
browserMediaReporter?.close();
|
|
600
957
|
connection.close();
|
|
601
958
|
store.dispatch({ type: "disconnected" });
|
|
602
959
|
notify();
|
|
@@ -623,6 +980,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
623
980
|
get partial() {
|
|
624
981
|
return store.getSnapshot().partial;
|
|
625
982
|
},
|
|
983
|
+
get reconnect() {
|
|
984
|
+
return store.getSnapshot().reconnect;
|
|
985
|
+
},
|
|
626
986
|
get sessionId() {
|
|
627
987
|
return connection.getSessionId();
|
|
628
988
|
},
|
|
@@ -941,6 +1301,7 @@ var createInitialState2 = (stream) => ({
|
|
|
941
1301
|
isConnected: stream.isConnected,
|
|
942
1302
|
isRecording: false,
|
|
943
1303
|
partial: stream.partial,
|
|
1304
|
+
reconnect: stream.reconnect,
|
|
944
1305
|
recordingError: null,
|
|
945
1306
|
sessionId: stream.sessionId,
|
|
946
1307
|
scenarioId: stream.scenarioId,
|
|
@@ -970,6 +1331,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
970
1331
|
error: stream.error,
|
|
971
1332
|
isConnected: stream.isConnected,
|
|
972
1333
|
partial: stream.partial,
|
|
1334
|
+
reconnect: stream.reconnect,
|
|
973
1335
|
sessionId: stream.sessionId,
|
|
974
1336
|
scenarioId: stream.scenarioId,
|
|
975
1337
|
status: stream.status,
|
|
@@ -994,7 +1356,13 @@ var createVoiceController = (path, options = {}) => {
|
|
|
994
1356
|
capture = createMicrophoneCapture({
|
|
995
1357
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
996
1358
|
onLevel: options.capture?.onLevel,
|
|
997
|
-
onAudio: (audio) =>
|
|
1359
|
+
onAudio: (audio) => {
|
|
1360
|
+
if (options.capture?.onAudio) {
|
|
1361
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
stream.sendAudio(audio);
|
|
1365
|
+
},
|
|
998
1366
|
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
|
|
999
1367
|
});
|
|
1000
1368
|
return capture;
|
|
@@ -1064,6 +1432,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1064
1432
|
get recordingError() {
|
|
1065
1433
|
return state.recordingError;
|
|
1066
1434
|
},
|
|
1435
|
+
get reconnect() {
|
|
1436
|
+
return state.reconnect;
|
|
1437
|
+
},
|
|
1067
1438
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1068
1439
|
get sessionId() {
|
|
1069
1440
|
return state.sessionId;
|
|
@@ -1104,6 +1475,475 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1104
1475
|
};
|
|
1105
1476
|
};
|
|
1106
1477
|
|
|
1478
|
+
// src/client/audioPlayer.ts
|
|
1479
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1480
|
+
var createInitialState3 = () => ({
|
|
1481
|
+
activeSourceCount: 0,
|
|
1482
|
+
error: null,
|
|
1483
|
+
isActive: false,
|
|
1484
|
+
isPlaying: false,
|
|
1485
|
+
lastInterruptLatencyMs: undefined,
|
|
1486
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
1487
|
+
processedChunkCount: 0,
|
|
1488
|
+
queuedChunkCount: 0
|
|
1489
|
+
});
|
|
1490
|
+
var getAudioContextCtor = () => {
|
|
1491
|
+
if (typeof window === "undefined") {
|
|
1492
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
1493
|
+
}
|
|
1494
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
1495
|
+
};
|
|
1496
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
1497
|
+
const format = chunk.format;
|
|
1498
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
1499
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
1500
|
+
}
|
|
1501
|
+
const bytes = chunk.chunk;
|
|
1502
|
+
const channels = Math.max(1, format.channels);
|
|
1503
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
1504
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
1505
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
1506
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1507
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1508
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
1509
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
1510
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
1511
|
+
const sampleOffset = sampleIndex * 2;
|
|
1512
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
1513
|
+
channelData[frameIndex] = 0;
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return audioBuffer;
|
|
1520
|
+
};
|
|
1521
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
1522
|
+
const subscribers = new Set;
|
|
1523
|
+
const sourceNodes = new Set;
|
|
1524
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
1525
|
+
let state = createInitialState3();
|
|
1526
|
+
let audioContext = null;
|
|
1527
|
+
let outputNode = null;
|
|
1528
|
+
let queueEndTime = 0;
|
|
1529
|
+
let syncPromise = Promise.resolve();
|
|
1530
|
+
let interruptStartedAt = null;
|
|
1531
|
+
let interruptPromise = null;
|
|
1532
|
+
let resolveInterruptPromise = null;
|
|
1533
|
+
let interruptFallbackTimer = null;
|
|
1534
|
+
const notify = () => {
|
|
1535
|
+
for (const subscriber of subscribers) {
|
|
1536
|
+
subscriber();
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
const setState = (next) => {
|
|
1540
|
+
state = {
|
|
1541
|
+
...state,
|
|
1542
|
+
...next
|
|
1543
|
+
};
|
|
1544
|
+
notify();
|
|
1545
|
+
};
|
|
1546
|
+
const clearError = () => {
|
|
1547
|
+
if (state.error !== null) {
|
|
1548
|
+
setState({ error: null });
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
const clearInterruptTimer = () => {
|
|
1552
|
+
if (interruptFallbackTimer !== null) {
|
|
1553
|
+
clearTimeout(interruptFallbackTimer);
|
|
1554
|
+
interruptFallbackTimer = null;
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
const resolveInterrupt = (latencyMs) => {
|
|
1558
|
+
clearInterruptTimer();
|
|
1559
|
+
interruptStartedAt = null;
|
|
1560
|
+
setState({
|
|
1561
|
+
activeSourceCount: sourceNodes.size,
|
|
1562
|
+
isPlaying: false,
|
|
1563
|
+
lastInterruptLatencyMs: latencyMs,
|
|
1564
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
1565
|
+
});
|
|
1566
|
+
resolveInterruptPromise?.();
|
|
1567
|
+
resolveInterruptPromise = null;
|
|
1568
|
+
interruptPromise = null;
|
|
1569
|
+
};
|
|
1570
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
1571
|
+
if (!context) {
|
|
1572
|
+
return 0;
|
|
1573
|
+
}
|
|
1574
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
1575
|
+
};
|
|
1576
|
+
const restoreOutputGain = (context) => {
|
|
1577
|
+
if (!outputNode) {
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const gainValue = 1;
|
|
1581
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1582
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
outputNode.gain.value = gainValue;
|
|
1586
|
+
};
|
|
1587
|
+
const muteOutputGain = (context) => {
|
|
1588
|
+
if (!outputNode) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const gainValue = 0;
|
|
1592
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1593
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
outputNode.gain.value = gainValue;
|
|
1597
|
+
};
|
|
1598
|
+
const maybeResolveInterrupt = () => {
|
|
1599
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
1603
|
+
};
|
|
1604
|
+
const ensureAudioContext = async () => {
|
|
1605
|
+
if (audioContext) {
|
|
1606
|
+
return audioContext;
|
|
1607
|
+
}
|
|
1608
|
+
if (options.createAudioContext) {
|
|
1609
|
+
audioContext = options.createAudioContext();
|
|
1610
|
+
} else {
|
|
1611
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
1612
|
+
if (!AudioContextCtor) {
|
|
1613
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
1614
|
+
}
|
|
1615
|
+
audioContext = new AudioContextCtor;
|
|
1616
|
+
}
|
|
1617
|
+
if (audioContext.createGain) {
|
|
1618
|
+
outputNode = audioContext.createGain();
|
|
1619
|
+
outputNode.connect?.(audioContext.destination);
|
|
1620
|
+
}
|
|
1621
|
+
queueEndTime = audioContext.currentTime;
|
|
1622
|
+
return audioContext;
|
|
1623
|
+
};
|
|
1624
|
+
const scheduleChunk = async (chunk) => {
|
|
1625
|
+
const context = await ensureAudioContext();
|
|
1626
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
1627
|
+
const node = context.createBufferSource();
|
|
1628
|
+
node.buffer = buffer;
|
|
1629
|
+
node.connect(outputNode ?? context.destination);
|
|
1630
|
+
node.onended = () => {
|
|
1631
|
+
sourceNodes.delete(node);
|
|
1632
|
+
node.disconnect?.();
|
|
1633
|
+
setState({
|
|
1634
|
+
activeSourceCount: sourceNodes.size,
|
|
1635
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
1636
|
+
});
|
|
1637
|
+
maybeResolveInterrupt();
|
|
1638
|
+
};
|
|
1639
|
+
const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
|
|
1640
|
+
queueEndTime = startAt + buffer.duration;
|
|
1641
|
+
sourceNodes.add(node);
|
|
1642
|
+
setState({
|
|
1643
|
+
activeSourceCount: sourceNodes.size,
|
|
1644
|
+
isPlaying: true
|
|
1645
|
+
});
|
|
1646
|
+
node.start(startAt);
|
|
1647
|
+
};
|
|
1648
|
+
const stopQueuedPlayback = (options2) => {
|
|
1649
|
+
for (const node of [...sourceNodes]) {
|
|
1650
|
+
node.stop?.();
|
|
1651
|
+
}
|
|
1652
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
1653
|
+
if (options2?.forceClear) {
|
|
1654
|
+
for (const node of sourceNodes) {
|
|
1655
|
+
node.disconnect?.();
|
|
1656
|
+
}
|
|
1657
|
+
sourceNodes.clear();
|
|
1658
|
+
maybeResolveInterrupt();
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
const sync = async () => {
|
|
1662
|
+
if (!state.isActive) {
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
1666
|
+
if (nextChunks.length === 0) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
try {
|
|
1670
|
+
clearError();
|
|
1671
|
+
for (const chunk of nextChunks) {
|
|
1672
|
+
await scheduleChunk(chunk);
|
|
1673
|
+
}
|
|
1674
|
+
setState({
|
|
1675
|
+
processedChunkCount: source.assistantAudio.length,
|
|
1676
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
1677
|
+
});
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
setState({
|
|
1680
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
const queueSync = () => {
|
|
1685
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
1686
|
+
return syncPromise;
|
|
1687
|
+
};
|
|
1688
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
1689
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
1690
|
+
player.start();
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (state.isActive) {
|
|
1694
|
+
queueSync();
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
const player = {
|
|
1698
|
+
close: async () => {
|
|
1699
|
+
unsubscribeSource();
|
|
1700
|
+
stopQueuedPlayback({ forceClear: true });
|
|
1701
|
+
clearInterruptTimer();
|
|
1702
|
+
resolveInterruptPromise?.();
|
|
1703
|
+
resolveInterruptPromise = null;
|
|
1704
|
+
interruptPromise = null;
|
|
1705
|
+
interruptStartedAt = null;
|
|
1706
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
1707
|
+
await audioContext.close();
|
|
1708
|
+
}
|
|
1709
|
+
audioContext = null;
|
|
1710
|
+
outputNode?.disconnect?.();
|
|
1711
|
+
outputNode = null;
|
|
1712
|
+
queueEndTime = 0;
|
|
1713
|
+
setState({
|
|
1714
|
+
activeSourceCount: 0,
|
|
1715
|
+
isActive: false,
|
|
1716
|
+
isPlaying: false
|
|
1717
|
+
});
|
|
1718
|
+
},
|
|
1719
|
+
get activeSourceCount() {
|
|
1720
|
+
return state.activeSourceCount;
|
|
1721
|
+
},
|
|
1722
|
+
get error() {
|
|
1723
|
+
return state.error;
|
|
1724
|
+
},
|
|
1725
|
+
getSnapshot: () => state,
|
|
1726
|
+
get isActive() {
|
|
1727
|
+
return state.isActive;
|
|
1728
|
+
},
|
|
1729
|
+
get isPlaying() {
|
|
1730
|
+
return state.isPlaying;
|
|
1731
|
+
},
|
|
1732
|
+
interrupt: async () => {
|
|
1733
|
+
const startedAt = Date.now();
|
|
1734
|
+
const context = await ensureAudioContext();
|
|
1735
|
+
interruptStartedAt = startedAt;
|
|
1736
|
+
muteOutputGain(context);
|
|
1737
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
1738
|
+
setState({
|
|
1739
|
+
isActive: false,
|
|
1740
|
+
isPlaying: sourceNodes.size > 0,
|
|
1741
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
1742
|
+
});
|
|
1743
|
+
if (sourceNodes.size === 0) {
|
|
1744
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (!interruptPromise) {
|
|
1748
|
+
interruptPromise = new Promise((resolve) => {
|
|
1749
|
+
resolveInterruptPromise = resolve;
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
clearInterruptTimer();
|
|
1753
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
1754
|
+
for (const node of sourceNodes) {
|
|
1755
|
+
node.disconnect?.();
|
|
1756
|
+
}
|
|
1757
|
+
sourceNodes.clear();
|
|
1758
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
1759
|
+
}, 250);
|
|
1760
|
+
stopQueuedPlayback();
|
|
1761
|
+
await interruptPromise;
|
|
1762
|
+
},
|
|
1763
|
+
get lastInterruptLatencyMs() {
|
|
1764
|
+
return state.lastInterruptLatencyMs;
|
|
1765
|
+
},
|
|
1766
|
+
get lastPlaybackStopLatencyMs() {
|
|
1767
|
+
return state.lastPlaybackStopLatencyMs;
|
|
1768
|
+
},
|
|
1769
|
+
pause: async () => {
|
|
1770
|
+
if (!audioContext) {
|
|
1771
|
+
setState({
|
|
1772
|
+
activeSourceCount: 0,
|
|
1773
|
+
isActive: false,
|
|
1774
|
+
isPlaying: false
|
|
1775
|
+
});
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
await audioContext.suspend();
|
|
1779
|
+
setState({
|
|
1780
|
+
activeSourceCount: sourceNodes.size,
|
|
1781
|
+
isActive: false,
|
|
1782
|
+
isPlaying: false
|
|
1783
|
+
});
|
|
1784
|
+
},
|
|
1785
|
+
get processedChunkCount() {
|
|
1786
|
+
return state.processedChunkCount;
|
|
1787
|
+
},
|
|
1788
|
+
get queuedChunkCount() {
|
|
1789
|
+
return state.queuedChunkCount;
|
|
1790
|
+
},
|
|
1791
|
+
start: async () => {
|
|
1792
|
+
try {
|
|
1793
|
+
clearError();
|
|
1794
|
+
const context = await ensureAudioContext();
|
|
1795
|
+
restoreOutputGain(context);
|
|
1796
|
+
if (context.state === "suspended") {
|
|
1797
|
+
await context.resume();
|
|
1798
|
+
}
|
|
1799
|
+
setState({
|
|
1800
|
+
activeSourceCount: sourceNodes.size,
|
|
1801
|
+
isActive: true,
|
|
1802
|
+
isPlaying: context.state === "running"
|
|
1803
|
+
});
|
|
1804
|
+
await queueSync();
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
setState({
|
|
1807
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1808
|
+
isActive: false,
|
|
1809
|
+
isPlaying: false
|
|
1810
|
+
});
|
|
1811
|
+
throw error;
|
|
1812
|
+
}
|
|
1813
|
+
},
|
|
1814
|
+
subscribe: (subscriber) => {
|
|
1815
|
+
subscribers.add(subscriber);
|
|
1816
|
+
return () => {
|
|
1817
|
+
subscribers.delete(subscriber);
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
return player;
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
// src/client/bargeInMonitor.ts
|
|
1825
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
1826
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
1827
|
+
var summarize = (events, thresholdMs) => {
|
|
1828
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
1829
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
1830
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
1831
|
+
const passed = stopped.length - failed;
|
|
1832
|
+
return {
|
|
1833
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
1834
|
+
events: [...events],
|
|
1835
|
+
failed,
|
|
1836
|
+
lastEvent: events.at(-1),
|
|
1837
|
+
passed,
|
|
1838
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
1839
|
+
thresholdMs,
|
|
1840
|
+
total: stopped.length
|
|
1841
|
+
};
|
|
1842
|
+
};
|
|
1843
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
1844
|
+
const listeners = new Set;
|
|
1845
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
1846
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1847
|
+
const events = [];
|
|
1848
|
+
const emit = () => {
|
|
1849
|
+
for (const listener of listeners) {
|
|
1850
|
+
listener();
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
const postEvent = (event) => {
|
|
1854
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
fetchImpl(options.path, {
|
|
1858
|
+
body: JSON.stringify(event),
|
|
1859
|
+
headers: {
|
|
1860
|
+
"Content-Type": "application/json"
|
|
1861
|
+
},
|
|
1862
|
+
method: "POST"
|
|
1863
|
+
}).catch(() => {});
|
|
1864
|
+
};
|
|
1865
|
+
const record = (status, input) => {
|
|
1866
|
+
const event = {
|
|
1867
|
+
at: Date.now(),
|
|
1868
|
+
id: createEventId(),
|
|
1869
|
+
latencyMs: input.latencyMs,
|
|
1870
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
1871
|
+
reason: input.reason,
|
|
1872
|
+
sessionId: input.sessionId,
|
|
1873
|
+
status,
|
|
1874
|
+
thresholdMs
|
|
1875
|
+
};
|
|
1876
|
+
events.push(event);
|
|
1877
|
+
postEvent(event);
|
|
1878
|
+
emit();
|
|
1879
|
+
return event;
|
|
1880
|
+
};
|
|
1881
|
+
return {
|
|
1882
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
1883
|
+
recordRequested: (input) => record("requested", input),
|
|
1884
|
+
recordSkipped: (input) => record("skipped", input),
|
|
1885
|
+
recordStopped: (input) => record("stopped", input),
|
|
1886
|
+
subscribe: (subscriber) => {
|
|
1887
|
+
listeners.add(subscriber);
|
|
1888
|
+
return () => {
|
|
1889
|
+
listeners.delete(subscriber);
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
// src/client/duplex.ts
|
|
1896
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
1897
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
1898
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
1899
|
+
let lastPartial = controller.partial;
|
|
1900
|
+
const interruptIfPlaying = (reason) => {
|
|
1901
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
1902
|
+
options.monitor?.recordSkipped({
|
|
1903
|
+
reason,
|
|
1904
|
+
sessionId: controller.sessionId
|
|
1905
|
+
});
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
options.monitor?.recordRequested({
|
|
1909
|
+
reason,
|
|
1910
|
+
sessionId: controller.sessionId
|
|
1911
|
+
});
|
|
1912
|
+
player.interrupt().then(() => {
|
|
1913
|
+
options.monitor?.recordStopped({
|
|
1914
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
1915
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
1916
|
+
reason,
|
|
1917
|
+
sessionId: controller.sessionId
|
|
1918
|
+
});
|
|
1919
|
+
});
|
|
1920
|
+
};
|
|
1921
|
+
const unsubscribe = controller.subscribe(() => {
|
|
1922
|
+
if (options.interruptOnPartial === false) {
|
|
1923
|
+
lastPartial = controller.partial;
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
if (!lastPartial && controller.partial) {
|
|
1927
|
+
interruptIfPlaying("partial-transcript");
|
|
1928
|
+
}
|
|
1929
|
+
lastPartial = controller.partial;
|
|
1930
|
+
});
|
|
1931
|
+
return {
|
|
1932
|
+
close: () => {
|
|
1933
|
+
unsubscribe();
|
|
1934
|
+
},
|
|
1935
|
+
handleLevel: (level) => {
|
|
1936
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
1937
|
+
interruptIfPlaying("input-level");
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
sendAudio: (audio) => {
|
|
1941
|
+
interruptIfPlaying("manual-audio");
|
|
1942
|
+
controller.sendAudio(audio);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1107
1947
|
// src/client/htmxBootstrap.ts
|
|
1108
1948
|
var VOICE_WAVE_POINTS = 48;
|
|
1109
1949
|
var VOICE_WAVE_WIDTH = 320;
|
|
@@ -1126,7 +1966,7 @@ var DEFAULT_GUIDED_PROMPTS = [
|
|
|
1126
1966
|
"Now describe what you are trying to do or test.",
|
|
1127
1967
|
"Finish with any detail that feels blocked, risky, or unclear."
|
|
1128
1968
|
];
|
|
1129
|
-
var clamp = (value, min,
|
|
1969
|
+
var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
|
|
1130
1970
|
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1131
1971
|
var readErrorField = (value, key) => {
|
|
1132
1972
|
const candidate = value[key];
|
|
@@ -1160,6 +2000,17 @@ var formatErrorMessage = (error) => {
|
|
|
1160
2000
|
}
|
|
1161
2001
|
return "Unexpected error";
|
|
1162
2002
|
};
|
|
2003
|
+
var formatReconnectState = (reconnect) => {
|
|
2004
|
+
const pieces = [reconnect.status];
|
|
2005
|
+
if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
|
|
2006
|
+
pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
|
|
2007
|
+
}
|
|
2008
|
+
if (reconnect.nextAttemptAt) {
|
|
2009
|
+
const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
|
|
2010
|
+
pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
|
|
2011
|
+
}
|
|
2012
|
+
return pieces.join(" · ");
|
|
2013
|
+
};
|
|
1163
2014
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1164
2015
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1165
2016
|
const next = levels.slice(-(count - 1));
|
|
@@ -1216,6 +2067,17 @@ var parsePromptList = (value) => {
|
|
|
1216
2067
|
} catch {}
|
|
1217
2068
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1218
2069
|
};
|
|
2070
|
+
var parseOptionalNumber = (value) => {
|
|
2071
|
+
if (!value) {
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const parsed = Number(value);
|
|
2075
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2076
|
+
};
|
|
2077
|
+
var resolveElement2 = (root, selector, ctor) => {
|
|
2078
|
+
const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
|
|
2079
|
+
return value instanceof ctor ? value : null;
|
|
2080
|
+
};
|
|
1219
2081
|
var requireElement = (root, selector, ctor, name) => {
|
|
1220
2082
|
const value = selector ? document.querySelector(selector) : null;
|
|
1221
2083
|
if (value instanceof ctor) {
|
|
@@ -1266,11 +2128,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1266
2128
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1267
2129
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1268
2130
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
2131
|
+
const reconnectReportPath = root.dataset.voiceReconnectReportPath;
|
|
2132
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
2133
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
2134
|
+
path: bargeInPath,
|
|
2135
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
2136
|
+
}) : null;
|
|
2137
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
2138
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1269
2139
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1270
2140
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1271
2141
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1272
2142
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1273
2143
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
2144
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1274
2145
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1275
2146
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1276
2147
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1279,35 +2150,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1279
2150
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1280
2151
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1281
2152
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
2153
|
+
let activeMode = null;
|
|
2154
|
+
let hasStartedModes = {
|
|
2155
|
+
general: false,
|
|
2156
|
+
guided: false
|
|
2157
|
+
};
|
|
2158
|
+
let isCapturing = false;
|
|
2159
|
+
let micError = null;
|
|
2160
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
2161
|
+
let guidedBargeInBinding = null;
|
|
2162
|
+
let generalBargeInBinding = null;
|
|
1282
2163
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1283
2164
|
capture: {
|
|
2165
|
+
onAudio: (audio, sendAudio) => {
|
|
2166
|
+
if (guidedBargeInBinding) {
|
|
2167
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
sendAudio(audio);
|
|
2171
|
+
},
|
|
1284
2172
|
onLevel: (level) => {
|
|
2173
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1285
2174
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1286
2175
|
renderWave();
|
|
1287
2176
|
}
|
|
1288
2177
|
},
|
|
2178
|
+
connection: {
|
|
2179
|
+
reconnectReportPath
|
|
2180
|
+
},
|
|
1289
2181
|
preset: "guided-intake"
|
|
1290
2182
|
});
|
|
1291
2183
|
const generalVoice = createVoiceController(generalPath, {
|
|
1292
2184
|
capture: {
|
|
2185
|
+
onAudio: (audio, sendAudio) => {
|
|
2186
|
+
if (generalBargeInBinding) {
|
|
2187
|
+
generalBargeInBinding.sendAudio(audio);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
sendAudio(audio);
|
|
2191
|
+
},
|
|
1293
2192
|
onLevel: (level) => {
|
|
2193
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1294
2194
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1295
2195
|
renderWave();
|
|
1296
2196
|
}
|
|
1297
2197
|
},
|
|
2198
|
+
connection: {
|
|
2199
|
+
reconnectReportPath
|
|
2200
|
+
},
|
|
1298
2201
|
preset: "dictation"
|
|
1299
2202
|
});
|
|
1300
2203
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1301
2204
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2205
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
2206
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
2207
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
2208
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2209
|
+
monitor: bargeInMonitor ?? undefined
|
|
2210
|
+
});
|
|
2211
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
2212
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2213
|
+
monitor: bargeInMonitor ?? undefined
|
|
2214
|
+
});
|
|
1310
2215
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
2216
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1311
2217
|
const renderWave = () => {
|
|
1312
2218
|
const path = createVoiceWavePath(waveLevels);
|
|
1313
2219
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1322,6 +2228,9 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1322
2228
|
const status = voice.status;
|
|
1323
2229
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1324
2230
|
errorStatus.textContent = micError || voice.error || "None";
|
|
2231
|
+
if (reconnectStatus) {
|
|
2232
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2233
|
+
}
|
|
1325
2234
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1326
2235
|
promptStatus.textContent = resolvePromptMessage({
|
|
1327
2236
|
guidedPrompts,
|
|
@@ -1385,8 +2294,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1385
2294
|
render();
|
|
1386
2295
|
}
|
|
1387
2296
|
};
|
|
1388
|
-
guidedVoice.subscribe(
|
|
1389
|
-
|
|
2297
|
+
guidedVoice.subscribe(() => {
|
|
2298
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
2299
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
2300
|
+
}
|
|
2301
|
+
render();
|
|
2302
|
+
});
|
|
2303
|
+
generalVoice.subscribe(() => {
|
|
2304
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
2305
|
+
generalAudioPlayer.start().catch(() => {});
|
|
2306
|
+
}
|
|
2307
|
+
render();
|
|
2308
|
+
});
|
|
1390
2309
|
startGuidedButton.addEventListener("click", () => {
|
|
1391
2310
|
startMode("guided");
|
|
1392
2311
|
});
|
|
@@ -1399,6 +2318,10 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1399
2318
|
window.addEventListener("beforeunload", () => {
|
|
1400
2319
|
guidedVoice.stopRecording();
|
|
1401
2320
|
generalVoice.stopRecording();
|
|
2321
|
+
guidedBargeInBinding?.close();
|
|
2322
|
+
generalBargeInBinding?.close();
|
|
2323
|
+
guidedAudioPlayer.close();
|
|
2324
|
+
generalAudioPlayer.close();
|
|
1402
2325
|
stopGuidedBinding();
|
|
1403
2326
|
stopGeneralBinding();
|
|
1404
2327
|
guidedVoice.close();
|