@absolutejs/voice 0.0.22-beta.329 → 0.0.22-beta.330

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/dist/index.d.ts CHANGED
@@ -109,6 +109,7 @@ export { conditionAudioChunk, resolveAudioConditioningConfig } from './audioCond
109
109
  export { resolveVoiceRuntimePreset } from './presets';
110
110
  export { resolveTurnDetectionConfig, TURN_PROFILE_DEFAULTS } from './turnProfiles';
111
111
  export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewRecorder, renderVoiceCallReviewHTML, renderVoiceCallReviewMarkdown } from './testing/review';
112
+ export { getDefaultVoiceTelephonyBenchmarkScenarios, runVoiceTelephonyBenchmark, runVoiceTelephonyBenchmarkScenario, runVoiceTelephonyMediaOperationsSmoke, summarizeVoiceTelephonyBenchmark } from './testing/telephony';
112
113
  export type { VoiceCampaign, VoiceCampaignAttempt, VoiceCampaignAttemptResultInput, VoiceCampaignAttemptStatus, VoiceCampaignCreateInput, VoiceCampaignDialer, VoiceCampaignDialerInput, VoiceCampaignDialerResult, VoiceCampaignProofOptions, VoiceCampaignProofReport, VoiceCampaignReadinessAssertionInput, VoiceCampaignReadinessAssertionReport, VoiceCampaignReadinessCheck, VoiceCampaignReadinessProofOptions, VoiceCampaignReadinessProofReport, VoiceCampaignRecipient, VoiceCampaignRecipientImportIssue, VoiceCampaignRecipientImportIssueCode, VoiceCampaignRecipientImportOptions, VoiceCampaignRecipientImportResult, VoiceCampaignRecipientImportRow, VoiceCampaignRecipientInput, VoiceCampaignRecipientStatus, VoiceCampaignRecord, VoiceCampaignRoutesOptions, VoiceCampaignRuntime, VoiceCampaignRuntimeOptions, VoiceCampaignRateLimit, VoiceCampaignRetryPolicy, VoiceCampaignSchedule, VoiceCampaignStatus, VoiceCampaignStore, VoiceCampaignSummary, VoiceCampaignTimeWindow, VoiceCampaignTickResult } from './campaign';
113
114
  export type { VoiceCampaignDialerProofAssertionInput, VoiceCampaignDialerProofAssertionReport, VoiceCampaignDialerProofCarrierRequest, VoiceCampaignDialerProofOptions, VoiceCampaignDialerProofProvider, VoiceCampaignDialerProofProviderResult, VoiceCampaignDialerProofReport, VoiceCampaignDialerProofStatus, VoicePlivoCampaignDialerOptions, VoiceTelnyxCampaignDialerOptions, VoiceTwilioCampaignDialerOptions } from './campaignDialers';
114
115
  export type { VoiceBargeInReport, VoiceBargeInRoutesOptions } from './bargeInRoutes';
@@ -162,6 +163,7 @@ export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLin
162
163
  export type { VoiceHandoffDelivery, VoiceHandoffDeliveryRecord, VoiceHandoffDeliveryRecordInput, VoiceHandoffFanoutResult, VoiceQueuedHandoffDeliveryOptions, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
163
164
  export type { VoiceHandoffHealthDelivery, VoiceHandoffHealthEvent, VoiceHandoffHealthHTMLHandlerOptions, VoiceHandoffHealthRoutesOptions, VoiceHandoffHealthStatus, VoiceHandoffHealthSummary, VoiceHandoffHealthSummaryOptions } from './handoffHealth';
164
165
  export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
166
+ export type { VoiceTelephonyBenchmarkReport, VoiceTelephonyBenchmarkScenario, VoiceTelephonyBenchmarkScenarioResult, VoiceTelephonyBenchmarkSummary, VoiceTelephonyMediaOperationsSmokeOptions, VoiceTelephonyMediaOperationsSmokeReport } from './testing/telephony';
165
167
  export type { StoredVoiceAuditEvent, VoiceAuditActor, VoiceAuditEvent, VoiceAuditEventFilter, VoiceAuditEventStore, VoiceAuditEventType, VoiceAuditLogger, VoiceAuditOutcome, VoiceAuditResource, VoiceHandoffAuditEventInput, VoiceOperatorAuditEventInput, VoiceProviderAuditEventInput, VoiceRetentionAuditEventInput, VoiceToolAuditEventInput } from './audit';
166
168
  export type { VoiceAuditTrailOptions, VoiceAuditTrailReport, VoiceAuditTrailRoutesOptions, VoiceAuditTrailSummary } from './auditRoutes';
167
169
  export type { VoiceAuditExport } from './auditExport';
package/dist/index.js CHANGED
@@ -35510,6 +35510,367 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
35510
35510
  }
35511
35511
  return createPhraseHintCorrectionHandler();
35512
35512
  };
35513
+ // src/testing/telephony.ts
35514
+ var DEFAULT_PCM16_FORMAT = {
35515
+ channels: 1,
35516
+ container: "raw",
35517
+ encoding: "pcm_s16le",
35518
+ sampleRateHz: 16000
35519
+ };
35520
+ var DEFAULT_SCENARIOS = [
35521
+ {
35522
+ expectClear: false,
35523
+ expectMark: true,
35524
+ expectOutboundMedia: true,
35525
+ id: "telephony-turn",
35526
+ title: "Telephony bridge streams assistant audio and mark"
35527
+ },
35528
+ {
35529
+ expectClear: true,
35530
+ expectMark: true,
35531
+ expectOutboundMedia: true,
35532
+ id: "telephony-barge-in",
35533
+ secondInboundDelayMs: 5,
35534
+ title: "Telephony bridge clears queued outbound audio on barge-in"
35535
+ },
35536
+ {
35537
+ expectClear: true,
35538
+ expectMark: true,
35539
+ expectOutboundMedia: true,
35540
+ id: "telephony-streaming",
35541
+ secondInboundDelayMs: 8,
35542
+ title: "Telephony bridge keeps streaming chunks and still clears on re-entry",
35543
+ ttsChunkCount: 3,
35544
+ ttsChunkDelayMs: 4
35545
+ }
35546
+ ];
35547
+ var waitFor = async (check, timeoutMs, intervalMs = 5) => {
35548
+ const deadline = Date.now() + timeoutMs;
35549
+ while (Date.now() < deadline) {
35550
+ if (check()) {
35551
+ return true;
35552
+ }
35553
+ await Bun.sleep(intervalMs);
35554
+ }
35555
+ return check();
35556
+ };
35557
+ var toUint8Array2 = (audio) => audio instanceof Uint8Array ? audio : audio instanceof ArrayBuffer ? new Uint8Array(audio) : new Uint8Array(audio.buffer, audio.byteOffset, audio.byteLength);
35558
+ var createFakeSTTAdapter = (inputSpy, sttDelayMs) => ({
35559
+ kind: "stt",
35560
+ open: (_options) => {
35561
+ const listeners = {
35562
+ close: new Set,
35563
+ endOfTurn: new Set,
35564
+ error: new Set,
35565
+ final: new Set,
35566
+ partial: new Set
35567
+ };
35568
+ let delivered = false;
35569
+ return {
35570
+ close: async () => {
35571
+ for (const handler of listeners.close) {
35572
+ handler({ type: "close" });
35573
+ }
35574
+ },
35575
+ on: (event, handler) => {
35576
+ listeners[event].add(handler);
35577
+ return () => {
35578
+ listeners[event].delete(handler);
35579
+ };
35580
+ },
35581
+ send: async (audio) => {
35582
+ inputSpy.push(toUint8Array2(audio));
35583
+ if (delivered) {
35584
+ return;
35585
+ }
35586
+ delivered = true;
35587
+ if (sttDelayMs > 0) {
35588
+ await Bun.sleep(sttDelayMs);
35589
+ }
35590
+ const receivedAt = Date.now();
35591
+ for (const handler of listeners.final) {
35592
+ handler({
35593
+ receivedAt,
35594
+ transcript: {
35595
+ id: "telephony-benchmark-final",
35596
+ isFinal: true,
35597
+ text: "hello from twilio"
35598
+ },
35599
+ type: "final"
35600
+ });
35601
+ }
35602
+ for (const handler of listeners.endOfTurn) {
35603
+ handler({
35604
+ receivedAt,
35605
+ reason: "vendor",
35606
+ type: "endOfTurn"
35607
+ });
35608
+ }
35609
+ }
35610
+ };
35611
+ }
35612
+ });
35613
+ var createFakeTTSAdapter = (chunkCount, chunkDelayMs) => ({
35614
+ kind: "tts",
35615
+ open: () => {
35616
+ const listeners = {
35617
+ audio: new Set,
35618
+ close: new Set,
35619
+ error: new Set
35620
+ };
35621
+ return {
35622
+ close: async () => {
35623
+ for (const handler of listeners.close) {
35624
+ handler({ type: "close" });
35625
+ }
35626
+ },
35627
+ on: (event, handler) => {
35628
+ listeners[event].add(handler);
35629
+ return () => {
35630
+ listeners[event].delete(handler);
35631
+ };
35632
+ },
35633
+ send: async () => {
35634
+ for (let index = 0;index < chunkCount; index += 1) {
35635
+ if (chunkDelayMs > 0) {
35636
+ await Bun.sleep(chunkDelayMs);
35637
+ }
35638
+ const chunk = new Uint8Array(320);
35639
+ for (let byteIndex = 0;byteIndex < chunk.length; byteIndex += 2) {
35640
+ chunk[byteIndex] = 255;
35641
+ chunk[byteIndex + 1] = 31;
35642
+ }
35643
+ for (const handler of listeners.audio) {
35644
+ handler({
35645
+ chunk,
35646
+ format: DEFAULT_PCM16_FORMAT,
35647
+ receivedAt: Date.now(),
35648
+ type: "audio"
35649
+ });
35650
+ }
35651
+ }
35652
+ }
35653
+ };
35654
+ }
35655
+ });
35656
+ var defaultOperationsRecordHref = ({
35657
+ sessionId
35658
+ }) => `/voice-operations/${encodeURIComponent(sessionId)}`;
35659
+ var resolveOperationsRecordHref = (href, input) => typeof href === "function" ? href(input) : href === undefined ? defaultOperationsRecordHref(input) : href;
35660
+ var runVoiceTelephonyMediaOperationsSmoke = async (options = {}) => {
35661
+ const timeoutMs = options.timeoutMs ?? 5000;
35662
+ const sessionId = options.sessionId ?? `telephony-media-ops-${Date.now()}`;
35663
+ const streamSid = options.streamSid ?? "telephony-media-ops-stream";
35664
+ const callSid = options.callSid ?? "telephony-media-ops-call";
35665
+ const scenarioId = options.scenarioId ?? "telephony-media-operations-smoke";
35666
+ const trace = options.store ?? createVoiceMemoryTraceEventStore();
35667
+ const sentEvents = [];
35668
+ const receivedAudio = [];
35669
+ const bridge = createTwilioMediaStreamBridge({
35670
+ close: () => {},
35671
+ send: (data) => {
35672
+ sentEvents.push(JSON.parse(data));
35673
+ }
35674
+ }, {
35675
+ context: {},
35676
+ onComplete: async () => {},
35677
+ onTurn: async () => ({
35678
+ assistantText: "Confirmed. Two way carrier media is visible."
35679
+ }),
35680
+ session: createVoiceMemoryStore(),
35681
+ stt: createFakeSTTAdapter(receivedAudio, 0),
35682
+ trace,
35683
+ tts: createFakeTTSAdapter(1, 0),
35684
+ turnDetection: {
35685
+ transcriptStabilityMs: 0
35686
+ }
35687
+ });
35688
+ const payload = encodeTwilioMulawBase64(new Int16Array([500, -500, 1500, -1500, 2500, -2500]));
35689
+ await bridge.handleMessage({
35690
+ event: "start",
35691
+ start: {
35692
+ callSid,
35693
+ customParameters: {
35694
+ scenarioId,
35695
+ sessionId
35696
+ },
35697
+ streamSid
35698
+ },
35699
+ streamSid
35700
+ });
35701
+ await bridge.handleMessage({
35702
+ event: "media",
35703
+ media: {
35704
+ payload,
35705
+ track: "inbound"
35706
+ },
35707
+ streamSid
35708
+ });
35709
+ await waitFor(() => sentEvents.some((message) => message.event === "media"), timeoutMs);
35710
+ await bridge.handleMessage({
35711
+ event: "media",
35712
+ media: {
35713
+ payload,
35714
+ track: "inbound"
35715
+ },
35716
+ streamSid
35717
+ });
35718
+ await waitFor(() => sentEvents.some((message) => message.event === "clear"), timeoutMs);
35719
+ await bridge.handleMessage({
35720
+ event: "stop",
35721
+ stop: {
35722
+ callSid
35723
+ },
35724
+ streamSid
35725
+ });
35726
+ await bridge.close("telephony-media-operations-smoke-complete");
35727
+ const operationsRecord = await buildVoiceOperationsRecord({
35728
+ sessionId,
35729
+ store: trace
35730
+ });
35731
+ const media = operationsRecord.telephonyMedia;
35732
+ const issues = [
35733
+ media.starts < 1 ? "Missing telephony media start event." : undefined,
35734
+ media.stops < 1 ? "Missing telephony media stop event." : undefined,
35735
+ media.inbound < 2 ? "Expected at least two inbound telephony media events." : undefined,
35736
+ media.outbound < 2 ? "Expected outbound assistant media/control evidence." : undefined,
35737
+ media.media < 3 ? "Expected inbound and outbound telephony media packet evidence." : undefined,
35738
+ media.clears < 1 ? "Missing outbound clear evidence." : undefined,
35739
+ media.audioBytes <= 0 ? "Missing telephony media audio bytes." : undefined,
35740
+ media.carriers.includes("twilio") ? undefined : "Missing Twilio carrier evidence.",
35741
+ media.streamIds.includes(streamSid) ? undefined : "Missing telephony media stream ID evidence."
35742
+ ].filter((issue) => typeof issue === "string");
35743
+ return {
35744
+ issues,
35745
+ ok: issues.length === 0,
35746
+ operationsRecord,
35747
+ operationsRecordHref: resolveOperationsRecordHref(options.operationsRecordHref, { sessionId, streamSid }),
35748
+ sentEvents: sentEvents.map((message) => message.event).filter((event) => typeof event === "string"),
35749
+ sessionId,
35750
+ streamSid,
35751
+ telephonyMedia: media
35752
+ };
35753
+ };
35754
+ var getDefaultVoiceTelephonyBenchmarkScenarios = () => DEFAULT_SCENARIOS.map((scenario) => ({ ...scenario }));
35755
+ var runVoiceTelephonyBenchmarkScenario = async (scenario, options = {}) => {
35756
+ const timeoutMs = options.timeoutMs ?? 1000;
35757
+ const sentEvents = [];
35758
+ const receivedAudio = [];
35759
+ const bridge = createTwilioMediaStreamBridge({
35760
+ close: () => {},
35761
+ send: (data) => {
35762
+ sentEvents.push({
35763
+ at: Date.now(),
35764
+ event: JSON.parse(data)
35765
+ });
35766
+ }
35767
+ }, {
35768
+ context: {},
35769
+ onComplete: async () => {},
35770
+ onTurn: async () => ({
35771
+ assistantText: "Copy that."
35772
+ }),
35773
+ session: createVoiceMemoryStore(),
35774
+ stt: createFakeSTTAdapter(receivedAudio, scenario.sttDelayMs ?? 0),
35775
+ tts: createFakeTTSAdapter(scenario.ttsChunkCount ?? 2, scenario.ttsChunkDelayMs ?? 0),
35776
+ turnDetection: {
35777
+ transcriptStabilityMs: 0
35778
+ }
35779
+ });
35780
+ const startedAt = Date.now();
35781
+ let secondInboundAt;
35782
+ try {
35783
+ await bridge.handleMessage({
35784
+ event: "start",
35785
+ start: {
35786
+ callSid: "CA-benchmark",
35787
+ customParameters: {
35788
+ scenarioId: scenario.id,
35789
+ sessionId: `phone-${scenario.id}`
35790
+ },
35791
+ streamSid: "MZ-benchmark"
35792
+ },
35793
+ streamSid: "MZ-benchmark"
35794
+ });
35795
+ await bridge.handleMessage({
35796
+ event: "media",
35797
+ media: {
35798
+ payload: encodeTwilioMulawBase64(new Int16Array([500, -500, 1500, -1500, 2500, -2500])),
35799
+ track: "inbound"
35800
+ },
35801
+ streamSid: "MZ-benchmark"
35802
+ });
35803
+ const sawOutboundMedia = await waitFor(() => sentEvents.some((entry) => entry.event.event === "media"), timeoutMs);
35804
+ if (scenario.expectClear) {
35805
+ if (scenario.secondInboundDelayMs) {
35806
+ await Bun.sleep(scenario.secondInboundDelayMs);
35807
+ }
35808
+ secondInboundAt = Date.now();
35809
+ await bridge.handleMessage({
35810
+ event: "media",
35811
+ media: {
35812
+ payload: encodeTwilioMulawBase64(new Int16Array([200, -200, 200, -200])),
35813
+ track: "inbound"
35814
+ },
35815
+ streamSid: "MZ-benchmark"
35816
+ });
35817
+ }
35818
+ await waitFor(() => (!scenario.expectOutboundMedia || sawOutboundMedia) && (!scenario.expectMark || sentEvents.some((entry) => entry.event.event === "mark")) && (!scenario.expectClear || sentEvents.some((entry) => entry.event.event === "clear")), timeoutMs);
35819
+ } finally {
35820
+ await bridge.close("telephony-benchmark");
35821
+ }
35822
+ const outboundMediaEvents = sentEvents.filter((entry) => entry.event.event === "media");
35823
+ const markEvents = sentEvents.filter((entry) => entry.event.event === "mark");
35824
+ const clearEvents = sentEvents.filter((entry) => entry.event.event === "clear");
35825
+ const firstOutboundMediaAt = outboundMediaEvents[0]?.at;
35826
+ const firstMarkAt = markEvents[0]?.at;
35827
+ const firstClearAt = clearEvents[0]?.at;
35828
+ const passes = (!scenario.expectOutboundMedia || outboundMediaEvents.length > 0) && (!scenario.expectMark || markEvents.length > 0) && (!scenario.expectClear || clearEvents.length > 0);
35829
+ return {
35830
+ clearCount: clearEvents.length,
35831
+ clearLatencyMs: secondInboundAt !== undefined && firstClearAt !== undefined ? firstClearAt - secondInboundAt : undefined,
35832
+ elapsedMs: Date.now() - startedAt,
35833
+ expectClear: scenario.expectClear,
35834
+ expectMark: scenario.expectMark,
35835
+ expectOutboundMedia: scenario.expectOutboundMedia,
35836
+ fixtureId: scenario.id,
35837
+ firstOutboundMediaLatencyMs: firstOutboundMediaAt !== undefined ? firstOutboundMediaAt - startedAt : undefined,
35838
+ markCount: markEvents.length,
35839
+ markLatencyMs: firstMarkAt !== undefined ? firstMarkAt - startedAt : undefined,
35840
+ outboundMediaCount: outboundMediaEvents.length,
35841
+ passes,
35842
+ receivedAudioBytes: receivedAudio.reduce((sum, chunk) => sum + chunk.byteLength, 0),
35843
+ title: scenario.title
35844
+ };
35845
+ };
35846
+ var summarizeVoiceTelephonyBenchmark = (fixtures) => {
35847
+ const scenarioCount = fixtures.length;
35848
+ const firstMediaSamples = fixtures.filter((fixture) => typeof fixture.firstOutboundMediaLatencyMs === "number");
35849
+ const markSamples = fixtures.filter((fixture) => typeof fixture.markLatencyMs === "number");
35850
+ const clearSamples = fixtures.filter((fixture) => typeof fixture.clearLatencyMs === "number");
35851
+ const passCount = fixtures.filter((fixture) => fixture.passes).length;
35852
+ return {
35853
+ averageClearLatencyMs: clearSamples.length > 0 ? clearSamples.reduce((sum, fixture) => sum + fixture.clearLatencyMs, 0) / clearSamples.length : undefined,
35854
+ averageElapsedMs: scenarioCount > 0 ? fixtures.reduce((sum, fixture) => sum + fixture.elapsedMs, 0) / scenarioCount : 0,
35855
+ averageFirstOutboundMediaLatencyMs: firstMediaSamples.length > 0 ? firstMediaSamples.reduce((sum, fixture) => sum + fixture.firstOutboundMediaLatencyMs, 0) / firstMediaSamples.length : undefined,
35856
+ averageMarkLatencyMs: markSamples.length > 0 ? markSamples.reduce((sum, fixture) => sum + fixture.markLatencyMs, 0) / markSamples.length : undefined,
35857
+ passCount,
35858
+ passRate: scenarioCount > 0 ? passCount / scenarioCount : 0,
35859
+ scenarioCount,
35860
+ totalOutboundMediaCount: fixtures.reduce((sum, fixture) => sum + fixture.outboundMediaCount, 0)
35861
+ };
35862
+ };
35863
+ var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenchmarkScenarios(), options = {}) => {
35864
+ const fixtures = [];
35865
+ for (const scenario of scenarios) {
35866
+ fixtures.push(await runVoiceTelephonyBenchmarkScenario(scenario, options));
35867
+ }
35868
+ return {
35869
+ fixtures,
35870
+ generatedAt: Date.now(),
35871
+ summary: summarizeVoiceTelephonyBenchmark(fixtures)
35872
+ };
35873
+ };
35513
35874
  // src/telephony/response.ts
35514
35875
  var normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
35515
35876
  var DEFAULT_MAX_WORDS = 12;
@@ -35584,6 +35945,7 @@ export {
35584
35945
  summarizeVoiceTraceTimeline,
35585
35946
  summarizeVoiceTraceSinkDeliveries,
35586
35947
  summarizeVoiceTrace,
35948
+ summarizeVoiceTelephonyBenchmark,
35587
35949
  summarizeVoiceSessions,
35588
35950
  summarizeVoiceSessionReplay,
35589
35951
  summarizeVoiceRoutingSessions,
@@ -35616,6 +35978,9 @@ export {
35616
35978
  saveVoiceIncidentBundleArtifact,
35617
35979
  runVoiceToolContractSuite,
35618
35980
  runVoiceToolContract,
35981
+ runVoiceTelephonyMediaOperationsSmoke,
35982
+ runVoiceTelephonyBenchmarkScenario,
35983
+ runVoiceTelephonyBenchmark,
35619
35984
  runVoiceSimulationSuite,
35620
35985
  runVoiceSessionEvals,
35621
35986
  runVoiceScenarioFixtureEvals,
@@ -35759,6 +36124,7 @@ export {
35759
36124
  getVoiceCampaignDialerProofStatus,
35760
36125
  getLatestVoiceTelephonyMediaReport,
35761
36126
  getLatestVoiceBrowserMediaReport,
36127
+ getDefaultVoiceTelephonyBenchmarkScenarios,
35762
36128
  formatVoiceProofTrendAge,
35763
36129
  filterVoiceTraceEvents,
35764
36130
  filterVoiceAuditEvents,