@absolutejs/voice 0.0.22-beta.32 → 0.0.22-beta.321
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 +3354 -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 +3911 -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 +61 -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 +875 -14
- package/dist/client/index.d.ts +72 -0
- package/dist/client/index.js +5671 -19
- 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 +146 -13
- package/dist/index.js +28615 -5228
- 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 +254 -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 +594 -0
- package/dist/proofTrends.d.ts +133 -0
- package/dist/providerAdapters.d.ts +48 -0
- 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 +5472 -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 +5337 -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 +135 -2
- package/dist/telephonyOutcome.d.ts +273 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2172 -76
- package/dist/testing/ioProviderSimulator.d.ts +41 -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 +116 -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 +5241 -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 +837 -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,170 @@ 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 secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
264
|
+
var normalizeWebRTCStat = (stat) => {
|
|
265
|
+
const sample = {};
|
|
266
|
+
for (const [key, value] of Object.entries(stat)) {
|
|
267
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
268
|
+
sample[key] = value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return sample;
|
|
272
|
+
};
|
|
273
|
+
var buildMediaWebRTCStatsReport = (input = {}) => {
|
|
274
|
+
const stats = input.stats ?? [];
|
|
275
|
+
const issues = [];
|
|
276
|
+
const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
277
|
+
const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
278
|
+
const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
|
|
279
|
+
const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
|
|
280
|
+
const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
|
|
281
|
+
const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
|
|
282
|
+
const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
|
|
283
|
+
const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
|
|
284
|
+
const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
|
|
285
|
+
const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
|
|
286
|
+
const packetLossDenominator = inboundPackets + packetsLost;
|
|
287
|
+
const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
|
|
288
|
+
const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
|
|
289
|
+
const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
|
|
290
|
+
const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
|
|
291
|
+
const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
|
|
292
|
+
const jitterBufferDelayMs = max(inbound.map((stat) => {
|
|
293
|
+
const delay = numericStat(stat, "jitterBufferDelay");
|
|
294
|
+
const emitted = numericStat(stat, "jitterBufferEmittedCount");
|
|
295
|
+
return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
|
|
296
|
+
}).filter((value) => value !== undefined));
|
|
297
|
+
const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
|
|
298
|
+
if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
|
|
299
|
+
pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
|
|
300
|
+
}
|
|
301
|
+
if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
|
|
302
|
+
pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
|
|
303
|
+
}
|
|
304
|
+
if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
|
|
305
|
+
pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
|
|
306
|
+
}
|
|
307
|
+
if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
|
|
308
|
+
pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
|
|
309
|
+
}
|
|
310
|
+
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
311
|
+
pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
activeCandidatePairs,
|
|
315
|
+
audioLevelAverage: average(audioLevels),
|
|
316
|
+
bytesReceived,
|
|
317
|
+
bytesSent,
|
|
318
|
+
checkedAt: Date.now(),
|
|
319
|
+
endedAudioTracks,
|
|
320
|
+
inboundPackets,
|
|
321
|
+
issues,
|
|
322
|
+
jitterBufferDelayMs,
|
|
323
|
+
jitterMs,
|
|
324
|
+
liveAudioTracks,
|
|
325
|
+
outboundPackets,
|
|
326
|
+
packetLossRatio,
|
|
327
|
+
packetsLost,
|
|
328
|
+
roundTripTimeMs,
|
|
329
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
330
|
+
totalStats: stats.length
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
var collectMediaWebRTCStats = async (input) => {
|
|
334
|
+
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
335
|
+
return [...report.values()].map(normalizeWebRTCStat);
|
|
336
|
+
};
|
|
337
|
+
var collectMediaWebRTCStatsReport = async (input) => {
|
|
338
|
+
const stats = await collectMediaWebRTCStats(input);
|
|
339
|
+
return buildMediaWebRTCStatsReport({
|
|
340
|
+
...input,
|
|
341
|
+
stats
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/client/browserMedia.ts
|
|
346
|
+
var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
|
|
347
|
+
var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
|
|
348
|
+
var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
|
|
349
|
+
var postBrowserMediaReport = async (payload, options) => {
|
|
350
|
+
const requestFetch = options.fetch ?? globalThis.fetch;
|
|
351
|
+
if (!requestFetch) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
|
|
355
|
+
body: JSON.stringify(payload),
|
|
356
|
+
headers: {
|
|
357
|
+
"Content-Type": "application/json"
|
|
358
|
+
},
|
|
359
|
+
keepalive: true,
|
|
360
|
+
method: "POST"
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
var createVoiceBrowserMediaReporter = (options) => {
|
|
364
|
+
let interval = null;
|
|
365
|
+
const reportOnce = async () => {
|
|
366
|
+
const peerConnection = await resolvePeerConnection(options);
|
|
367
|
+
if (!peerConnection) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const report = await collectMediaWebRTCStatsReport({
|
|
371
|
+
...options,
|
|
372
|
+
peerConnection
|
|
373
|
+
});
|
|
374
|
+
const payload = {
|
|
375
|
+
at: Date.now(),
|
|
376
|
+
report,
|
|
377
|
+
scenarioId: options.getScenarioId?.() ?? null,
|
|
378
|
+
sessionId: options.getSessionId?.() ?? null
|
|
379
|
+
};
|
|
380
|
+
options.onReport?.(payload);
|
|
381
|
+
await postBrowserMediaReport(payload, options);
|
|
382
|
+
return payload;
|
|
383
|
+
};
|
|
384
|
+
const run = () => {
|
|
385
|
+
reportOnce().catch((error) => {
|
|
386
|
+
options.onError?.(error);
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
const stop = () => {
|
|
390
|
+
if (interval) {
|
|
391
|
+
clearInterval(interval);
|
|
392
|
+
interval = null;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
return {
|
|
396
|
+
close: stop,
|
|
397
|
+
reportOnce,
|
|
398
|
+
start: () => {
|
|
399
|
+
if (interval) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
run();
|
|
403
|
+
interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
|
|
404
|
+
},
|
|
405
|
+
stop
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
|
|
229
409
|
// src/client/connection.ts
|
|
230
410
|
var WS_OPEN = 1;
|
|
231
411
|
var WS_CLOSED = 3;
|
|
@@ -269,10 +449,12 @@ var isVoiceServerMessage = (value) => {
|
|
|
269
449
|
case "assistant":
|
|
270
450
|
case "call_lifecycle":
|
|
271
451
|
case "complete":
|
|
452
|
+
case "connection":
|
|
272
453
|
case "error":
|
|
273
454
|
case "final":
|
|
274
455
|
case "partial":
|
|
275
456
|
case "pong":
|
|
457
|
+
case "replay":
|
|
276
458
|
case "session":
|
|
277
459
|
case "turn":
|
|
278
460
|
return true;
|
|
@@ -309,6 +491,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
309
491
|
sessionId: options.sessionId ?? createSessionId(),
|
|
310
492
|
ws: null
|
|
311
493
|
};
|
|
494
|
+
const emitConnection = (reconnect) => {
|
|
495
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
496
|
+
};
|
|
312
497
|
const clearTimers = () => {
|
|
313
498
|
if (state.pingInterval) {
|
|
314
499
|
clearInterval(state.pingInterval);
|
|
@@ -331,9 +516,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
331
516
|
}
|
|
332
517
|
};
|
|
333
518
|
const scheduleReconnect = () => {
|
|
519
|
+
const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
|
|
334
520
|
state.reconnectAttempts += 1;
|
|
521
|
+
emitConnection({
|
|
522
|
+
reconnect: {
|
|
523
|
+
attempts: state.reconnectAttempts,
|
|
524
|
+
lastDisconnectAt: Date.now(),
|
|
525
|
+
maxAttempts: maxReconnectAttempts,
|
|
526
|
+
nextAttemptAt,
|
|
527
|
+
status: "reconnecting"
|
|
528
|
+
},
|
|
529
|
+
type: "connection"
|
|
530
|
+
});
|
|
335
531
|
state.reconnectTimeout = setTimeout(() => {
|
|
336
532
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
533
|
+
emitConnection({
|
|
534
|
+
reconnect: {
|
|
535
|
+
attempts: state.reconnectAttempts,
|
|
536
|
+
maxAttempts: maxReconnectAttempts,
|
|
537
|
+
status: "exhausted"
|
|
538
|
+
},
|
|
539
|
+
type: "connection"
|
|
540
|
+
});
|
|
337
541
|
return;
|
|
338
542
|
}
|
|
339
543
|
connect();
|
|
@@ -343,9 +547,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
343
547
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
|
|
344
548
|
ws.binaryType = "arraybuffer";
|
|
345
549
|
ws.onopen = () => {
|
|
550
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
346
551
|
state.isConnected = true;
|
|
347
|
-
state.reconnectAttempts = 0;
|
|
348
552
|
flushPendingMessages();
|
|
553
|
+
if (wasReconnecting) {
|
|
554
|
+
emitConnection({
|
|
555
|
+
reconnect: {
|
|
556
|
+
attempts: state.reconnectAttempts,
|
|
557
|
+
lastResumedAt: Date.now(),
|
|
558
|
+
maxAttempts: maxReconnectAttempts,
|
|
559
|
+
status: "resumed"
|
|
560
|
+
},
|
|
561
|
+
type: "connection"
|
|
562
|
+
});
|
|
563
|
+
state.reconnectAttempts = 0;
|
|
564
|
+
}
|
|
349
565
|
listeners.forEach((listener) => listener({
|
|
350
566
|
scenarioId: state.scenarioId ?? undefined,
|
|
351
567
|
sessionId: state.sessionId,
|
|
@@ -375,6 +591,16 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
375
591
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
|
|
376
592
|
if (reconnectable) {
|
|
377
593
|
scheduleReconnect();
|
|
594
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
595
|
+
emitConnection({
|
|
596
|
+
reconnect: {
|
|
597
|
+
attempts: state.reconnectAttempts,
|
|
598
|
+
lastDisconnectAt: Date.now(),
|
|
599
|
+
maxAttempts: maxReconnectAttempts,
|
|
600
|
+
status: "exhausted"
|
|
601
|
+
},
|
|
602
|
+
type: "connection"
|
|
603
|
+
});
|
|
378
604
|
}
|
|
379
605
|
};
|
|
380
606
|
state.ws = ws;
|
|
@@ -445,6 +671,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
445
671
|
};
|
|
446
672
|
|
|
447
673
|
// src/client/store.ts
|
|
674
|
+
var createInitialReconnectState = () => ({
|
|
675
|
+
attempts: 0,
|
|
676
|
+
maxAttempts: 0,
|
|
677
|
+
status: "idle"
|
|
678
|
+
});
|
|
448
679
|
var createInitialState = () => ({
|
|
449
680
|
assistantAudio: [],
|
|
450
681
|
assistantTexts: [],
|
|
@@ -453,6 +684,7 @@ var createInitialState = () => ({
|
|
|
453
684
|
isConnected: false,
|
|
454
685
|
scenarioId: null,
|
|
455
686
|
partial: "",
|
|
687
|
+
reconnect: createInitialReconnectState(),
|
|
456
688
|
sessionId: null,
|
|
457
689
|
status: "idle",
|
|
458
690
|
turns: []
|
|
@@ -509,7 +741,19 @@ var createVoiceStreamStore = () => {
|
|
|
509
741
|
case "connected":
|
|
510
742
|
state = {
|
|
511
743
|
...state,
|
|
512
|
-
isConnected: true
|
|
744
|
+
isConnected: true,
|
|
745
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
746
|
+
...state.reconnect,
|
|
747
|
+
lastResumedAt: Date.now(),
|
|
748
|
+
nextAttemptAt: undefined,
|
|
749
|
+
status: "resumed"
|
|
750
|
+
} : state.reconnect
|
|
751
|
+
};
|
|
752
|
+
break;
|
|
753
|
+
case "connection":
|
|
754
|
+
state = {
|
|
755
|
+
...state,
|
|
756
|
+
reconnect: action.reconnect
|
|
513
757
|
};
|
|
514
758
|
break;
|
|
515
759
|
case "disconnected":
|
|
@@ -537,6 +781,26 @@ var createVoiceStreamStore = () => {
|
|
|
537
781
|
partial: action.transcript.text
|
|
538
782
|
};
|
|
539
783
|
break;
|
|
784
|
+
case "replay":
|
|
785
|
+
state = {
|
|
786
|
+
...state,
|
|
787
|
+
assistantTexts: [...action.assistantTexts],
|
|
788
|
+
call: action.call ?? null,
|
|
789
|
+
error: null,
|
|
790
|
+
isConnected: action.status === "active",
|
|
791
|
+
partial: action.partial,
|
|
792
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
793
|
+
...state.reconnect,
|
|
794
|
+
lastResumedAt: Date.now(),
|
|
795
|
+
nextAttemptAt: undefined,
|
|
796
|
+
status: "resumed"
|
|
797
|
+
} : state.reconnect,
|
|
798
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
799
|
+
sessionId: action.sessionId,
|
|
800
|
+
status: action.status,
|
|
801
|
+
turns: [...action.turns]
|
|
802
|
+
};
|
|
803
|
+
break;
|
|
540
804
|
case "session":
|
|
541
805
|
state = {
|
|
542
806
|
...state,
|
|
@@ -574,20 +838,50 @@ var createVoiceStreamStore = () => {
|
|
|
574
838
|
var createVoiceStream = (path, options = {}) => {
|
|
575
839
|
const connection = createVoiceConnection(path, options);
|
|
576
840
|
const store = createVoiceStreamStore();
|
|
841
|
+
const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
|
|
842
|
+
...options.browserMedia,
|
|
843
|
+
getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
|
|
844
|
+
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
845
|
+
}) : null;
|
|
577
846
|
const subscribers = new Set;
|
|
578
847
|
const start = (input) => Promise.resolve().then(() => {
|
|
579
848
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
580
849
|
return;
|
|
581
850
|
}
|
|
582
851
|
connection.start(input);
|
|
852
|
+
browserMediaReporter?.start();
|
|
583
853
|
});
|
|
584
854
|
const notify = () => {
|
|
585
855
|
subscribers.forEach((subscriber) => subscriber());
|
|
586
856
|
};
|
|
857
|
+
const reportReconnect = () => {
|
|
858
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const snapshot = store.getSnapshot();
|
|
862
|
+
const body = JSON.stringify({
|
|
863
|
+
at: Date.now(),
|
|
864
|
+
reconnect: snapshot.reconnect,
|
|
865
|
+
scenarioId: snapshot.scenarioId,
|
|
866
|
+
sessionId: connection.getSessionId(),
|
|
867
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
868
|
+
});
|
|
869
|
+
fetch(options.reconnectReportPath, {
|
|
870
|
+
body,
|
|
871
|
+
headers: {
|
|
872
|
+
"Content-Type": "application/json"
|
|
873
|
+
},
|
|
874
|
+
keepalive: true,
|
|
875
|
+
method: "POST"
|
|
876
|
+
}).catch(() => {});
|
|
877
|
+
};
|
|
587
878
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
588
879
|
const action = serverMessageToAction(message);
|
|
589
880
|
if (action) {
|
|
590
881
|
store.dispatch(action);
|
|
882
|
+
if (message.type === "connection") {
|
|
883
|
+
reportReconnect();
|
|
884
|
+
}
|
|
591
885
|
notify();
|
|
592
886
|
}
|
|
593
887
|
});
|
|
@@ -597,6 +891,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
597
891
|
},
|
|
598
892
|
close() {
|
|
599
893
|
unsubscribeConnection();
|
|
894
|
+
browserMediaReporter?.close();
|
|
600
895
|
connection.close();
|
|
601
896
|
store.dispatch({ type: "disconnected" });
|
|
602
897
|
notify();
|
|
@@ -623,6 +918,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
623
918
|
get partial() {
|
|
624
919
|
return store.getSnapshot().partial;
|
|
625
920
|
},
|
|
921
|
+
get reconnect() {
|
|
922
|
+
return store.getSnapshot().reconnect;
|
|
923
|
+
},
|
|
626
924
|
get sessionId() {
|
|
627
925
|
return connection.getSessionId();
|
|
628
926
|
},
|
|
@@ -941,6 +1239,7 @@ var createInitialState2 = (stream) => ({
|
|
|
941
1239
|
isConnected: stream.isConnected,
|
|
942
1240
|
isRecording: false,
|
|
943
1241
|
partial: stream.partial,
|
|
1242
|
+
reconnect: stream.reconnect,
|
|
944
1243
|
recordingError: null,
|
|
945
1244
|
sessionId: stream.sessionId,
|
|
946
1245
|
scenarioId: stream.scenarioId,
|
|
@@ -970,6 +1269,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
970
1269
|
error: stream.error,
|
|
971
1270
|
isConnected: stream.isConnected,
|
|
972
1271
|
partial: stream.partial,
|
|
1272
|
+
reconnect: stream.reconnect,
|
|
973
1273
|
sessionId: stream.sessionId,
|
|
974
1274
|
scenarioId: stream.scenarioId,
|
|
975
1275
|
status: stream.status,
|
|
@@ -994,7 +1294,13 @@ var createVoiceController = (path, options = {}) => {
|
|
|
994
1294
|
capture = createMicrophoneCapture({
|
|
995
1295
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
996
1296
|
onLevel: options.capture?.onLevel,
|
|
997
|
-
onAudio: (audio) =>
|
|
1297
|
+
onAudio: (audio) => {
|
|
1298
|
+
if (options.capture?.onAudio) {
|
|
1299
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
stream.sendAudio(audio);
|
|
1303
|
+
},
|
|
998
1304
|
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
|
|
999
1305
|
});
|
|
1000
1306
|
return capture;
|
|
@@ -1064,6 +1370,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1064
1370
|
get recordingError() {
|
|
1065
1371
|
return state.recordingError;
|
|
1066
1372
|
},
|
|
1373
|
+
get reconnect() {
|
|
1374
|
+
return state.reconnect;
|
|
1375
|
+
},
|
|
1067
1376
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1068
1377
|
get sessionId() {
|
|
1069
1378
|
return state.sessionId;
|
|
@@ -1104,6 +1413,475 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1104
1413
|
};
|
|
1105
1414
|
};
|
|
1106
1415
|
|
|
1416
|
+
// src/client/audioPlayer.ts
|
|
1417
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1418
|
+
var createInitialState3 = () => ({
|
|
1419
|
+
activeSourceCount: 0,
|
|
1420
|
+
error: null,
|
|
1421
|
+
isActive: false,
|
|
1422
|
+
isPlaying: false,
|
|
1423
|
+
lastInterruptLatencyMs: undefined,
|
|
1424
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
1425
|
+
processedChunkCount: 0,
|
|
1426
|
+
queuedChunkCount: 0
|
|
1427
|
+
});
|
|
1428
|
+
var getAudioContextCtor = () => {
|
|
1429
|
+
if (typeof window === "undefined") {
|
|
1430
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
1431
|
+
}
|
|
1432
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
1433
|
+
};
|
|
1434
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
1435
|
+
const format = chunk.format;
|
|
1436
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
1437
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
1438
|
+
}
|
|
1439
|
+
const bytes = chunk.chunk;
|
|
1440
|
+
const channels = Math.max(1, format.channels);
|
|
1441
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
1442
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
1443
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
1444
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1445
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1446
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
1447
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
1448
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
1449
|
+
const sampleOffset = sampleIndex * 2;
|
|
1450
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
1451
|
+
channelData[frameIndex] = 0;
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return audioBuffer;
|
|
1458
|
+
};
|
|
1459
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
1460
|
+
const subscribers = new Set;
|
|
1461
|
+
const sourceNodes = new Set;
|
|
1462
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
1463
|
+
let state = createInitialState3();
|
|
1464
|
+
let audioContext = null;
|
|
1465
|
+
let outputNode = null;
|
|
1466
|
+
let queueEndTime = 0;
|
|
1467
|
+
let syncPromise = Promise.resolve();
|
|
1468
|
+
let interruptStartedAt = null;
|
|
1469
|
+
let interruptPromise = null;
|
|
1470
|
+
let resolveInterruptPromise = null;
|
|
1471
|
+
let interruptFallbackTimer = null;
|
|
1472
|
+
const notify = () => {
|
|
1473
|
+
for (const subscriber of subscribers) {
|
|
1474
|
+
subscriber();
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
const setState = (next) => {
|
|
1478
|
+
state = {
|
|
1479
|
+
...state,
|
|
1480
|
+
...next
|
|
1481
|
+
};
|
|
1482
|
+
notify();
|
|
1483
|
+
};
|
|
1484
|
+
const clearError = () => {
|
|
1485
|
+
if (state.error !== null) {
|
|
1486
|
+
setState({ error: null });
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
const clearInterruptTimer = () => {
|
|
1490
|
+
if (interruptFallbackTimer !== null) {
|
|
1491
|
+
clearTimeout(interruptFallbackTimer);
|
|
1492
|
+
interruptFallbackTimer = null;
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
const resolveInterrupt = (latencyMs) => {
|
|
1496
|
+
clearInterruptTimer();
|
|
1497
|
+
interruptStartedAt = null;
|
|
1498
|
+
setState({
|
|
1499
|
+
activeSourceCount: sourceNodes.size,
|
|
1500
|
+
isPlaying: false,
|
|
1501
|
+
lastInterruptLatencyMs: latencyMs,
|
|
1502
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
1503
|
+
});
|
|
1504
|
+
resolveInterruptPromise?.();
|
|
1505
|
+
resolveInterruptPromise = null;
|
|
1506
|
+
interruptPromise = null;
|
|
1507
|
+
};
|
|
1508
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
1509
|
+
if (!context) {
|
|
1510
|
+
return 0;
|
|
1511
|
+
}
|
|
1512
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
1513
|
+
};
|
|
1514
|
+
const restoreOutputGain = (context) => {
|
|
1515
|
+
if (!outputNode) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
const gainValue = 1;
|
|
1519
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1520
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
outputNode.gain.value = gainValue;
|
|
1524
|
+
};
|
|
1525
|
+
const muteOutputGain = (context) => {
|
|
1526
|
+
if (!outputNode) {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const gainValue = 0;
|
|
1530
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1531
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
outputNode.gain.value = gainValue;
|
|
1535
|
+
};
|
|
1536
|
+
const maybeResolveInterrupt = () => {
|
|
1537
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
1541
|
+
};
|
|
1542
|
+
const ensureAudioContext = async () => {
|
|
1543
|
+
if (audioContext) {
|
|
1544
|
+
return audioContext;
|
|
1545
|
+
}
|
|
1546
|
+
if (options.createAudioContext) {
|
|
1547
|
+
audioContext = options.createAudioContext();
|
|
1548
|
+
} else {
|
|
1549
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
1550
|
+
if (!AudioContextCtor) {
|
|
1551
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
1552
|
+
}
|
|
1553
|
+
audioContext = new AudioContextCtor;
|
|
1554
|
+
}
|
|
1555
|
+
if (audioContext.createGain) {
|
|
1556
|
+
outputNode = audioContext.createGain();
|
|
1557
|
+
outputNode.connect?.(audioContext.destination);
|
|
1558
|
+
}
|
|
1559
|
+
queueEndTime = audioContext.currentTime;
|
|
1560
|
+
return audioContext;
|
|
1561
|
+
};
|
|
1562
|
+
const scheduleChunk = async (chunk) => {
|
|
1563
|
+
const context = await ensureAudioContext();
|
|
1564
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
1565
|
+
const node = context.createBufferSource();
|
|
1566
|
+
node.buffer = buffer;
|
|
1567
|
+
node.connect(outputNode ?? context.destination);
|
|
1568
|
+
node.onended = () => {
|
|
1569
|
+
sourceNodes.delete(node);
|
|
1570
|
+
node.disconnect?.();
|
|
1571
|
+
setState({
|
|
1572
|
+
activeSourceCount: sourceNodes.size,
|
|
1573
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
1574
|
+
});
|
|
1575
|
+
maybeResolveInterrupt();
|
|
1576
|
+
};
|
|
1577
|
+
const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
|
|
1578
|
+
queueEndTime = startAt + buffer.duration;
|
|
1579
|
+
sourceNodes.add(node);
|
|
1580
|
+
setState({
|
|
1581
|
+
activeSourceCount: sourceNodes.size,
|
|
1582
|
+
isPlaying: true
|
|
1583
|
+
});
|
|
1584
|
+
node.start(startAt);
|
|
1585
|
+
};
|
|
1586
|
+
const stopQueuedPlayback = (options2) => {
|
|
1587
|
+
for (const node of [...sourceNodes]) {
|
|
1588
|
+
node.stop?.();
|
|
1589
|
+
}
|
|
1590
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
1591
|
+
if (options2?.forceClear) {
|
|
1592
|
+
for (const node of sourceNodes) {
|
|
1593
|
+
node.disconnect?.();
|
|
1594
|
+
}
|
|
1595
|
+
sourceNodes.clear();
|
|
1596
|
+
maybeResolveInterrupt();
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
const sync = async () => {
|
|
1600
|
+
if (!state.isActive) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
1604
|
+
if (nextChunks.length === 0) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
try {
|
|
1608
|
+
clearError();
|
|
1609
|
+
for (const chunk of nextChunks) {
|
|
1610
|
+
await scheduleChunk(chunk);
|
|
1611
|
+
}
|
|
1612
|
+
setState({
|
|
1613
|
+
processedChunkCount: source.assistantAudio.length,
|
|
1614
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
1615
|
+
});
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
setState({
|
|
1618
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
const queueSync = () => {
|
|
1623
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
1624
|
+
return syncPromise;
|
|
1625
|
+
};
|
|
1626
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
1627
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
1628
|
+
player.start();
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (state.isActive) {
|
|
1632
|
+
queueSync();
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
const player = {
|
|
1636
|
+
close: async () => {
|
|
1637
|
+
unsubscribeSource();
|
|
1638
|
+
stopQueuedPlayback({ forceClear: true });
|
|
1639
|
+
clearInterruptTimer();
|
|
1640
|
+
resolveInterruptPromise?.();
|
|
1641
|
+
resolveInterruptPromise = null;
|
|
1642
|
+
interruptPromise = null;
|
|
1643
|
+
interruptStartedAt = null;
|
|
1644
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
1645
|
+
await audioContext.close();
|
|
1646
|
+
}
|
|
1647
|
+
audioContext = null;
|
|
1648
|
+
outputNode?.disconnect?.();
|
|
1649
|
+
outputNode = null;
|
|
1650
|
+
queueEndTime = 0;
|
|
1651
|
+
setState({
|
|
1652
|
+
activeSourceCount: 0,
|
|
1653
|
+
isActive: false,
|
|
1654
|
+
isPlaying: false
|
|
1655
|
+
});
|
|
1656
|
+
},
|
|
1657
|
+
get activeSourceCount() {
|
|
1658
|
+
return state.activeSourceCount;
|
|
1659
|
+
},
|
|
1660
|
+
get error() {
|
|
1661
|
+
return state.error;
|
|
1662
|
+
},
|
|
1663
|
+
getSnapshot: () => state,
|
|
1664
|
+
get isActive() {
|
|
1665
|
+
return state.isActive;
|
|
1666
|
+
},
|
|
1667
|
+
get isPlaying() {
|
|
1668
|
+
return state.isPlaying;
|
|
1669
|
+
},
|
|
1670
|
+
interrupt: async () => {
|
|
1671
|
+
const startedAt = Date.now();
|
|
1672
|
+
const context = await ensureAudioContext();
|
|
1673
|
+
interruptStartedAt = startedAt;
|
|
1674
|
+
muteOutputGain(context);
|
|
1675
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
1676
|
+
setState({
|
|
1677
|
+
isActive: false,
|
|
1678
|
+
isPlaying: sourceNodes.size > 0,
|
|
1679
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
1680
|
+
});
|
|
1681
|
+
if (sourceNodes.size === 0) {
|
|
1682
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (!interruptPromise) {
|
|
1686
|
+
interruptPromise = new Promise((resolve) => {
|
|
1687
|
+
resolveInterruptPromise = resolve;
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
clearInterruptTimer();
|
|
1691
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
1692
|
+
for (const node of sourceNodes) {
|
|
1693
|
+
node.disconnect?.();
|
|
1694
|
+
}
|
|
1695
|
+
sourceNodes.clear();
|
|
1696
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
1697
|
+
}, 250);
|
|
1698
|
+
stopQueuedPlayback();
|
|
1699
|
+
await interruptPromise;
|
|
1700
|
+
},
|
|
1701
|
+
get lastInterruptLatencyMs() {
|
|
1702
|
+
return state.lastInterruptLatencyMs;
|
|
1703
|
+
},
|
|
1704
|
+
get lastPlaybackStopLatencyMs() {
|
|
1705
|
+
return state.lastPlaybackStopLatencyMs;
|
|
1706
|
+
},
|
|
1707
|
+
pause: async () => {
|
|
1708
|
+
if (!audioContext) {
|
|
1709
|
+
setState({
|
|
1710
|
+
activeSourceCount: 0,
|
|
1711
|
+
isActive: false,
|
|
1712
|
+
isPlaying: false
|
|
1713
|
+
});
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
await audioContext.suspend();
|
|
1717
|
+
setState({
|
|
1718
|
+
activeSourceCount: sourceNodes.size,
|
|
1719
|
+
isActive: false,
|
|
1720
|
+
isPlaying: false
|
|
1721
|
+
});
|
|
1722
|
+
},
|
|
1723
|
+
get processedChunkCount() {
|
|
1724
|
+
return state.processedChunkCount;
|
|
1725
|
+
},
|
|
1726
|
+
get queuedChunkCount() {
|
|
1727
|
+
return state.queuedChunkCount;
|
|
1728
|
+
},
|
|
1729
|
+
start: async () => {
|
|
1730
|
+
try {
|
|
1731
|
+
clearError();
|
|
1732
|
+
const context = await ensureAudioContext();
|
|
1733
|
+
restoreOutputGain(context);
|
|
1734
|
+
if (context.state === "suspended") {
|
|
1735
|
+
await context.resume();
|
|
1736
|
+
}
|
|
1737
|
+
setState({
|
|
1738
|
+
activeSourceCount: sourceNodes.size,
|
|
1739
|
+
isActive: true,
|
|
1740
|
+
isPlaying: context.state === "running"
|
|
1741
|
+
});
|
|
1742
|
+
await queueSync();
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
setState({
|
|
1745
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1746
|
+
isActive: false,
|
|
1747
|
+
isPlaying: false
|
|
1748
|
+
});
|
|
1749
|
+
throw error;
|
|
1750
|
+
}
|
|
1751
|
+
},
|
|
1752
|
+
subscribe: (subscriber) => {
|
|
1753
|
+
subscribers.add(subscriber);
|
|
1754
|
+
return () => {
|
|
1755
|
+
subscribers.delete(subscriber);
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
return player;
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
// src/client/bargeInMonitor.ts
|
|
1763
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
1764
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
1765
|
+
var summarize = (events, thresholdMs) => {
|
|
1766
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
1767
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
1768
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
1769
|
+
const passed = stopped.length - failed;
|
|
1770
|
+
return {
|
|
1771
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
1772
|
+
events: [...events],
|
|
1773
|
+
failed,
|
|
1774
|
+
lastEvent: events.at(-1),
|
|
1775
|
+
passed,
|
|
1776
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
1777
|
+
thresholdMs,
|
|
1778
|
+
total: stopped.length
|
|
1779
|
+
};
|
|
1780
|
+
};
|
|
1781
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
1782
|
+
const listeners = new Set;
|
|
1783
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
1784
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1785
|
+
const events = [];
|
|
1786
|
+
const emit = () => {
|
|
1787
|
+
for (const listener of listeners) {
|
|
1788
|
+
listener();
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
const postEvent = (event) => {
|
|
1792
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
fetchImpl(options.path, {
|
|
1796
|
+
body: JSON.stringify(event),
|
|
1797
|
+
headers: {
|
|
1798
|
+
"Content-Type": "application/json"
|
|
1799
|
+
},
|
|
1800
|
+
method: "POST"
|
|
1801
|
+
}).catch(() => {});
|
|
1802
|
+
};
|
|
1803
|
+
const record = (status, input) => {
|
|
1804
|
+
const event = {
|
|
1805
|
+
at: Date.now(),
|
|
1806
|
+
id: createEventId(),
|
|
1807
|
+
latencyMs: input.latencyMs,
|
|
1808
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
1809
|
+
reason: input.reason,
|
|
1810
|
+
sessionId: input.sessionId,
|
|
1811
|
+
status,
|
|
1812
|
+
thresholdMs
|
|
1813
|
+
};
|
|
1814
|
+
events.push(event);
|
|
1815
|
+
postEvent(event);
|
|
1816
|
+
emit();
|
|
1817
|
+
return event;
|
|
1818
|
+
};
|
|
1819
|
+
return {
|
|
1820
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
1821
|
+
recordRequested: (input) => record("requested", input),
|
|
1822
|
+
recordSkipped: (input) => record("skipped", input),
|
|
1823
|
+
recordStopped: (input) => record("stopped", input),
|
|
1824
|
+
subscribe: (subscriber) => {
|
|
1825
|
+
listeners.add(subscriber);
|
|
1826
|
+
return () => {
|
|
1827
|
+
listeners.delete(subscriber);
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
};
|
|
1832
|
+
|
|
1833
|
+
// src/client/duplex.ts
|
|
1834
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
1835
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
1836
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
1837
|
+
let lastPartial = controller.partial;
|
|
1838
|
+
const interruptIfPlaying = (reason) => {
|
|
1839
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
1840
|
+
options.monitor?.recordSkipped({
|
|
1841
|
+
reason,
|
|
1842
|
+
sessionId: controller.sessionId
|
|
1843
|
+
});
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
options.monitor?.recordRequested({
|
|
1847
|
+
reason,
|
|
1848
|
+
sessionId: controller.sessionId
|
|
1849
|
+
});
|
|
1850
|
+
player.interrupt().then(() => {
|
|
1851
|
+
options.monitor?.recordStopped({
|
|
1852
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
1853
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
1854
|
+
reason,
|
|
1855
|
+
sessionId: controller.sessionId
|
|
1856
|
+
});
|
|
1857
|
+
});
|
|
1858
|
+
};
|
|
1859
|
+
const unsubscribe = controller.subscribe(() => {
|
|
1860
|
+
if (options.interruptOnPartial === false) {
|
|
1861
|
+
lastPartial = controller.partial;
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (!lastPartial && controller.partial) {
|
|
1865
|
+
interruptIfPlaying("partial-transcript");
|
|
1866
|
+
}
|
|
1867
|
+
lastPartial = controller.partial;
|
|
1868
|
+
});
|
|
1869
|
+
return {
|
|
1870
|
+
close: () => {
|
|
1871
|
+
unsubscribe();
|
|
1872
|
+
},
|
|
1873
|
+
handleLevel: (level) => {
|
|
1874
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
1875
|
+
interruptIfPlaying("input-level");
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
sendAudio: (audio) => {
|
|
1879
|
+
interruptIfPlaying("manual-audio");
|
|
1880
|
+
controller.sendAudio(audio);
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1107
1885
|
// src/client/htmxBootstrap.ts
|
|
1108
1886
|
var VOICE_WAVE_POINTS = 48;
|
|
1109
1887
|
var VOICE_WAVE_WIDTH = 320;
|
|
@@ -1126,7 +1904,7 @@ var DEFAULT_GUIDED_PROMPTS = [
|
|
|
1126
1904
|
"Now describe what you are trying to do or test.",
|
|
1127
1905
|
"Finish with any detail that feels blocked, risky, or unclear."
|
|
1128
1906
|
];
|
|
1129
|
-
var clamp = (value, min,
|
|
1907
|
+
var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
|
|
1130
1908
|
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1131
1909
|
var readErrorField = (value, key) => {
|
|
1132
1910
|
const candidate = value[key];
|
|
@@ -1160,6 +1938,17 @@ var formatErrorMessage = (error) => {
|
|
|
1160
1938
|
}
|
|
1161
1939
|
return "Unexpected error";
|
|
1162
1940
|
};
|
|
1941
|
+
var formatReconnectState = (reconnect) => {
|
|
1942
|
+
const pieces = [reconnect.status];
|
|
1943
|
+
if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
|
|
1944
|
+
pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
|
|
1945
|
+
}
|
|
1946
|
+
if (reconnect.nextAttemptAt) {
|
|
1947
|
+
const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
|
|
1948
|
+
pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
|
|
1949
|
+
}
|
|
1950
|
+
return pieces.join(" · ");
|
|
1951
|
+
};
|
|
1163
1952
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1164
1953
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1165
1954
|
const next = levels.slice(-(count - 1));
|
|
@@ -1216,6 +2005,17 @@ var parsePromptList = (value) => {
|
|
|
1216
2005
|
} catch {}
|
|
1217
2006
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1218
2007
|
};
|
|
2008
|
+
var parseOptionalNumber = (value) => {
|
|
2009
|
+
if (!value) {
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
const parsed = Number(value);
|
|
2013
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2014
|
+
};
|
|
2015
|
+
var resolveElement2 = (root, selector, ctor) => {
|
|
2016
|
+
const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
|
|
2017
|
+
return value instanceof ctor ? value : null;
|
|
2018
|
+
};
|
|
1219
2019
|
var requireElement = (root, selector, ctor, name) => {
|
|
1220
2020
|
const value = selector ? document.querySelector(selector) : null;
|
|
1221
2021
|
if (value instanceof ctor) {
|
|
@@ -1266,11 +2066,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1266
2066
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1267
2067
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1268
2068
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
2069
|
+
const reconnectReportPath = root.dataset.voiceReconnectReportPath;
|
|
2070
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
2071
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
2072
|
+
path: bargeInPath,
|
|
2073
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
2074
|
+
}) : null;
|
|
2075
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
2076
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1269
2077
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1270
2078
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1271
2079
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1272
2080
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1273
2081
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
2082
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1274
2083
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1275
2084
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1276
2085
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1279,35 +2088,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1279
2088
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1280
2089
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1281
2090
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
2091
|
+
let activeMode = null;
|
|
2092
|
+
let hasStartedModes = {
|
|
2093
|
+
general: false,
|
|
2094
|
+
guided: false
|
|
2095
|
+
};
|
|
2096
|
+
let isCapturing = false;
|
|
2097
|
+
let micError = null;
|
|
2098
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
2099
|
+
let guidedBargeInBinding = null;
|
|
2100
|
+
let generalBargeInBinding = null;
|
|
1282
2101
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1283
2102
|
capture: {
|
|
2103
|
+
onAudio: (audio, sendAudio) => {
|
|
2104
|
+
if (guidedBargeInBinding) {
|
|
2105
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
sendAudio(audio);
|
|
2109
|
+
},
|
|
1284
2110
|
onLevel: (level) => {
|
|
2111
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1285
2112
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1286
2113
|
renderWave();
|
|
1287
2114
|
}
|
|
1288
2115
|
},
|
|
2116
|
+
connection: {
|
|
2117
|
+
reconnectReportPath
|
|
2118
|
+
},
|
|
1289
2119
|
preset: "guided-intake"
|
|
1290
2120
|
});
|
|
1291
2121
|
const generalVoice = createVoiceController(generalPath, {
|
|
1292
2122
|
capture: {
|
|
2123
|
+
onAudio: (audio, sendAudio) => {
|
|
2124
|
+
if (generalBargeInBinding) {
|
|
2125
|
+
generalBargeInBinding.sendAudio(audio);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
sendAudio(audio);
|
|
2129
|
+
},
|
|
1293
2130
|
onLevel: (level) => {
|
|
2131
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1294
2132
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1295
2133
|
renderWave();
|
|
1296
2134
|
}
|
|
1297
2135
|
},
|
|
2136
|
+
connection: {
|
|
2137
|
+
reconnectReportPath
|
|
2138
|
+
},
|
|
1298
2139
|
preset: "dictation"
|
|
1299
2140
|
});
|
|
1300
2141
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1301
2142
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2143
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
2144
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
2145
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
2146
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2147
|
+
monitor: bargeInMonitor ?? undefined
|
|
2148
|
+
});
|
|
2149
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
2150
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
2151
|
+
monitor: bargeInMonitor ?? undefined
|
|
2152
|
+
});
|
|
1310
2153
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
2154
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1311
2155
|
const renderWave = () => {
|
|
1312
2156
|
const path = createVoiceWavePath(waveLevels);
|
|
1313
2157
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1322,6 +2166,9 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1322
2166
|
const status = voice.status;
|
|
1323
2167
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1324
2168
|
errorStatus.textContent = micError || voice.error || "None";
|
|
2169
|
+
if (reconnectStatus) {
|
|
2170
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2171
|
+
}
|
|
1325
2172
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1326
2173
|
promptStatus.textContent = resolvePromptMessage({
|
|
1327
2174
|
guidedPrompts,
|
|
@@ -1385,8 +2232,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1385
2232
|
render();
|
|
1386
2233
|
}
|
|
1387
2234
|
};
|
|
1388
|
-
guidedVoice.subscribe(
|
|
1389
|
-
|
|
2235
|
+
guidedVoice.subscribe(() => {
|
|
2236
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
2237
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
2238
|
+
}
|
|
2239
|
+
render();
|
|
2240
|
+
});
|
|
2241
|
+
generalVoice.subscribe(() => {
|
|
2242
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
2243
|
+
generalAudioPlayer.start().catch(() => {});
|
|
2244
|
+
}
|
|
2245
|
+
render();
|
|
2246
|
+
});
|
|
1390
2247
|
startGuidedButton.addEventListener("click", () => {
|
|
1391
2248
|
startMode("guided");
|
|
1392
2249
|
});
|
|
@@ -1399,6 +2256,10 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1399
2256
|
window.addEventListener("beforeunload", () => {
|
|
1400
2257
|
guidedVoice.stopRecording();
|
|
1401
2258
|
generalVoice.stopRecording();
|
|
2259
|
+
guidedBargeInBinding?.close();
|
|
2260
|
+
generalBargeInBinding?.close();
|
|
2261
|
+
guidedAudioPlayer.close();
|
|
2262
|
+
generalAudioPlayer.close();
|
|
1402
2263
|
stopGuidedBinding();
|
|
1403
2264
|
stopGeneralBinding();
|
|
1404
2265
|
guidedVoice.close();
|