@absolutejs/voice 0.0.22-beta.31 → 0.0.22-beta.311
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 +3250 -73
- 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 +3498 -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/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/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 +703 -13
- package/dist/client/index.d.ts +70 -0
- package/dist/client/index.js +5257 -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 +28245 -5219
- package/dist/latencySlo.d.ts +56 -0
- package/dist/liveLatency.d.ts +78 -0
- package/dist/liveOps.d.ts +190 -0
- package/dist/mediaPipeline.d.ts +125 -0
- package/dist/mediaPipelineRoutes.d.ts +93 -0
- package/dist/modelAdapters.d.ts +60 -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 +559 -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/providerHealth.d.ts +1 -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 +5059 -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 +4924 -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 +1767 -44
- 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 +97 -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 +4828 -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 +775 -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 +1 -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,
|
|
@@ -269,10 +285,12 @@ var isVoiceServerMessage = (value) => {
|
|
|
269
285
|
case "assistant":
|
|
270
286
|
case "call_lifecycle":
|
|
271
287
|
case "complete":
|
|
288
|
+
case "connection":
|
|
272
289
|
case "error":
|
|
273
290
|
case "final":
|
|
274
291
|
case "partial":
|
|
275
292
|
case "pong":
|
|
293
|
+
case "replay":
|
|
276
294
|
case "session":
|
|
277
295
|
case "turn":
|
|
278
296
|
return true;
|
|
@@ -309,6 +327,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
309
327
|
sessionId: options.sessionId ?? createSessionId(),
|
|
310
328
|
ws: null
|
|
311
329
|
};
|
|
330
|
+
const emitConnection = (reconnect) => {
|
|
331
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
332
|
+
};
|
|
312
333
|
const clearTimers = () => {
|
|
313
334
|
if (state.pingInterval) {
|
|
314
335
|
clearInterval(state.pingInterval);
|
|
@@ -331,9 +352,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
331
352
|
}
|
|
332
353
|
};
|
|
333
354
|
const scheduleReconnect = () => {
|
|
355
|
+
const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
|
|
334
356
|
state.reconnectAttempts += 1;
|
|
357
|
+
emitConnection({
|
|
358
|
+
reconnect: {
|
|
359
|
+
attempts: state.reconnectAttempts,
|
|
360
|
+
lastDisconnectAt: Date.now(),
|
|
361
|
+
maxAttempts: maxReconnectAttempts,
|
|
362
|
+
nextAttemptAt,
|
|
363
|
+
status: "reconnecting"
|
|
364
|
+
},
|
|
365
|
+
type: "connection"
|
|
366
|
+
});
|
|
335
367
|
state.reconnectTimeout = setTimeout(() => {
|
|
336
368
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
369
|
+
emitConnection({
|
|
370
|
+
reconnect: {
|
|
371
|
+
attempts: state.reconnectAttempts,
|
|
372
|
+
maxAttempts: maxReconnectAttempts,
|
|
373
|
+
status: "exhausted"
|
|
374
|
+
},
|
|
375
|
+
type: "connection"
|
|
376
|
+
});
|
|
337
377
|
return;
|
|
338
378
|
}
|
|
339
379
|
connect();
|
|
@@ -343,9 +383,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
343
383
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
|
|
344
384
|
ws.binaryType = "arraybuffer";
|
|
345
385
|
ws.onopen = () => {
|
|
386
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
346
387
|
state.isConnected = true;
|
|
347
|
-
state.reconnectAttempts = 0;
|
|
348
388
|
flushPendingMessages();
|
|
389
|
+
if (wasReconnecting) {
|
|
390
|
+
emitConnection({
|
|
391
|
+
reconnect: {
|
|
392
|
+
attempts: state.reconnectAttempts,
|
|
393
|
+
lastResumedAt: Date.now(),
|
|
394
|
+
maxAttempts: maxReconnectAttempts,
|
|
395
|
+
status: "resumed"
|
|
396
|
+
},
|
|
397
|
+
type: "connection"
|
|
398
|
+
});
|
|
399
|
+
state.reconnectAttempts = 0;
|
|
400
|
+
}
|
|
349
401
|
listeners.forEach((listener) => listener({
|
|
350
402
|
scenarioId: state.scenarioId ?? undefined,
|
|
351
403
|
sessionId: state.sessionId,
|
|
@@ -375,6 +427,16 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
375
427
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
|
|
376
428
|
if (reconnectable) {
|
|
377
429
|
scheduleReconnect();
|
|
430
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
431
|
+
emitConnection({
|
|
432
|
+
reconnect: {
|
|
433
|
+
attempts: state.reconnectAttempts,
|
|
434
|
+
lastDisconnectAt: Date.now(),
|
|
435
|
+
maxAttempts: maxReconnectAttempts,
|
|
436
|
+
status: "exhausted"
|
|
437
|
+
},
|
|
438
|
+
type: "connection"
|
|
439
|
+
});
|
|
378
440
|
}
|
|
379
441
|
};
|
|
380
442
|
state.ws = ws;
|
|
@@ -445,6 +507,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
445
507
|
};
|
|
446
508
|
|
|
447
509
|
// src/client/store.ts
|
|
510
|
+
var createInitialReconnectState = () => ({
|
|
511
|
+
attempts: 0,
|
|
512
|
+
maxAttempts: 0,
|
|
513
|
+
status: "idle"
|
|
514
|
+
});
|
|
448
515
|
var createInitialState = () => ({
|
|
449
516
|
assistantAudio: [],
|
|
450
517
|
assistantTexts: [],
|
|
@@ -453,6 +520,7 @@ var createInitialState = () => ({
|
|
|
453
520
|
isConnected: false,
|
|
454
521
|
scenarioId: null,
|
|
455
522
|
partial: "",
|
|
523
|
+
reconnect: createInitialReconnectState(),
|
|
456
524
|
sessionId: null,
|
|
457
525
|
status: "idle",
|
|
458
526
|
turns: []
|
|
@@ -509,7 +577,19 @@ var createVoiceStreamStore = () => {
|
|
|
509
577
|
case "connected":
|
|
510
578
|
state = {
|
|
511
579
|
...state,
|
|
512
|
-
isConnected: true
|
|
580
|
+
isConnected: true,
|
|
581
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
582
|
+
...state.reconnect,
|
|
583
|
+
lastResumedAt: Date.now(),
|
|
584
|
+
nextAttemptAt: undefined,
|
|
585
|
+
status: "resumed"
|
|
586
|
+
} : state.reconnect
|
|
587
|
+
};
|
|
588
|
+
break;
|
|
589
|
+
case "connection":
|
|
590
|
+
state = {
|
|
591
|
+
...state,
|
|
592
|
+
reconnect: action.reconnect
|
|
513
593
|
};
|
|
514
594
|
break;
|
|
515
595
|
case "disconnected":
|
|
@@ -537,6 +617,26 @@ var createVoiceStreamStore = () => {
|
|
|
537
617
|
partial: action.transcript.text
|
|
538
618
|
};
|
|
539
619
|
break;
|
|
620
|
+
case "replay":
|
|
621
|
+
state = {
|
|
622
|
+
...state,
|
|
623
|
+
assistantTexts: [...action.assistantTexts],
|
|
624
|
+
call: action.call ?? null,
|
|
625
|
+
error: null,
|
|
626
|
+
isConnected: action.status === "active",
|
|
627
|
+
partial: action.partial,
|
|
628
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
629
|
+
...state.reconnect,
|
|
630
|
+
lastResumedAt: Date.now(),
|
|
631
|
+
nextAttemptAt: undefined,
|
|
632
|
+
status: "resumed"
|
|
633
|
+
} : state.reconnect,
|
|
634
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
635
|
+
sessionId: action.sessionId,
|
|
636
|
+
status: action.status,
|
|
637
|
+
turns: [...action.turns]
|
|
638
|
+
};
|
|
639
|
+
break;
|
|
540
640
|
case "session":
|
|
541
641
|
state = {
|
|
542
642
|
...state,
|
|
@@ -584,10 +684,34 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
584
684
|
const notify = () => {
|
|
585
685
|
subscribers.forEach((subscriber) => subscriber());
|
|
586
686
|
};
|
|
687
|
+
const reportReconnect = () => {
|
|
688
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const snapshot = store.getSnapshot();
|
|
692
|
+
const body = JSON.stringify({
|
|
693
|
+
at: Date.now(),
|
|
694
|
+
reconnect: snapshot.reconnect,
|
|
695
|
+
scenarioId: snapshot.scenarioId,
|
|
696
|
+
sessionId: connection.getSessionId(),
|
|
697
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
698
|
+
});
|
|
699
|
+
fetch(options.reconnectReportPath, {
|
|
700
|
+
body,
|
|
701
|
+
headers: {
|
|
702
|
+
"Content-Type": "application/json"
|
|
703
|
+
},
|
|
704
|
+
keepalive: true,
|
|
705
|
+
method: "POST"
|
|
706
|
+
}).catch(() => {});
|
|
707
|
+
};
|
|
587
708
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
588
709
|
const action = serverMessageToAction(message);
|
|
589
710
|
if (action) {
|
|
590
711
|
store.dispatch(action);
|
|
712
|
+
if (message.type === "connection") {
|
|
713
|
+
reportReconnect();
|
|
714
|
+
}
|
|
591
715
|
notify();
|
|
592
716
|
}
|
|
593
717
|
});
|
|
@@ -623,6 +747,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
623
747
|
get partial() {
|
|
624
748
|
return store.getSnapshot().partial;
|
|
625
749
|
},
|
|
750
|
+
get reconnect() {
|
|
751
|
+
return store.getSnapshot().reconnect;
|
|
752
|
+
},
|
|
626
753
|
get sessionId() {
|
|
627
754
|
return connection.getSessionId();
|
|
628
755
|
},
|
|
@@ -941,6 +1068,7 @@ var createInitialState2 = (stream) => ({
|
|
|
941
1068
|
isConnected: stream.isConnected,
|
|
942
1069
|
isRecording: false,
|
|
943
1070
|
partial: stream.partial,
|
|
1071
|
+
reconnect: stream.reconnect,
|
|
944
1072
|
recordingError: null,
|
|
945
1073
|
sessionId: stream.sessionId,
|
|
946
1074
|
scenarioId: stream.scenarioId,
|
|
@@ -970,6 +1098,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
970
1098
|
error: stream.error,
|
|
971
1099
|
isConnected: stream.isConnected,
|
|
972
1100
|
partial: stream.partial,
|
|
1101
|
+
reconnect: stream.reconnect,
|
|
973
1102
|
sessionId: stream.sessionId,
|
|
974
1103
|
scenarioId: stream.scenarioId,
|
|
975
1104
|
status: stream.status,
|
|
@@ -994,7 +1123,13 @@ var createVoiceController = (path, options = {}) => {
|
|
|
994
1123
|
capture = createMicrophoneCapture({
|
|
995
1124
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
996
1125
|
onLevel: options.capture?.onLevel,
|
|
997
|
-
onAudio: (audio) =>
|
|
1126
|
+
onAudio: (audio) => {
|
|
1127
|
+
if (options.capture?.onAudio) {
|
|
1128
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
stream.sendAudio(audio);
|
|
1132
|
+
},
|
|
998
1133
|
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
|
|
999
1134
|
});
|
|
1000
1135
|
return capture;
|
|
@@ -1064,6 +1199,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1064
1199
|
get recordingError() {
|
|
1065
1200
|
return state.recordingError;
|
|
1066
1201
|
},
|
|
1202
|
+
get reconnect() {
|
|
1203
|
+
return state.reconnect;
|
|
1204
|
+
},
|
|
1067
1205
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1068
1206
|
get sessionId() {
|
|
1069
1207
|
return state.sessionId;
|
|
@@ -1104,6 +1242,475 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1104
1242
|
};
|
|
1105
1243
|
};
|
|
1106
1244
|
|
|
1245
|
+
// src/client/audioPlayer.ts
|
|
1246
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1247
|
+
var createInitialState3 = () => ({
|
|
1248
|
+
activeSourceCount: 0,
|
|
1249
|
+
error: null,
|
|
1250
|
+
isActive: false,
|
|
1251
|
+
isPlaying: false,
|
|
1252
|
+
lastInterruptLatencyMs: undefined,
|
|
1253
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
1254
|
+
processedChunkCount: 0,
|
|
1255
|
+
queuedChunkCount: 0
|
|
1256
|
+
});
|
|
1257
|
+
var getAudioContextCtor = () => {
|
|
1258
|
+
if (typeof window === "undefined") {
|
|
1259
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
1260
|
+
}
|
|
1261
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
1262
|
+
};
|
|
1263
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
1264
|
+
const format = chunk.format;
|
|
1265
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
1266
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
1267
|
+
}
|
|
1268
|
+
const bytes = chunk.chunk;
|
|
1269
|
+
const channels = Math.max(1, format.channels);
|
|
1270
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
1271
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
1272
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
1273
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1274
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1275
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
1276
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
1277
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
1278
|
+
const sampleOffset = sampleIndex * 2;
|
|
1279
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
1280
|
+
channelData[frameIndex] = 0;
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return audioBuffer;
|
|
1287
|
+
};
|
|
1288
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
1289
|
+
const subscribers = new Set;
|
|
1290
|
+
const sourceNodes = new Set;
|
|
1291
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
1292
|
+
let state = createInitialState3();
|
|
1293
|
+
let audioContext = null;
|
|
1294
|
+
let outputNode = null;
|
|
1295
|
+
let queueEndTime = 0;
|
|
1296
|
+
let syncPromise = Promise.resolve();
|
|
1297
|
+
let interruptStartedAt = null;
|
|
1298
|
+
let interruptPromise = null;
|
|
1299
|
+
let resolveInterruptPromise = null;
|
|
1300
|
+
let interruptFallbackTimer = null;
|
|
1301
|
+
const notify = () => {
|
|
1302
|
+
for (const subscriber of subscribers) {
|
|
1303
|
+
subscriber();
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
const setState = (next) => {
|
|
1307
|
+
state = {
|
|
1308
|
+
...state,
|
|
1309
|
+
...next
|
|
1310
|
+
};
|
|
1311
|
+
notify();
|
|
1312
|
+
};
|
|
1313
|
+
const clearError = () => {
|
|
1314
|
+
if (state.error !== null) {
|
|
1315
|
+
setState({ error: null });
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
const clearInterruptTimer = () => {
|
|
1319
|
+
if (interruptFallbackTimer !== null) {
|
|
1320
|
+
clearTimeout(interruptFallbackTimer);
|
|
1321
|
+
interruptFallbackTimer = null;
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
const resolveInterrupt = (latencyMs) => {
|
|
1325
|
+
clearInterruptTimer();
|
|
1326
|
+
interruptStartedAt = null;
|
|
1327
|
+
setState({
|
|
1328
|
+
activeSourceCount: sourceNodes.size,
|
|
1329
|
+
isPlaying: false,
|
|
1330
|
+
lastInterruptLatencyMs: latencyMs,
|
|
1331
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
1332
|
+
});
|
|
1333
|
+
resolveInterruptPromise?.();
|
|
1334
|
+
resolveInterruptPromise = null;
|
|
1335
|
+
interruptPromise = null;
|
|
1336
|
+
};
|
|
1337
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
1338
|
+
if (!context) {
|
|
1339
|
+
return 0;
|
|
1340
|
+
}
|
|
1341
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
1342
|
+
};
|
|
1343
|
+
const restoreOutputGain = (context) => {
|
|
1344
|
+
if (!outputNode) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const gainValue = 1;
|
|
1348
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1349
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
outputNode.gain.value = gainValue;
|
|
1353
|
+
};
|
|
1354
|
+
const muteOutputGain = (context) => {
|
|
1355
|
+
if (!outputNode) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const gainValue = 0;
|
|
1359
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1360
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
outputNode.gain.value = gainValue;
|
|
1364
|
+
};
|
|
1365
|
+
const maybeResolveInterrupt = () => {
|
|
1366
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
1370
|
+
};
|
|
1371
|
+
const ensureAudioContext = async () => {
|
|
1372
|
+
if (audioContext) {
|
|
1373
|
+
return audioContext;
|
|
1374
|
+
}
|
|
1375
|
+
if (options.createAudioContext) {
|
|
1376
|
+
audioContext = options.createAudioContext();
|
|
1377
|
+
} else {
|
|
1378
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
1379
|
+
if (!AudioContextCtor) {
|
|
1380
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
1381
|
+
}
|
|
1382
|
+
audioContext = new AudioContextCtor;
|
|
1383
|
+
}
|
|
1384
|
+
if (audioContext.createGain) {
|
|
1385
|
+
outputNode = audioContext.createGain();
|
|
1386
|
+
outputNode.connect?.(audioContext.destination);
|
|
1387
|
+
}
|
|
1388
|
+
queueEndTime = audioContext.currentTime;
|
|
1389
|
+
return audioContext;
|
|
1390
|
+
};
|
|
1391
|
+
const scheduleChunk = async (chunk) => {
|
|
1392
|
+
const context = await ensureAudioContext();
|
|
1393
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
1394
|
+
const node = context.createBufferSource();
|
|
1395
|
+
node.buffer = buffer;
|
|
1396
|
+
node.connect(outputNode ?? context.destination);
|
|
1397
|
+
node.onended = () => {
|
|
1398
|
+
sourceNodes.delete(node);
|
|
1399
|
+
node.disconnect?.();
|
|
1400
|
+
setState({
|
|
1401
|
+
activeSourceCount: sourceNodes.size,
|
|
1402
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
1403
|
+
});
|
|
1404
|
+
maybeResolveInterrupt();
|
|
1405
|
+
};
|
|
1406
|
+
const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
|
|
1407
|
+
queueEndTime = startAt + buffer.duration;
|
|
1408
|
+
sourceNodes.add(node);
|
|
1409
|
+
setState({
|
|
1410
|
+
activeSourceCount: sourceNodes.size,
|
|
1411
|
+
isPlaying: true
|
|
1412
|
+
});
|
|
1413
|
+
node.start(startAt);
|
|
1414
|
+
};
|
|
1415
|
+
const stopQueuedPlayback = (options2) => {
|
|
1416
|
+
for (const node of [...sourceNodes]) {
|
|
1417
|
+
node.stop?.();
|
|
1418
|
+
}
|
|
1419
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
1420
|
+
if (options2?.forceClear) {
|
|
1421
|
+
for (const node of sourceNodes) {
|
|
1422
|
+
node.disconnect?.();
|
|
1423
|
+
}
|
|
1424
|
+
sourceNodes.clear();
|
|
1425
|
+
maybeResolveInterrupt();
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
const sync = async () => {
|
|
1429
|
+
if (!state.isActive) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
1433
|
+
if (nextChunks.length === 0) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
try {
|
|
1437
|
+
clearError();
|
|
1438
|
+
for (const chunk of nextChunks) {
|
|
1439
|
+
await scheduleChunk(chunk);
|
|
1440
|
+
}
|
|
1441
|
+
setState({
|
|
1442
|
+
processedChunkCount: source.assistantAudio.length,
|
|
1443
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
1444
|
+
});
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
setState({
|
|
1447
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
const queueSync = () => {
|
|
1452
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
1453
|
+
return syncPromise;
|
|
1454
|
+
};
|
|
1455
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
1456
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
1457
|
+
player.start();
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (state.isActive) {
|
|
1461
|
+
queueSync();
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
const player = {
|
|
1465
|
+
close: async () => {
|
|
1466
|
+
unsubscribeSource();
|
|
1467
|
+
stopQueuedPlayback({ forceClear: true });
|
|
1468
|
+
clearInterruptTimer();
|
|
1469
|
+
resolveInterruptPromise?.();
|
|
1470
|
+
resolveInterruptPromise = null;
|
|
1471
|
+
interruptPromise = null;
|
|
1472
|
+
interruptStartedAt = null;
|
|
1473
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
1474
|
+
await audioContext.close();
|
|
1475
|
+
}
|
|
1476
|
+
audioContext = null;
|
|
1477
|
+
outputNode?.disconnect?.();
|
|
1478
|
+
outputNode = null;
|
|
1479
|
+
queueEndTime = 0;
|
|
1480
|
+
setState({
|
|
1481
|
+
activeSourceCount: 0,
|
|
1482
|
+
isActive: false,
|
|
1483
|
+
isPlaying: false
|
|
1484
|
+
});
|
|
1485
|
+
},
|
|
1486
|
+
get activeSourceCount() {
|
|
1487
|
+
return state.activeSourceCount;
|
|
1488
|
+
},
|
|
1489
|
+
get error() {
|
|
1490
|
+
return state.error;
|
|
1491
|
+
},
|
|
1492
|
+
getSnapshot: () => state,
|
|
1493
|
+
get isActive() {
|
|
1494
|
+
return state.isActive;
|
|
1495
|
+
},
|
|
1496
|
+
get isPlaying() {
|
|
1497
|
+
return state.isPlaying;
|
|
1498
|
+
},
|
|
1499
|
+
interrupt: async () => {
|
|
1500
|
+
const startedAt = Date.now();
|
|
1501
|
+
const context = await ensureAudioContext();
|
|
1502
|
+
interruptStartedAt = startedAt;
|
|
1503
|
+
muteOutputGain(context);
|
|
1504
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
1505
|
+
setState({
|
|
1506
|
+
isActive: false,
|
|
1507
|
+
isPlaying: sourceNodes.size > 0,
|
|
1508
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
1509
|
+
});
|
|
1510
|
+
if (sourceNodes.size === 0) {
|
|
1511
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (!interruptPromise) {
|
|
1515
|
+
interruptPromise = new Promise((resolve) => {
|
|
1516
|
+
resolveInterruptPromise = resolve;
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
clearInterruptTimer();
|
|
1520
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
1521
|
+
for (const node of sourceNodes) {
|
|
1522
|
+
node.disconnect?.();
|
|
1523
|
+
}
|
|
1524
|
+
sourceNodes.clear();
|
|
1525
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
1526
|
+
}, 250);
|
|
1527
|
+
stopQueuedPlayback();
|
|
1528
|
+
await interruptPromise;
|
|
1529
|
+
},
|
|
1530
|
+
get lastInterruptLatencyMs() {
|
|
1531
|
+
return state.lastInterruptLatencyMs;
|
|
1532
|
+
},
|
|
1533
|
+
get lastPlaybackStopLatencyMs() {
|
|
1534
|
+
return state.lastPlaybackStopLatencyMs;
|
|
1535
|
+
},
|
|
1536
|
+
pause: async () => {
|
|
1537
|
+
if (!audioContext) {
|
|
1538
|
+
setState({
|
|
1539
|
+
activeSourceCount: 0,
|
|
1540
|
+
isActive: false,
|
|
1541
|
+
isPlaying: false
|
|
1542
|
+
});
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
await audioContext.suspend();
|
|
1546
|
+
setState({
|
|
1547
|
+
activeSourceCount: sourceNodes.size,
|
|
1548
|
+
isActive: false,
|
|
1549
|
+
isPlaying: false
|
|
1550
|
+
});
|
|
1551
|
+
},
|
|
1552
|
+
get processedChunkCount() {
|
|
1553
|
+
return state.processedChunkCount;
|
|
1554
|
+
},
|
|
1555
|
+
get queuedChunkCount() {
|
|
1556
|
+
return state.queuedChunkCount;
|
|
1557
|
+
},
|
|
1558
|
+
start: async () => {
|
|
1559
|
+
try {
|
|
1560
|
+
clearError();
|
|
1561
|
+
const context = await ensureAudioContext();
|
|
1562
|
+
restoreOutputGain(context);
|
|
1563
|
+
if (context.state === "suspended") {
|
|
1564
|
+
await context.resume();
|
|
1565
|
+
}
|
|
1566
|
+
setState({
|
|
1567
|
+
activeSourceCount: sourceNodes.size,
|
|
1568
|
+
isActive: true,
|
|
1569
|
+
isPlaying: context.state === "running"
|
|
1570
|
+
});
|
|
1571
|
+
await queueSync();
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
setState({
|
|
1574
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1575
|
+
isActive: false,
|
|
1576
|
+
isPlaying: false
|
|
1577
|
+
});
|
|
1578
|
+
throw error;
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
subscribe: (subscriber) => {
|
|
1582
|
+
subscribers.add(subscriber);
|
|
1583
|
+
return () => {
|
|
1584
|
+
subscribers.delete(subscriber);
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
return player;
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
// src/client/bargeInMonitor.ts
|
|
1592
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
1593
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
1594
|
+
var summarize = (events, thresholdMs) => {
|
|
1595
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
1596
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
1597
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
1598
|
+
const passed = stopped.length - failed;
|
|
1599
|
+
return {
|
|
1600
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
1601
|
+
events: [...events],
|
|
1602
|
+
failed,
|
|
1603
|
+
lastEvent: events.at(-1),
|
|
1604
|
+
passed,
|
|
1605
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
1606
|
+
thresholdMs,
|
|
1607
|
+
total: stopped.length
|
|
1608
|
+
};
|
|
1609
|
+
};
|
|
1610
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
1611
|
+
const listeners = new Set;
|
|
1612
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
1613
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1614
|
+
const events = [];
|
|
1615
|
+
const emit = () => {
|
|
1616
|
+
for (const listener of listeners) {
|
|
1617
|
+
listener();
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
const postEvent = (event) => {
|
|
1621
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
fetchImpl(options.path, {
|
|
1625
|
+
body: JSON.stringify(event),
|
|
1626
|
+
headers: {
|
|
1627
|
+
"Content-Type": "application/json"
|
|
1628
|
+
},
|
|
1629
|
+
method: "POST"
|
|
1630
|
+
}).catch(() => {});
|
|
1631
|
+
};
|
|
1632
|
+
const record = (status, input) => {
|
|
1633
|
+
const event = {
|
|
1634
|
+
at: Date.now(),
|
|
1635
|
+
id: createEventId(),
|
|
1636
|
+
latencyMs: input.latencyMs,
|
|
1637
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
1638
|
+
reason: input.reason,
|
|
1639
|
+
sessionId: input.sessionId,
|
|
1640
|
+
status,
|
|
1641
|
+
thresholdMs
|
|
1642
|
+
};
|
|
1643
|
+
events.push(event);
|
|
1644
|
+
postEvent(event);
|
|
1645
|
+
emit();
|
|
1646
|
+
return event;
|
|
1647
|
+
};
|
|
1648
|
+
return {
|
|
1649
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
1650
|
+
recordRequested: (input) => record("requested", input),
|
|
1651
|
+
recordSkipped: (input) => record("skipped", input),
|
|
1652
|
+
recordStopped: (input) => record("stopped", input),
|
|
1653
|
+
subscribe: (subscriber) => {
|
|
1654
|
+
listeners.add(subscriber);
|
|
1655
|
+
return () => {
|
|
1656
|
+
listeners.delete(subscriber);
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
// src/client/duplex.ts
|
|
1663
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
1664
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
1665
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
1666
|
+
let lastPartial = controller.partial;
|
|
1667
|
+
const interruptIfPlaying = (reason) => {
|
|
1668
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
1669
|
+
options.monitor?.recordSkipped({
|
|
1670
|
+
reason,
|
|
1671
|
+
sessionId: controller.sessionId
|
|
1672
|
+
});
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
options.monitor?.recordRequested({
|
|
1676
|
+
reason,
|
|
1677
|
+
sessionId: controller.sessionId
|
|
1678
|
+
});
|
|
1679
|
+
player.interrupt().then(() => {
|
|
1680
|
+
options.monitor?.recordStopped({
|
|
1681
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
1682
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
1683
|
+
reason,
|
|
1684
|
+
sessionId: controller.sessionId
|
|
1685
|
+
});
|
|
1686
|
+
});
|
|
1687
|
+
};
|
|
1688
|
+
const unsubscribe = controller.subscribe(() => {
|
|
1689
|
+
if (options.interruptOnPartial === false) {
|
|
1690
|
+
lastPartial = controller.partial;
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (!lastPartial && controller.partial) {
|
|
1694
|
+
interruptIfPlaying("partial-transcript");
|
|
1695
|
+
}
|
|
1696
|
+
lastPartial = controller.partial;
|
|
1697
|
+
});
|
|
1698
|
+
return {
|
|
1699
|
+
close: () => {
|
|
1700
|
+
unsubscribe();
|
|
1701
|
+
},
|
|
1702
|
+
handleLevel: (level) => {
|
|
1703
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
1704
|
+
interruptIfPlaying("input-level");
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
sendAudio: (audio) => {
|
|
1708
|
+
interruptIfPlaying("manual-audio");
|
|
1709
|
+
controller.sendAudio(audio);
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1107
1714
|
// src/client/htmxBootstrap.ts
|
|
1108
1715
|
var VOICE_WAVE_POINTS = 48;
|
|
1109
1716
|
var VOICE_WAVE_WIDTH = 320;
|
|
@@ -1160,6 +1767,17 @@ var formatErrorMessage = (error) => {
|
|
|
1160
1767
|
}
|
|
1161
1768
|
return "Unexpected error";
|
|
1162
1769
|
};
|
|
1770
|
+
var formatReconnectState = (reconnect) => {
|
|
1771
|
+
const pieces = [reconnect.status];
|
|
1772
|
+
if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
|
|
1773
|
+
pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
|
|
1774
|
+
}
|
|
1775
|
+
if (reconnect.nextAttemptAt) {
|
|
1776
|
+
const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
|
|
1777
|
+
pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
|
|
1778
|
+
}
|
|
1779
|
+
return pieces.join(" · ");
|
|
1780
|
+
};
|
|
1163
1781
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1164
1782
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1165
1783
|
const next = levels.slice(-(count - 1));
|
|
@@ -1216,6 +1834,17 @@ var parsePromptList = (value) => {
|
|
|
1216
1834
|
} catch {}
|
|
1217
1835
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1218
1836
|
};
|
|
1837
|
+
var parseOptionalNumber = (value) => {
|
|
1838
|
+
if (!value) {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const parsed = Number(value);
|
|
1842
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1843
|
+
};
|
|
1844
|
+
var resolveElement2 = (root, selector, ctor) => {
|
|
1845
|
+
const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
|
|
1846
|
+
return value instanceof ctor ? value : null;
|
|
1847
|
+
};
|
|
1219
1848
|
var requireElement = (root, selector, ctor, name) => {
|
|
1220
1849
|
const value = selector ? document.querySelector(selector) : null;
|
|
1221
1850
|
if (value instanceof ctor) {
|
|
@@ -1266,11 +1895,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1266
1895
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1267
1896
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1268
1897
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
1898
|
+
const reconnectReportPath = root.dataset.voiceReconnectReportPath;
|
|
1899
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
1900
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
1901
|
+
path: bargeInPath,
|
|
1902
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
1903
|
+
}) : null;
|
|
1904
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
1905
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1269
1906
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1270
1907
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1271
1908
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1272
1909
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1273
1910
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
1911
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1274
1912
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1275
1913
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1276
1914
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1279,35 +1917,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1279
1917
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1280
1918
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1281
1919
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
1920
|
+
let activeMode = null;
|
|
1921
|
+
let hasStartedModes = {
|
|
1922
|
+
general: false,
|
|
1923
|
+
guided: false
|
|
1924
|
+
};
|
|
1925
|
+
let isCapturing = false;
|
|
1926
|
+
let micError = null;
|
|
1927
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
1928
|
+
let guidedBargeInBinding = null;
|
|
1929
|
+
let generalBargeInBinding = null;
|
|
1282
1930
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1283
1931
|
capture: {
|
|
1932
|
+
onAudio: (audio, sendAudio) => {
|
|
1933
|
+
if (guidedBargeInBinding) {
|
|
1934
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
sendAudio(audio);
|
|
1938
|
+
},
|
|
1284
1939
|
onLevel: (level) => {
|
|
1940
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1285
1941
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1286
1942
|
renderWave();
|
|
1287
1943
|
}
|
|
1288
1944
|
},
|
|
1945
|
+
connection: {
|
|
1946
|
+
reconnectReportPath
|
|
1947
|
+
},
|
|
1289
1948
|
preset: "guided-intake"
|
|
1290
1949
|
});
|
|
1291
1950
|
const generalVoice = createVoiceController(generalPath, {
|
|
1292
1951
|
capture: {
|
|
1952
|
+
onAudio: (audio, sendAudio) => {
|
|
1953
|
+
if (generalBargeInBinding) {
|
|
1954
|
+
generalBargeInBinding.sendAudio(audio);
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
sendAudio(audio);
|
|
1958
|
+
},
|
|
1293
1959
|
onLevel: (level) => {
|
|
1960
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1294
1961
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1295
1962
|
renderWave();
|
|
1296
1963
|
}
|
|
1297
1964
|
},
|
|
1965
|
+
connection: {
|
|
1966
|
+
reconnectReportPath
|
|
1967
|
+
},
|
|
1298
1968
|
preset: "dictation"
|
|
1299
1969
|
});
|
|
1300
1970
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1301
1971
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1972
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
1973
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
1974
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
1975
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
1976
|
+
monitor: bargeInMonitor ?? undefined
|
|
1977
|
+
});
|
|
1978
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
1979
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
1980
|
+
monitor: bargeInMonitor ?? undefined
|
|
1981
|
+
});
|
|
1310
1982
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
1983
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1311
1984
|
const renderWave = () => {
|
|
1312
1985
|
const path = createVoiceWavePath(waveLevels);
|
|
1313
1986
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1322,6 +1995,9 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1322
1995
|
const status = voice.status;
|
|
1323
1996
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1324
1997
|
errorStatus.textContent = micError || voice.error || "None";
|
|
1998
|
+
if (reconnectStatus) {
|
|
1999
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2000
|
+
}
|
|
1325
2001
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1326
2002
|
promptStatus.textContent = resolvePromptMessage({
|
|
1327
2003
|
guidedPrompts,
|
|
@@ -1385,8 +2061,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1385
2061
|
render();
|
|
1386
2062
|
}
|
|
1387
2063
|
};
|
|
1388
|
-
guidedVoice.subscribe(
|
|
1389
|
-
|
|
2064
|
+
guidedVoice.subscribe(() => {
|
|
2065
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
2066
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
2067
|
+
}
|
|
2068
|
+
render();
|
|
2069
|
+
});
|
|
2070
|
+
generalVoice.subscribe(() => {
|
|
2071
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
2072
|
+
generalAudioPlayer.start().catch(() => {});
|
|
2073
|
+
}
|
|
2074
|
+
render();
|
|
2075
|
+
});
|
|
1390
2076
|
startGuidedButton.addEventListener("click", () => {
|
|
1391
2077
|
startMode("guided");
|
|
1392
2078
|
});
|
|
@@ -1399,6 +2085,10 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1399
2085
|
window.addEventListener("beforeunload", () => {
|
|
1400
2086
|
guidedVoice.stopRecording();
|
|
1401
2087
|
generalVoice.stopRecording();
|
|
2088
|
+
guidedBargeInBinding?.close();
|
|
2089
|
+
generalBargeInBinding?.close();
|
|
2090
|
+
guidedAudioPlayer.close();
|
|
2091
|
+
generalAudioPlayer.close();
|
|
1402
2092
|
stopGuidedBinding();
|
|
1403
2093
|
stopGeneralBinding();
|
|
1404
2094
|
guidedVoice.close();
|