@absolutejs/voice 0.0.22-beta.26 → 0.0.22-beta.260
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 +3234 -228
- package/dist/agent.d.ts +62 -0
- package/dist/agentSquadContract.d.ts +98 -0
- package/dist/angular/index.d.ts +15 -0
- package/dist/angular/index.js +3387 -1093
- 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-routing-status.service.d.ts +11 -0
- package/dist/angular/voice-stream.service.d.ts +3 -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 +105 -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/connection.d.ts +3 -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 +747 -15
- package/dist/client/index.d.ts +66 -0
- package/dist/client/index.js +4972 -21
- 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/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/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/handoff.d.ts +54 -0
- package/dist/handoffHealth.d.ts +94 -0
- package/dist/incidentBundle.d.ts +116 -0
- package/dist/index.d.ts +132 -13
- package/dist/index.js +25379 -4971
- package/dist/latencySlo.d.ts +56 -0
- package/dist/liveLatency.d.ts +78 -0
- package/dist/liveOps.d.ts +122 -0
- package/dist/modelAdapters.d.ts +23 -2
- package/dist/observabilityExport.d.ts +481 -0
- package/dist/openaiRealtime.d.ts +27 -0
- package/dist/openaiTTS.d.ts +18 -0
- package/dist/operationsRecord.d.ts +210 -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 +116 -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 +484 -0
- package/dist/proofTrends.d.ts +133 -0
- package/dist/providerAdapters.d.ts +48 -0
- package/dist/providerCapabilities.d.ts +92 -0
- package/dist/providerHealth.d.ts +1 -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 +61 -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/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 +30 -0
- package/dist/react/index.js +4817 -33
- package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
- package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
- package/dist/react/useVoiceController.d.ts +3 -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/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/react/useVoiceStream.d.ts +3 -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 +37 -0
- package/dist/reconnectContract.d.ts +87 -0
- package/dist/resilienceRoutes.d.ts +143 -0
- package/dist/sessionReplay.d.ts +12 -0
- package/dist/simulationSuite.d.ts +143 -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/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 +16 -0
- package/dist/svelte/index.js +4754 -439
- package/dist/telephony/contract.d.ts +61 -0
- package/dist/telephony/matrix.d.ts +97 -0
- package/dist/telephony/plivo.d.ts +254 -0
- package/dist/telephony/telnyx.d.ts +247 -0
- package/dist/telephony/twilio.d.ts +135 -2
- package/dist/telephonyOutcome.d.ts +201 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2024 -69
- 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 +180 -4
- 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/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 +28 -0
- package/dist/vue/index.js +4597 -57
- 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/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/vue/useVoiceStream.d.ts +4 -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,17 @@ 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
|
+
};
|
|
196
|
+
case "call_lifecycle":
|
|
197
|
+
return {
|
|
198
|
+
event: message.event,
|
|
199
|
+
sessionId: message.sessionId,
|
|
200
|
+
type: "call_lifecycle"
|
|
201
|
+
};
|
|
191
202
|
case "error":
|
|
192
203
|
return {
|
|
193
204
|
message: normalizeErrorMessage(message.message),
|
|
@@ -203,6 +214,17 @@ var serverMessageToAction = (message) => {
|
|
|
203
214
|
transcript: message.transcript,
|
|
204
215
|
type: "partial"
|
|
205
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
|
+
};
|
|
206
228
|
case "session":
|
|
207
229
|
return {
|
|
208
230
|
sessionId: message.sessionId,
|
|
@@ -231,7 +253,7 @@ var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
|
231
253
|
var noop = () => {};
|
|
232
254
|
var noopUnsubscribe = () => noop;
|
|
233
255
|
var NOOP_CONNECTION = {
|
|
234
|
-
|
|
256
|
+
callControl: noop,
|
|
235
257
|
close: noop,
|
|
236
258
|
endTurn: noop,
|
|
237
259
|
getReadyState: () => WS_CLOSED,
|
|
@@ -239,6 +261,7 @@ var NOOP_CONNECTION = {
|
|
|
239
261
|
getSessionId: () => "",
|
|
240
262
|
send: noop,
|
|
241
263
|
sendAudio: noop,
|
|
264
|
+
start: () => {},
|
|
242
265
|
subscribe: noopUnsubscribe
|
|
243
266
|
};
|
|
244
267
|
var createSessionId = () => crypto.randomUUID();
|
|
@@ -260,11 +283,14 @@ var isVoiceServerMessage = (value) => {
|
|
|
260
283
|
switch (value.type) {
|
|
261
284
|
case "audio":
|
|
262
285
|
case "assistant":
|
|
286
|
+
case "call_lifecycle":
|
|
263
287
|
case "complete":
|
|
288
|
+
case "connection":
|
|
264
289
|
case "error":
|
|
265
290
|
case "final":
|
|
266
291
|
case "partial":
|
|
267
292
|
case "pong":
|
|
293
|
+
case "replay":
|
|
268
294
|
case "session":
|
|
269
295
|
case "turn":
|
|
270
296
|
return true;
|
|
@@ -301,6 +327,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
301
327
|
sessionId: options.sessionId ?? createSessionId(),
|
|
302
328
|
ws: null
|
|
303
329
|
};
|
|
330
|
+
const emitConnection = (reconnect) => {
|
|
331
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
332
|
+
};
|
|
304
333
|
const clearTimers = () => {
|
|
305
334
|
if (state.pingInterval) {
|
|
306
335
|
clearInterval(state.pingInterval);
|
|
@@ -323,9 +352,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
323
352
|
}
|
|
324
353
|
};
|
|
325
354
|
const scheduleReconnect = () => {
|
|
355
|
+
const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
|
|
326
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
|
+
});
|
|
327
367
|
state.reconnectTimeout = setTimeout(() => {
|
|
328
368
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
369
|
+
emitConnection({
|
|
370
|
+
reconnect: {
|
|
371
|
+
attempts: state.reconnectAttempts,
|
|
372
|
+
maxAttempts: maxReconnectAttempts,
|
|
373
|
+
status: "exhausted"
|
|
374
|
+
},
|
|
375
|
+
type: "connection"
|
|
376
|
+
});
|
|
329
377
|
return;
|
|
330
378
|
}
|
|
331
379
|
connect();
|
|
@@ -335,9 +383,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
335
383
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
|
|
336
384
|
ws.binaryType = "arraybuffer";
|
|
337
385
|
ws.onopen = () => {
|
|
386
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
338
387
|
state.isConnected = true;
|
|
339
|
-
state.reconnectAttempts = 0;
|
|
340
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
|
+
}
|
|
341
401
|
listeners.forEach((listener) => listener({
|
|
342
402
|
scenarioId: state.scenarioId ?? undefined,
|
|
343
403
|
sessionId: state.sessionId,
|
|
@@ -367,6 +427,16 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
367
427
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
|
|
368
428
|
if (reconnectable) {
|
|
369
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
|
+
});
|
|
370
440
|
}
|
|
371
441
|
};
|
|
372
442
|
state.ws = ws;
|
|
@@ -400,6 +470,12 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
400
470
|
const endTurn = () => {
|
|
401
471
|
send({ type: "end_turn" });
|
|
402
472
|
};
|
|
473
|
+
const callControl = (message) => {
|
|
474
|
+
send({
|
|
475
|
+
...message,
|
|
476
|
+
type: "call_control"
|
|
477
|
+
});
|
|
478
|
+
};
|
|
403
479
|
const close = () => {
|
|
404
480
|
clearTimers();
|
|
405
481
|
if (state.ws) {
|
|
@@ -417,7 +493,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
417
493
|
};
|
|
418
494
|
connect();
|
|
419
495
|
return {
|
|
420
|
-
|
|
496
|
+
callControl,
|
|
421
497
|
close,
|
|
422
498
|
endTurn,
|
|
423
499
|
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
@@ -425,18 +501,26 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
425
501
|
getSessionId: () => state.sessionId,
|
|
426
502
|
send,
|
|
427
503
|
sendAudio,
|
|
504
|
+
start,
|
|
428
505
|
subscribe
|
|
429
506
|
};
|
|
430
507
|
};
|
|
431
508
|
|
|
432
509
|
// src/client/store.ts
|
|
510
|
+
var createInitialReconnectState = () => ({
|
|
511
|
+
attempts: 0,
|
|
512
|
+
maxAttempts: 0,
|
|
513
|
+
status: "idle"
|
|
514
|
+
});
|
|
433
515
|
var createInitialState = () => ({
|
|
434
516
|
assistantAudio: [],
|
|
435
517
|
assistantTexts: [],
|
|
518
|
+
call: null,
|
|
436
519
|
error: null,
|
|
437
520
|
isConnected: false,
|
|
438
521
|
scenarioId: null,
|
|
439
522
|
partial: "",
|
|
523
|
+
reconnect: createInitialReconnectState(),
|
|
440
524
|
sessionId: null,
|
|
441
525
|
status: "idle",
|
|
442
526
|
turns: []
|
|
@@ -476,10 +560,36 @@ var createVoiceStreamStore = () => {
|
|
|
476
560
|
status: "completed"
|
|
477
561
|
};
|
|
478
562
|
break;
|
|
563
|
+
case "call_lifecycle":
|
|
564
|
+
state = {
|
|
565
|
+
...state,
|
|
566
|
+
call: {
|
|
567
|
+
...state.call,
|
|
568
|
+
disposition: action.event.type === "end" ? action.event.disposition : state.call?.disposition,
|
|
569
|
+
endedAt: action.event.type === "end" ? action.event.at : state.call?.endedAt,
|
|
570
|
+
events: [...state.call?.events ?? [], action.event],
|
|
571
|
+
lastEventAt: action.event.at,
|
|
572
|
+
startedAt: state.call?.startedAt ?? action.event.at
|
|
573
|
+
},
|
|
574
|
+
sessionId: action.sessionId
|
|
575
|
+
};
|
|
576
|
+
break;
|
|
479
577
|
case "connected":
|
|
480
578
|
state = {
|
|
481
579
|
...state,
|
|
482
|
-
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
|
|
483
593
|
};
|
|
484
594
|
break;
|
|
485
595
|
case "disconnected":
|
|
@@ -507,6 +617,26 @@ var createVoiceStreamStore = () => {
|
|
|
507
617
|
partial: action.transcript.text
|
|
508
618
|
};
|
|
509
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;
|
|
510
640
|
case "session":
|
|
511
641
|
state = {
|
|
512
642
|
...state,
|
|
@@ -554,14 +684,41 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
554
684
|
const notify = () => {
|
|
555
685
|
subscribers.forEach((subscriber) => subscriber());
|
|
556
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
|
+
};
|
|
557
708
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
558
709
|
const action = serverMessageToAction(message);
|
|
559
710
|
if (action) {
|
|
560
711
|
store.dispatch(action);
|
|
712
|
+
if (message.type === "connection") {
|
|
713
|
+
reportReconnect();
|
|
714
|
+
}
|
|
561
715
|
notify();
|
|
562
716
|
}
|
|
563
717
|
});
|
|
564
718
|
return {
|
|
719
|
+
callControl(message) {
|
|
720
|
+
connection.callControl(message);
|
|
721
|
+
},
|
|
565
722
|
close() {
|
|
566
723
|
unsubscribeConnection();
|
|
567
724
|
connection.close();
|
|
@@ -590,6 +747,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
590
747
|
get partial() {
|
|
591
748
|
return store.getSnapshot().partial;
|
|
592
749
|
},
|
|
750
|
+
get reconnect() {
|
|
751
|
+
return store.getSnapshot().reconnect;
|
|
752
|
+
},
|
|
593
753
|
get sessionId() {
|
|
594
754
|
return connection.getSessionId();
|
|
595
755
|
},
|
|
@@ -605,6 +765,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
605
765
|
get assistantAudio() {
|
|
606
766
|
return store.getSnapshot().assistantAudio;
|
|
607
767
|
},
|
|
768
|
+
get call() {
|
|
769
|
+
return store.getSnapshot().call;
|
|
770
|
+
},
|
|
608
771
|
sendAudio(audio) {
|
|
609
772
|
connection.sendAudio(audio);
|
|
610
773
|
},
|
|
@@ -900,10 +1063,12 @@ var resolveVoiceRuntimePreset = (name = "default") => {
|
|
|
900
1063
|
var createInitialState2 = (stream) => ({
|
|
901
1064
|
assistantAudio: [...stream.assistantAudio],
|
|
902
1065
|
assistantTexts: [...stream.assistantTexts],
|
|
1066
|
+
call: stream.call,
|
|
903
1067
|
error: stream.error,
|
|
904
1068
|
isConnected: stream.isConnected,
|
|
905
1069
|
isRecording: false,
|
|
906
1070
|
partial: stream.partial,
|
|
1071
|
+
reconnect: stream.reconnect,
|
|
907
1072
|
recordingError: null,
|
|
908
1073
|
sessionId: stream.sessionId,
|
|
909
1074
|
scenarioId: stream.scenarioId,
|
|
@@ -929,9 +1094,11 @@ var createVoiceController = (path, options = {}) => {
|
|
|
929
1094
|
...state,
|
|
930
1095
|
assistantAudio: [...stream.assistantAudio],
|
|
931
1096
|
assistantTexts: [...stream.assistantTexts],
|
|
1097
|
+
call: stream.call,
|
|
932
1098
|
error: stream.error,
|
|
933
1099
|
isConnected: stream.isConnected,
|
|
934
1100
|
partial: stream.partial,
|
|
1101
|
+
reconnect: stream.reconnect,
|
|
935
1102
|
sessionId: stream.sessionId,
|
|
936
1103
|
scenarioId: stream.scenarioId,
|
|
937
1104
|
status: stream.status,
|
|
@@ -956,7 +1123,13 @@ var createVoiceController = (path, options = {}) => {
|
|
|
956
1123
|
capture = createMicrophoneCapture({
|
|
957
1124
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
958
1125
|
onLevel: options.capture?.onLevel,
|
|
959
|
-
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
|
+
},
|
|
960
1133
|
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
|
|
961
1134
|
});
|
|
962
1135
|
return capture;
|
|
@@ -1006,6 +1179,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1006
1179
|
bindHTMX(bindingOptions) {
|
|
1007
1180
|
return bindVoiceHTMX(stream, bindingOptions);
|
|
1008
1181
|
},
|
|
1182
|
+
callControl: (message) => stream.callControl(message),
|
|
1009
1183
|
close,
|
|
1010
1184
|
endTurn: () => stream.endTurn(),
|
|
1011
1185
|
get error() {
|
|
@@ -1025,6 +1199,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1025
1199
|
get recordingError() {
|
|
1026
1200
|
return state.recordingError;
|
|
1027
1201
|
},
|
|
1202
|
+
get reconnect() {
|
|
1203
|
+
return state.reconnect;
|
|
1204
|
+
},
|
|
1028
1205
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1029
1206
|
get sessionId() {
|
|
1030
1207
|
return state.sessionId;
|
|
@@ -1058,6 +1235,478 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1058
1235
|
},
|
|
1059
1236
|
get assistantAudio() {
|
|
1060
1237
|
return state.assistantAudio;
|
|
1238
|
+
},
|
|
1239
|
+
get call() {
|
|
1240
|
+
return state.call;
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
};
|
|
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);
|
|
1061
1710
|
}
|
|
1062
1711
|
};
|
|
1063
1712
|
};
|
|
@@ -1118,6 +1767,17 @@ var formatErrorMessage = (error) => {
|
|
|
1118
1767
|
}
|
|
1119
1768
|
return "Unexpected error";
|
|
1120
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
|
+
};
|
|
1121
1781
|
var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
|
|
1122
1782
|
var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
|
|
1123
1783
|
const next = levels.slice(-(count - 1));
|
|
@@ -1174,6 +1834,17 @@ var parsePromptList = (value) => {
|
|
|
1174
1834
|
} catch {}
|
|
1175
1835
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1176
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
|
+
};
|
|
1177
1848
|
var requireElement = (root, selector, ctor, name) => {
|
|
1178
1849
|
const value = selector ? document.querySelector(selector) : null;
|
|
1179
1850
|
if (value instanceof ctor) {
|
|
@@ -1224,11 +1895,20 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1224
1895
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1225
1896
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1226
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;
|
|
1227
1906
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1228
1907
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1229
1908
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
1230
1909
|
const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
|
|
1231
1910
|
const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
|
|
1911
|
+
const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
|
|
1232
1912
|
const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
|
|
1233
1913
|
const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
|
|
1234
1914
|
const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
|
|
@@ -1237,35 +1917,70 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1237
1917
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1238
1918
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1239
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;
|
|
1240
1930
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1241
1931
|
capture: {
|
|
1932
|
+
onAudio: (audio, sendAudio) => {
|
|
1933
|
+
if (guidedBargeInBinding) {
|
|
1934
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
sendAudio(audio);
|
|
1938
|
+
},
|
|
1242
1939
|
onLevel: (level) => {
|
|
1940
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1243
1941
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1244
1942
|
renderWave();
|
|
1245
1943
|
}
|
|
1246
1944
|
},
|
|
1945
|
+
connection: {
|
|
1946
|
+
reconnectReportPath
|
|
1947
|
+
},
|
|
1247
1948
|
preset: "guided-intake"
|
|
1248
1949
|
});
|
|
1249
1950
|
const generalVoice = createVoiceController(generalPath, {
|
|
1250
1951
|
capture: {
|
|
1952
|
+
onAudio: (audio, sendAudio) => {
|
|
1953
|
+
if (generalBargeInBinding) {
|
|
1954
|
+
generalBargeInBinding.sendAudio(audio);
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
sendAudio(audio);
|
|
1958
|
+
},
|
|
1251
1959
|
onLevel: (level) => {
|
|
1960
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1252
1961
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1253
1962
|
renderWave();
|
|
1254
1963
|
}
|
|
1255
1964
|
},
|
|
1965
|
+
connection: {
|
|
1966
|
+
reconnectReportPath
|
|
1967
|
+
},
|
|
1256
1968
|
preset: "dictation"
|
|
1257
1969
|
});
|
|
1258
1970
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1259
1971
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
+
});
|
|
1268
1982
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
1983
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1269
1984
|
const renderWave = () => {
|
|
1270
1985
|
const path = createVoiceWavePath(waveLevels);
|
|
1271
1986
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1280,6 +1995,9 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1280
1995
|
const status = voice.status;
|
|
1281
1996
|
connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
|
|
1282
1997
|
errorStatus.textContent = micError || voice.error || "None";
|
|
1998
|
+
if (reconnectStatus) {
|
|
1999
|
+
reconnectStatus.textContent = formatReconnectState(voice.reconnect);
|
|
2000
|
+
}
|
|
1283
2001
|
microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
|
|
1284
2002
|
promptStatus.textContent = resolvePromptMessage({
|
|
1285
2003
|
guidedPrompts,
|
|
@@ -1343,8 +2061,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1343
2061
|
render();
|
|
1344
2062
|
}
|
|
1345
2063
|
};
|
|
1346
|
-
guidedVoice.subscribe(
|
|
1347
|
-
|
|
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
|
+
});
|
|
1348
2076
|
startGuidedButton.addEventListener("click", () => {
|
|
1349
2077
|
startMode("guided");
|
|
1350
2078
|
});
|
|
@@ -1357,6 +2085,10 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1357
2085
|
window.addEventListener("beforeunload", () => {
|
|
1358
2086
|
guidedVoice.stopRecording();
|
|
1359
2087
|
generalVoice.stopRecording();
|
|
2088
|
+
guidedBargeInBinding?.close();
|
|
2089
|
+
generalBargeInBinding?.close();
|
|
2090
|
+
guidedAudioPlayer.close();
|
|
2091
|
+
generalAudioPlayer.close();
|
|
1360
2092
|
stopGuidedBinding();
|
|
1361
2093
|
stopGeneralBinding();
|
|
1362
2094
|
guidedVoice.close();
|