@absolutejs/voice 0.0.22-beta.13 → 0.0.22-beta.130
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 +779 -5
- package/dist/agent.d.ts +24 -0
- package/dist/agentSquadContract.d.ts +64 -0
- package/dist/angular/index.d.ts +9 -0
- package/dist/angular/index.js +1394 -46
- 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-ops-status.component.d.ts +15 -0
- package/dist/angular/voice-ops-status.service.d.ts +12 -0
- package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
- package/dist/angular/voice-provider-status.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/assistantHealth.d.ts +81 -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 +133 -0
- package/dist/bargeInRoutes.d.ts +56 -0
- package/dist/campaign.d.ts +610 -0
- package/dist/campaignDialers.d.ts +90 -0
- package/dist/client/actions.d.ts +105 -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/duplex.d.ts +1 -1
- package/dist/client/htmxBootstrap.js +697 -15
- package/dist/client/index.d.ts +40 -0
- package/dist/client/index.js +2138 -10
- package/dist/client/liveTurnLatency.d.ts +41 -0
- package/dist/client/opsStatus.d.ts +19 -0
- package/dist/client/opsStatusWidget.d.ts +40 -0
- package/dist/client/providerCapabilities.d.ts +19 -0
- package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
- package/dist/client/providerSimulationControls.d.ts +33 -0
- package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
- package/dist/client/providerStatus.d.ts +19 -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 +32 -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 +47 -0
- package/dist/demoReadyRoutes.d.ts +98 -0
- package/dist/diagnosticsRoutes.d.ts +44 -0
- package/dist/evalRoutes.d.ts +213 -0
- package/dist/fileStore.d.ts +11 -2
- package/dist/handoff.d.ts +54 -0
- package/dist/handoffHealth.d.ts +94 -0
- package/dist/index.d.ts +103 -9
- package/dist/index.js +17158 -4684
- package/dist/liveLatency.d.ts +78 -0
- package/dist/modelAdapters.d.ts +26 -2
- package/dist/openaiRealtime.d.ts +27 -0
- package/dist/openaiTTS.d.ts +18 -0
- package/dist/opsConsoleRoutes.d.ts +77 -0
- package/dist/opsStatus.d.ts +65 -0
- package/dist/opsStatusRoutes.d.ts +33 -0
- package/dist/opsWebhook.d.ts +126 -0
- package/dist/outcomeContract.d.ts +112 -0
- package/dist/phoneAgent.d.ts +62 -0
- package/dist/phoneAgentProductionSmoke.d.ts +115 -0
- package/dist/postgresStore.d.ts +13 -2
- package/dist/productionReadiness.d.ts +227 -0
- package/dist/providerAdapters.d.ts +48 -0
- package/dist/providerCapabilities.d.ts +92 -0
- package/dist/providerHealth.d.ts +79 -0
- package/dist/providerRoutingContract.d.ts +38 -0
- package/dist/qualityRoutes.d.ts +76 -0
- package/dist/queue.d.ts +61 -0
- package/dist/react/VoiceOpsStatus.d.ts +6 -0
- package/dist/react/VoiceProviderCapabilities.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 +18 -0
- package/dist/react/index.js +2726 -14
- package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
- package/dist/react/useVoiceController.d.ts +3 -0
- package/dist/react/useVoiceOpsStatus.d.ts +8 -0
- package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
- package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
- package/dist/react/useVoiceProviderStatus.d.ts +8 -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/resilienceRoutes.d.ts +142 -0
- package/dist/sessionReplay.d.ts +175 -0
- package/dist/simulationSuite.d.ts +120 -0
- package/dist/sqliteStore.d.ts +13 -2
- package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
- package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
- package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
- package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
- package/dist/svelte/createVoiceProviderStatus.d.ts +10 -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 +10 -0
- package/dist/svelte/index.js +2152 -202
- 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 +2 -0
- package/dist/testing/index.js +2830 -37
- package/dist/testing/ioProviderSimulator.d.ts +41 -0
- package/dist/testing/providerSimulator.d.ts +44 -0
- package/dist/toolContract.d.ts +130 -0
- package/dist/toolRuntime.d.ts +50 -0
- package/dist/trace.d.ts +1 -1
- package/dist/traceDeliveryRoutes.d.ts +86 -0
- package/dist/traceTimeline.d.ts +93 -0
- package/dist/turnLatency.d.ts +95 -0
- package/dist/turnQuality.d.ts +94 -0
- package/dist/types.d.ts +169 -4
- package/dist/vue/VoiceOpsStatus.d.ts +30 -0
- package/dist/vue/VoiceProviderCapabilities.d.ts +51 -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 +17 -0
- package/dist/vue/index.js +2636 -31
- package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
- package/dist/vue/useVoiceController.d.ts +2 -1
- package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
- package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
- package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
- package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
- 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,
|
|
@@ -562,6 +692,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
562
692
|
}
|
|
563
693
|
});
|
|
564
694
|
return {
|
|
695
|
+
callControl(message) {
|
|
696
|
+
connection.callControl(message);
|
|
697
|
+
},
|
|
565
698
|
close() {
|
|
566
699
|
unsubscribeConnection();
|
|
567
700
|
connection.close();
|
|
@@ -590,6 +723,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
590
723
|
get partial() {
|
|
591
724
|
return store.getSnapshot().partial;
|
|
592
725
|
},
|
|
726
|
+
get reconnect() {
|
|
727
|
+
return store.getSnapshot().reconnect;
|
|
728
|
+
},
|
|
593
729
|
get sessionId() {
|
|
594
730
|
return connection.getSessionId();
|
|
595
731
|
},
|
|
@@ -605,6 +741,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
605
741
|
get assistantAudio() {
|
|
606
742
|
return store.getSnapshot().assistantAudio;
|
|
607
743
|
},
|
|
744
|
+
get call() {
|
|
745
|
+
return store.getSnapshot().call;
|
|
746
|
+
},
|
|
608
747
|
sendAudio(audio) {
|
|
609
748
|
connection.sendAudio(audio);
|
|
610
749
|
},
|
|
@@ -900,10 +1039,12 @@ var resolveVoiceRuntimePreset = (name = "default") => {
|
|
|
900
1039
|
var createInitialState2 = (stream) => ({
|
|
901
1040
|
assistantAudio: [...stream.assistantAudio],
|
|
902
1041
|
assistantTexts: [...stream.assistantTexts],
|
|
1042
|
+
call: stream.call,
|
|
903
1043
|
error: stream.error,
|
|
904
1044
|
isConnected: stream.isConnected,
|
|
905
1045
|
isRecording: false,
|
|
906
1046
|
partial: stream.partial,
|
|
1047
|
+
reconnect: stream.reconnect,
|
|
907
1048
|
recordingError: null,
|
|
908
1049
|
sessionId: stream.sessionId,
|
|
909
1050
|
scenarioId: stream.scenarioId,
|
|
@@ -929,9 +1070,11 @@ var createVoiceController = (path, options = {}) => {
|
|
|
929
1070
|
...state,
|
|
930
1071
|
assistantAudio: [...stream.assistantAudio],
|
|
931
1072
|
assistantTexts: [...stream.assistantTexts],
|
|
1073
|
+
call: stream.call,
|
|
932
1074
|
error: stream.error,
|
|
933
1075
|
isConnected: stream.isConnected,
|
|
934
1076
|
partial: stream.partial,
|
|
1077
|
+
reconnect: stream.reconnect,
|
|
935
1078
|
sessionId: stream.sessionId,
|
|
936
1079
|
scenarioId: stream.scenarioId,
|
|
937
1080
|
status: stream.status,
|
|
@@ -956,7 +1099,13 @@ var createVoiceController = (path, options = {}) => {
|
|
|
956
1099
|
capture = createMicrophoneCapture({
|
|
957
1100
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
958
1101
|
onLevel: options.capture?.onLevel,
|
|
959
|
-
onAudio: (audio) =>
|
|
1102
|
+
onAudio: (audio) => {
|
|
1103
|
+
if (options.capture?.onAudio) {
|
|
1104
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
stream.sendAudio(audio);
|
|
1108
|
+
},
|
|
960
1109
|
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
|
|
961
1110
|
});
|
|
962
1111
|
return capture;
|
|
@@ -1006,6 +1155,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1006
1155
|
bindHTMX(bindingOptions) {
|
|
1007
1156
|
return bindVoiceHTMX(stream, bindingOptions);
|
|
1008
1157
|
},
|
|
1158
|
+
callControl: (message) => stream.callControl(message),
|
|
1009
1159
|
close,
|
|
1010
1160
|
endTurn: () => stream.endTurn(),
|
|
1011
1161
|
get error() {
|
|
@@ -1025,6 +1175,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1025
1175
|
get recordingError() {
|
|
1026
1176
|
return state.recordingError;
|
|
1027
1177
|
},
|
|
1178
|
+
get reconnect() {
|
|
1179
|
+
return state.reconnect;
|
|
1180
|
+
},
|
|
1028
1181
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
1029
1182
|
get sessionId() {
|
|
1030
1183
|
return state.sessionId;
|
|
@@ -1058,6 +1211,478 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1058
1211
|
},
|
|
1059
1212
|
get assistantAudio() {
|
|
1060
1213
|
return state.assistantAudio;
|
|
1214
|
+
},
|
|
1215
|
+
get call() {
|
|
1216
|
+
return state.call;
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// src/client/audioPlayer.ts
|
|
1222
|
+
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1223
|
+
var createInitialState3 = () => ({
|
|
1224
|
+
activeSourceCount: 0,
|
|
1225
|
+
error: null,
|
|
1226
|
+
isActive: false,
|
|
1227
|
+
isPlaying: false,
|
|
1228
|
+
lastInterruptLatencyMs: undefined,
|
|
1229
|
+
lastPlaybackStopLatencyMs: undefined,
|
|
1230
|
+
processedChunkCount: 0,
|
|
1231
|
+
queuedChunkCount: 0
|
|
1232
|
+
});
|
|
1233
|
+
var getAudioContextCtor = () => {
|
|
1234
|
+
if (typeof window === "undefined") {
|
|
1235
|
+
return typeof AudioContext === "undefined" ? undefined : AudioContext;
|
|
1236
|
+
}
|
|
1237
|
+
return window.AudioContext ?? window.webkitAudioContext;
|
|
1238
|
+
};
|
|
1239
|
+
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
1240
|
+
const format = chunk.format;
|
|
1241
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
1242
|
+
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
1243
|
+
}
|
|
1244
|
+
const bytes = chunk.chunk;
|
|
1245
|
+
const channels = Math.max(1, format.channels);
|
|
1246
|
+
const sampleCount = Math.floor(bytes.byteLength / 2);
|
|
1247
|
+
const frameCount = Math.max(1, Math.floor(sampleCount / channels));
|
|
1248
|
+
const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
|
|
1249
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1250
|
+
for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
|
|
1251
|
+
const channelData = audioBuffer.getChannelData(channelIndex);
|
|
1252
|
+
for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
|
|
1253
|
+
const sampleIndex = frameIndex * channels + channelIndex;
|
|
1254
|
+
const sampleOffset = sampleIndex * 2;
|
|
1255
|
+
if (sampleOffset + 1 >= bytes.byteLength) {
|
|
1256
|
+
channelData[frameIndex] = 0;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return audioBuffer;
|
|
1263
|
+
};
|
|
1264
|
+
var createVoiceAudioPlayer = (source, options = {}) => {
|
|
1265
|
+
const subscribers = new Set;
|
|
1266
|
+
const sourceNodes = new Set;
|
|
1267
|
+
const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
|
|
1268
|
+
let state = createInitialState3();
|
|
1269
|
+
let audioContext = null;
|
|
1270
|
+
let outputNode = null;
|
|
1271
|
+
let queueEndTime = 0;
|
|
1272
|
+
let syncPromise = Promise.resolve();
|
|
1273
|
+
let interruptStartedAt = null;
|
|
1274
|
+
let interruptPromise = null;
|
|
1275
|
+
let resolveInterruptPromise = null;
|
|
1276
|
+
let interruptFallbackTimer = null;
|
|
1277
|
+
const notify = () => {
|
|
1278
|
+
for (const subscriber of subscribers) {
|
|
1279
|
+
subscriber();
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
const setState = (next) => {
|
|
1283
|
+
state = {
|
|
1284
|
+
...state,
|
|
1285
|
+
...next
|
|
1286
|
+
};
|
|
1287
|
+
notify();
|
|
1288
|
+
};
|
|
1289
|
+
const clearError = () => {
|
|
1290
|
+
if (state.error !== null) {
|
|
1291
|
+
setState({ error: null });
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
const clearInterruptTimer = () => {
|
|
1295
|
+
if (interruptFallbackTimer !== null) {
|
|
1296
|
+
clearTimeout(interruptFallbackTimer);
|
|
1297
|
+
interruptFallbackTimer = null;
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
const resolveInterrupt = (latencyMs) => {
|
|
1301
|
+
clearInterruptTimer();
|
|
1302
|
+
interruptStartedAt = null;
|
|
1303
|
+
setState({
|
|
1304
|
+
activeSourceCount: sourceNodes.size,
|
|
1305
|
+
isPlaying: false,
|
|
1306
|
+
lastInterruptLatencyMs: latencyMs,
|
|
1307
|
+
lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
|
|
1308
|
+
});
|
|
1309
|
+
resolveInterruptPromise?.();
|
|
1310
|
+
resolveInterruptPromise = null;
|
|
1311
|
+
interruptPromise = null;
|
|
1312
|
+
};
|
|
1313
|
+
const estimateOutputStopLatencyMs = (context) => {
|
|
1314
|
+
if (!context) {
|
|
1315
|
+
return 0;
|
|
1316
|
+
}
|
|
1317
|
+
return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
|
|
1318
|
+
};
|
|
1319
|
+
const restoreOutputGain = (context) => {
|
|
1320
|
+
if (!outputNode) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const gainValue = 1;
|
|
1324
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1325
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
outputNode.gain.value = gainValue;
|
|
1329
|
+
};
|
|
1330
|
+
const muteOutputGain = (context) => {
|
|
1331
|
+
if (!outputNode) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const gainValue = 0;
|
|
1335
|
+
if (outputNode.gain.setValueAtTime) {
|
|
1336
|
+
outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
outputNode.gain.value = gainValue;
|
|
1340
|
+
};
|
|
1341
|
+
const maybeResolveInterrupt = () => {
|
|
1342
|
+
if (interruptStartedAt === null || sourceNodes.size > 0) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
resolveInterrupt(Date.now() - interruptStartedAt);
|
|
1346
|
+
};
|
|
1347
|
+
const ensureAudioContext = async () => {
|
|
1348
|
+
if (audioContext) {
|
|
1349
|
+
return audioContext;
|
|
1350
|
+
}
|
|
1351
|
+
if (options.createAudioContext) {
|
|
1352
|
+
audioContext = options.createAudioContext();
|
|
1353
|
+
} else {
|
|
1354
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
1355
|
+
if (!AudioContextCtor) {
|
|
1356
|
+
throw new Error("Assistant audio playback requires AudioContext support.");
|
|
1357
|
+
}
|
|
1358
|
+
audioContext = new AudioContextCtor;
|
|
1359
|
+
}
|
|
1360
|
+
if (audioContext.createGain) {
|
|
1361
|
+
outputNode = audioContext.createGain();
|
|
1362
|
+
outputNode.connect?.(audioContext.destination);
|
|
1363
|
+
}
|
|
1364
|
+
queueEndTime = audioContext.currentTime;
|
|
1365
|
+
return audioContext;
|
|
1366
|
+
};
|
|
1367
|
+
const scheduleChunk = async (chunk) => {
|
|
1368
|
+
const context = await ensureAudioContext();
|
|
1369
|
+
const buffer = decodePCM16LEChunk(context, chunk);
|
|
1370
|
+
const node = context.createBufferSource();
|
|
1371
|
+
node.buffer = buffer;
|
|
1372
|
+
node.connect(outputNode ?? context.destination);
|
|
1373
|
+
node.onended = () => {
|
|
1374
|
+
sourceNodes.delete(node);
|
|
1375
|
+
node.disconnect?.();
|
|
1376
|
+
setState({
|
|
1377
|
+
activeSourceCount: sourceNodes.size,
|
|
1378
|
+
isPlaying: sourceNodes.size > 0 && state.isActive
|
|
1379
|
+
});
|
|
1380
|
+
maybeResolveInterrupt();
|
|
1381
|
+
};
|
|
1382
|
+
const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
|
|
1383
|
+
queueEndTime = startAt + buffer.duration;
|
|
1384
|
+
sourceNodes.add(node);
|
|
1385
|
+
setState({
|
|
1386
|
+
activeSourceCount: sourceNodes.size,
|
|
1387
|
+
isPlaying: true
|
|
1388
|
+
});
|
|
1389
|
+
node.start(startAt);
|
|
1390
|
+
};
|
|
1391
|
+
const stopQueuedPlayback = (options2) => {
|
|
1392
|
+
for (const node of [...sourceNodes]) {
|
|
1393
|
+
node.stop?.();
|
|
1394
|
+
}
|
|
1395
|
+
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
1396
|
+
if (options2?.forceClear) {
|
|
1397
|
+
for (const node of sourceNodes) {
|
|
1398
|
+
node.disconnect?.();
|
|
1399
|
+
}
|
|
1400
|
+
sourceNodes.clear();
|
|
1401
|
+
maybeResolveInterrupt();
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
const sync = async () => {
|
|
1405
|
+
if (!state.isActive) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
|
|
1409
|
+
if (nextChunks.length === 0) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
try {
|
|
1413
|
+
clearError();
|
|
1414
|
+
for (const chunk of nextChunks) {
|
|
1415
|
+
await scheduleChunk(chunk);
|
|
1416
|
+
}
|
|
1417
|
+
setState({
|
|
1418
|
+
processedChunkCount: source.assistantAudio.length,
|
|
1419
|
+
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
1420
|
+
});
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
setState({
|
|
1423
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
const queueSync = () => {
|
|
1428
|
+
syncPromise = syncPromise.then(() => sync(), () => sync());
|
|
1429
|
+
return syncPromise;
|
|
1430
|
+
};
|
|
1431
|
+
const unsubscribeSource = source.subscribe(() => {
|
|
1432
|
+
if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
|
|
1433
|
+
player.start();
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (state.isActive) {
|
|
1437
|
+
queueSync();
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
const player = {
|
|
1441
|
+
close: async () => {
|
|
1442
|
+
unsubscribeSource();
|
|
1443
|
+
stopQueuedPlayback({ forceClear: true });
|
|
1444
|
+
clearInterruptTimer();
|
|
1445
|
+
resolveInterruptPromise?.();
|
|
1446
|
+
resolveInterruptPromise = null;
|
|
1447
|
+
interruptPromise = null;
|
|
1448
|
+
interruptStartedAt = null;
|
|
1449
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
1450
|
+
await audioContext.close();
|
|
1451
|
+
}
|
|
1452
|
+
audioContext = null;
|
|
1453
|
+
outputNode?.disconnect?.();
|
|
1454
|
+
outputNode = null;
|
|
1455
|
+
queueEndTime = 0;
|
|
1456
|
+
setState({
|
|
1457
|
+
activeSourceCount: 0,
|
|
1458
|
+
isActive: false,
|
|
1459
|
+
isPlaying: false
|
|
1460
|
+
});
|
|
1461
|
+
},
|
|
1462
|
+
get activeSourceCount() {
|
|
1463
|
+
return state.activeSourceCount;
|
|
1464
|
+
},
|
|
1465
|
+
get error() {
|
|
1466
|
+
return state.error;
|
|
1467
|
+
},
|
|
1468
|
+
getSnapshot: () => state,
|
|
1469
|
+
get isActive() {
|
|
1470
|
+
return state.isActive;
|
|
1471
|
+
},
|
|
1472
|
+
get isPlaying() {
|
|
1473
|
+
return state.isPlaying;
|
|
1474
|
+
},
|
|
1475
|
+
interrupt: async () => {
|
|
1476
|
+
const startedAt = Date.now();
|
|
1477
|
+
const context = await ensureAudioContext();
|
|
1478
|
+
interruptStartedAt = startedAt;
|
|
1479
|
+
muteOutputGain(context);
|
|
1480
|
+
const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
|
|
1481
|
+
setState({
|
|
1482
|
+
isActive: false,
|
|
1483
|
+
isPlaying: sourceNodes.size > 0,
|
|
1484
|
+
lastPlaybackStopLatencyMs: playbackStopLatencyMs
|
|
1485
|
+
});
|
|
1486
|
+
if (sourceNodes.size === 0) {
|
|
1487
|
+
resolveInterrupt(playbackStopLatencyMs);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (!interruptPromise) {
|
|
1491
|
+
interruptPromise = new Promise((resolve) => {
|
|
1492
|
+
resolveInterruptPromise = resolve;
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
clearInterruptTimer();
|
|
1496
|
+
interruptFallbackTimer = setTimeout(() => {
|
|
1497
|
+
for (const node of sourceNodes) {
|
|
1498
|
+
node.disconnect?.();
|
|
1499
|
+
}
|
|
1500
|
+
sourceNodes.clear();
|
|
1501
|
+
resolveInterrupt(Date.now() - startedAt);
|
|
1502
|
+
}, 250);
|
|
1503
|
+
stopQueuedPlayback();
|
|
1504
|
+
await interruptPromise;
|
|
1505
|
+
},
|
|
1506
|
+
get lastInterruptLatencyMs() {
|
|
1507
|
+
return state.lastInterruptLatencyMs;
|
|
1508
|
+
},
|
|
1509
|
+
get lastPlaybackStopLatencyMs() {
|
|
1510
|
+
return state.lastPlaybackStopLatencyMs;
|
|
1511
|
+
},
|
|
1512
|
+
pause: async () => {
|
|
1513
|
+
if (!audioContext) {
|
|
1514
|
+
setState({
|
|
1515
|
+
activeSourceCount: 0,
|
|
1516
|
+
isActive: false,
|
|
1517
|
+
isPlaying: false
|
|
1518
|
+
});
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
await audioContext.suspend();
|
|
1522
|
+
setState({
|
|
1523
|
+
activeSourceCount: sourceNodes.size,
|
|
1524
|
+
isActive: false,
|
|
1525
|
+
isPlaying: false
|
|
1526
|
+
});
|
|
1527
|
+
},
|
|
1528
|
+
get processedChunkCount() {
|
|
1529
|
+
return state.processedChunkCount;
|
|
1530
|
+
},
|
|
1531
|
+
get queuedChunkCount() {
|
|
1532
|
+
return state.queuedChunkCount;
|
|
1533
|
+
},
|
|
1534
|
+
start: async () => {
|
|
1535
|
+
try {
|
|
1536
|
+
clearError();
|
|
1537
|
+
const context = await ensureAudioContext();
|
|
1538
|
+
restoreOutputGain(context);
|
|
1539
|
+
if (context.state === "suspended") {
|
|
1540
|
+
await context.resume();
|
|
1541
|
+
}
|
|
1542
|
+
setState({
|
|
1543
|
+
activeSourceCount: sourceNodes.size,
|
|
1544
|
+
isActive: true,
|
|
1545
|
+
isPlaying: context.state === "running"
|
|
1546
|
+
});
|
|
1547
|
+
await queueSync();
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
setState({
|
|
1550
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1551
|
+
isActive: false,
|
|
1552
|
+
isPlaying: false
|
|
1553
|
+
});
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
subscribe: (subscriber) => {
|
|
1558
|
+
subscribers.add(subscriber);
|
|
1559
|
+
return () => {
|
|
1560
|
+
subscribers.delete(subscriber);
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
return player;
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
// src/client/bargeInMonitor.ts
|
|
1568
|
+
var DEFAULT_THRESHOLD_MS = 250;
|
|
1569
|
+
var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
1570
|
+
var summarize = (events, thresholdMs) => {
|
|
1571
|
+
const stopped = events.filter((event) => event.status === "stopped");
|
|
1572
|
+
const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
|
|
1573
|
+
const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
|
|
1574
|
+
const passed = stopped.length - failed;
|
|
1575
|
+
return {
|
|
1576
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
1577
|
+
events: [...events],
|
|
1578
|
+
failed,
|
|
1579
|
+
lastEvent: events.at(-1),
|
|
1580
|
+
passed,
|
|
1581
|
+
status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
|
|
1582
|
+
thresholdMs,
|
|
1583
|
+
total: stopped.length
|
|
1584
|
+
};
|
|
1585
|
+
};
|
|
1586
|
+
var createVoiceBargeInMonitor = (options = {}) => {
|
|
1587
|
+
const listeners = new Set;
|
|
1588
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
1589
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1590
|
+
const events = [];
|
|
1591
|
+
const emit = () => {
|
|
1592
|
+
for (const listener of listeners) {
|
|
1593
|
+
listener();
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
const postEvent = (event) => {
|
|
1597
|
+
if (!options.path || typeof fetchImpl !== "function") {
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
fetchImpl(options.path, {
|
|
1601
|
+
body: JSON.stringify(event),
|
|
1602
|
+
headers: {
|
|
1603
|
+
"Content-Type": "application/json"
|
|
1604
|
+
},
|
|
1605
|
+
method: "POST"
|
|
1606
|
+
}).catch(() => {});
|
|
1607
|
+
};
|
|
1608
|
+
const record = (status, input) => {
|
|
1609
|
+
const event = {
|
|
1610
|
+
at: Date.now(),
|
|
1611
|
+
id: createEventId(),
|
|
1612
|
+
latencyMs: input.latencyMs,
|
|
1613
|
+
playbackStopLatencyMs: input.playbackStopLatencyMs,
|
|
1614
|
+
reason: input.reason,
|
|
1615
|
+
sessionId: input.sessionId,
|
|
1616
|
+
status,
|
|
1617
|
+
thresholdMs
|
|
1618
|
+
};
|
|
1619
|
+
events.push(event);
|
|
1620
|
+
postEvent(event);
|
|
1621
|
+
emit();
|
|
1622
|
+
return event;
|
|
1623
|
+
};
|
|
1624
|
+
return {
|
|
1625
|
+
getSnapshot: () => summarize(events, thresholdMs),
|
|
1626
|
+
recordRequested: (input) => record("requested", input),
|
|
1627
|
+
recordSkipped: (input) => record("skipped", input),
|
|
1628
|
+
recordStopped: (input) => record("stopped", input),
|
|
1629
|
+
subscribe: (subscriber) => {
|
|
1630
|
+
listeners.add(subscriber);
|
|
1631
|
+
return () => {
|
|
1632
|
+
listeners.delete(subscriber);
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
// src/client/duplex.ts
|
|
1639
|
+
var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
1640
|
+
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
1641
|
+
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
1642
|
+
let lastPartial = controller.partial;
|
|
1643
|
+
const interruptIfPlaying = (reason) => {
|
|
1644
|
+
if (!player.isPlaying || options.enabled === false) {
|
|
1645
|
+
options.monitor?.recordSkipped({
|
|
1646
|
+
reason,
|
|
1647
|
+
sessionId: controller.sessionId
|
|
1648
|
+
});
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
options.monitor?.recordRequested({
|
|
1652
|
+
reason,
|
|
1653
|
+
sessionId: controller.sessionId
|
|
1654
|
+
});
|
|
1655
|
+
player.interrupt().then(() => {
|
|
1656
|
+
options.monitor?.recordStopped({
|
|
1657
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
1658
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
1659
|
+
reason,
|
|
1660
|
+
sessionId: controller.sessionId
|
|
1661
|
+
});
|
|
1662
|
+
});
|
|
1663
|
+
};
|
|
1664
|
+
const unsubscribe = controller.subscribe(() => {
|
|
1665
|
+
if (options.interruptOnPartial === false) {
|
|
1666
|
+
lastPartial = controller.partial;
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (!lastPartial && controller.partial) {
|
|
1670
|
+
interruptIfPlaying("partial-transcript");
|
|
1671
|
+
}
|
|
1672
|
+
lastPartial = controller.partial;
|
|
1673
|
+
});
|
|
1674
|
+
return {
|
|
1675
|
+
close: () => {
|
|
1676
|
+
unsubscribe();
|
|
1677
|
+
},
|
|
1678
|
+
handleLevel: (level) => {
|
|
1679
|
+
if (shouldInterruptForLevel(level, options)) {
|
|
1680
|
+
interruptIfPlaying("input-level");
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
sendAudio: (audio) => {
|
|
1684
|
+
interruptIfPlaying("manual-audio");
|
|
1685
|
+
controller.sendAudio(audio);
|
|
1061
1686
|
}
|
|
1062
1687
|
};
|
|
1063
1688
|
};
|
|
@@ -1174,6 +1799,13 @@ var parsePromptList = (value) => {
|
|
|
1174
1799
|
} catch {}
|
|
1175
1800
|
return DEFAULT_GUIDED_PROMPTS;
|
|
1176
1801
|
};
|
|
1802
|
+
var parseOptionalNumber = (value) => {
|
|
1803
|
+
if (!value) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const parsed = Number(value);
|
|
1807
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1808
|
+
};
|
|
1177
1809
|
var requireElement = (root, selector, ctor, name) => {
|
|
1178
1810
|
const value = selector ? document.querySelector(selector) : null;
|
|
1179
1811
|
if (value instanceof ctor) {
|
|
@@ -1224,6 +1856,13 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1224
1856
|
const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
|
|
1225
1857
|
const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
|
|
1226
1858
|
const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
|
|
1859
|
+
const bargeInPath = root.dataset.voiceBargeInPath;
|
|
1860
|
+
const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
|
|
1861
|
+
path: bargeInPath,
|
|
1862
|
+
thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
|
|
1863
|
+
}) : null;
|
|
1864
|
+
const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
|
|
1865
|
+
const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
|
|
1227
1866
|
const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
|
|
1228
1867
|
const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
|
|
1229
1868
|
const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
|
|
@@ -1237,9 +1876,27 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1237
1876
|
const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
|
|
1238
1877
|
const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
|
|
1239
1878
|
const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
|
|
1879
|
+
let activeMode = null;
|
|
1880
|
+
let hasStartedModes = {
|
|
1881
|
+
general: false,
|
|
1882
|
+
guided: false
|
|
1883
|
+
};
|
|
1884
|
+
let isCapturing = false;
|
|
1885
|
+
let micError = null;
|
|
1886
|
+
let waveLevels = createInitialVoiceWaveLevels();
|
|
1887
|
+
let guidedBargeInBinding = null;
|
|
1888
|
+
let generalBargeInBinding = null;
|
|
1240
1889
|
const guidedVoice = createVoiceController(guidedPath, {
|
|
1241
1890
|
capture: {
|
|
1891
|
+
onAudio: (audio, sendAudio) => {
|
|
1892
|
+
if (guidedBargeInBinding) {
|
|
1893
|
+
guidedBargeInBinding.sendAudio(audio);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
sendAudio(audio);
|
|
1897
|
+
},
|
|
1242
1898
|
onLevel: (level) => {
|
|
1899
|
+
guidedBargeInBinding?.handleLevel(level);
|
|
1243
1900
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1244
1901
|
renderWave();
|
|
1245
1902
|
}
|
|
@@ -1248,7 +1905,15 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1248
1905
|
});
|
|
1249
1906
|
const generalVoice = createVoiceController(generalPath, {
|
|
1250
1907
|
capture: {
|
|
1908
|
+
onAudio: (audio, sendAudio) => {
|
|
1909
|
+
if (generalBargeInBinding) {
|
|
1910
|
+
generalBargeInBinding.sendAudio(audio);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
sendAudio(audio);
|
|
1914
|
+
},
|
|
1251
1915
|
onLevel: (level) => {
|
|
1916
|
+
generalBargeInBinding?.handleLevel(level);
|
|
1252
1917
|
waveLevels = pushVoiceWaveLevel(waveLevels, level);
|
|
1253
1918
|
renderWave();
|
|
1254
1919
|
}
|
|
@@ -1257,15 +1922,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1257
1922
|
});
|
|
1258
1923
|
const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
|
|
1259
1924
|
const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1925
|
+
const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
|
|
1926
|
+
const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
|
|
1927
|
+
guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
|
|
1928
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
1929
|
+
monitor: bargeInMonitor ?? undefined
|
|
1930
|
+
});
|
|
1931
|
+
generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
|
|
1932
|
+
interruptThreshold: bargeInSpeechThreshold,
|
|
1933
|
+
monitor: bargeInMonitor ?? undefined
|
|
1934
|
+
});
|
|
1268
1935
|
const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
|
|
1936
|
+
const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
|
|
1269
1937
|
const renderWave = () => {
|
|
1270
1938
|
const path = createVoiceWavePath(waveLevels);
|
|
1271
1939
|
voiceWaveGlow.setAttribute("d", path);
|
|
@@ -1343,8 +2011,18 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1343
2011
|
render();
|
|
1344
2012
|
}
|
|
1345
2013
|
};
|
|
1346
|
-
guidedVoice.subscribe(
|
|
1347
|
-
|
|
2014
|
+
guidedVoice.subscribe(() => {
|
|
2015
|
+
if (guidedVoice.assistantAudio.length > 0) {
|
|
2016
|
+
guidedAudioPlayer.start().catch(() => {});
|
|
2017
|
+
}
|
|
2018
|
+
render();
|
|
2019
|
+
});
|
|
2020
|
+
generalVoice.subscribe(() => {
|
|
2021
|
+
if (generalVoice.assistantAudio.length > 0) {
|
|
2022
|
+
generalAudioPlayer.start().catch(() => {});
|
|
2023
|
+
}
|
|
2024
|
+
render();
|
|
2025
|
+
});
|
|
1348
2026
|
startGuidedButton.addEventListener("click", () => {
|
|
1349
2027
|
startMode("guided");
|
|
1350
2028
|
});
|
|
@@ -1357,6 +2035,10 @@ var initVoiceHTMXRoot = (root) => {
|
|
|
1357
2035
|
window.addEventListener("beforeunload", () => {
|
|
1358
2036
|
guidedVoice.stopRecording();
|
|
1359
2037
|
generalVoice.stopRecording();
|
|
2038
|
+
guidedBargeInBinding?.close();
|
|
2039
|
+
generalBargeInBinding?.close();
|
|
2040
|
+
guidedAudioPlayer.close();
|
|
2041
|
+
generalAudioPlayer.close();
|
|
1360
2042
|
stopGuidedBinding();
|
|
1361
2043
|
stopGeneralBinding();
|
|
1362
2044
|
guidedVoice.close();
|