@absolutejs/voice 0.0.22-beta.505 → 0.0.22-beta.506

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.
@@ -0,0 +1,19 @@
1
+ import { type VoiceCallPlayerOptions, type VoiceCallPlayerState } from "../client/callPlayer";
2
+ export type VoiceCallPlayerServiceOptions = VoiceCallPlayerOptions & {
3
+ title?: string;
4
+ };
5
+ export declare class VoiceCallPlayerService {
6
+ build(options?: VoiceCallPlayerServiceOptions): {
7
+ formatTimestamp: (ms: number) => string;
8
+ pause: () => void;
9
+ play: () => Promise<void>;
10
+ seekMs: (ms: number) => void;
11
+ seekToTranscript: (id: string) => void;
12
+ setPlaybackRate: (rate: number) => void;
13
+ setTime: (ms: number) => void;
14
+ state: import("@angular/core").Signal<VoiceCallPlayerState>;
15
+ stop: () => void;
16
+ title: string;
17
+ transcripts: () => readonly import("..").Transcript[];
18
+ };
19
+ }
@@ -0,0 +1,41 @@
1
+ import type { DefinedVoiceAssistant } from "./defineVoiceAssistant";
2
+ import type { VoiceSessionRecord } from "./types";
3
+ export type VoiceAssistantVariant<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
4
+ /** Stable variant id used for trace tagging + rollups. */
5
+ id: string;
6
+ /** Relative weight when allocator is 'random' or 'sticky' bucketing. Default 1. */
7
+ weight?: number;
8
+ /** The assistant definition produced by defineVoiceAssistant. */
9
+ assistant: DefinedVoiceAssistant<TContext, TSession, TResult>;
10
+ };
11
+ export type VoiceAssistantAllocatorInput<TContext = unknown> = {
12
+ context: TContext;
13
+ sessionId: string;
14
+ stickyKey?: string;
15
+ };
16
+ export type VoiceAssistantAllocator<TContext = unknown> = (input: VoiceAssistantAllocatorInput<TContext>) => string;
17
+ export type VoiceAssistantExperimentOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
18
+ /** Variant chooser. Default 'sticky' (hash of stickyKey → variant) when stickyKey is present, otherwise 'random'. */
19
+ allocator?: "random" | "sticky" | VoiceAssistantAllocator<TContext>;
20
+ /** Stable id for this experiment. Used in trace tagging. */
21
+ experimentId: string;
22
+ /** Callback for every allocation decision. Wire to trace.append({ type: 'assistant.experiment' }) for rollups. */
23
+ onAllocation?: (input: {
24
+ context: TContext;
25
+ experimentId: string;
26
+ sessionId: string;
27
+ stickyKey?: string;
28
+ variant: VoiceAssistantVariant<TContext, TSession, TResult>;
29
+ }) => void;
30
+ variants: ReadonlyArray<VoiceAssistantVariant<TContext, TSession, TResult>>;
31
+ };
32
+ export type VoiceAssistantExperimentDecision<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
33
+ experimentId: string;
34
+ variant: VoiceAssistantVariant<TContext, TSession, TResult>;
35
+ };
36
+ export type VoiceAssistantExperiment<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
37
+ allocate: (input: VoiceAssistantAllocatorInput<TContext>) => VoiceAssistantExperimentDecision<TContext, TSession, TResult>;
38
+ experimentId: string;
39
+ variants: ReadonlyArray<VoiceAssistantVariant<TContext, TSession, TResult>>;
40
+ };
41
+ export declare const createVoiceAssistantExperiment: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceAssistantExperimentOptions<TContext, TSession, TResult>) => VoiceAssistantExperiment<TContext, TSession, TResult>;
@@ -0,0 +1,41 @@
1
+ import type { Transcript } from "../types";
2
+ export type VoiceCallPlayerState = {
3
+ activeTranscriptId?: string;
4
+ activeTranscriptIndex?: number;
5
+ audioUrl?: string;
6
+ buffered: number;
7
+ currentTimeMs: number;
8
+ durationMs: number;
9
+ error?: string;
10
+ isPlaying: boolean;
11
+ isReady: boolean;
12
+ playbackRate: number;
13
+ };
14
+ export type VoiceCallPlayerOptions = {
15
+ audioUrl?: string;
16
+ initialPlaybackRate?: number;
17
+ /** Recording start in epoch ms; used to convert transcript epoch timestamps to playback offsets. */
18
+ recordingStartedAtEpochMs?: number;
19
+ transcripts?: ReadonlyArray<Transcript>;
20
+ };
21
+ export type VoiceCallPlayer = {
22
+ getState: () => VoiceCallPlayerState;
23
+ pause: () => void;
24
+ play: () => Promise<void>;
25
+ reset: () => void;
26
+ seekMs: (positionMs: number) => void;
27
+ seekToTranscript: (transcriptId: string) => void;
28
+ setAudioUrl: (url: string | undefined) => void;
29
+ setBuffered: (seconds: number) => void;
30
+ setDuration: (durationMs: number) => void;
31
+ setError: (error: string | undefined) => void;
32
+ setPlaybackRate: (rate: number) => void;
33
+ setPlaying: (playing: boolean) => void;
34
+ setReady: (ready: boolean) => void;
35
+ setTime: (positionMs: number) => void;
36
+ setTranscripts: (transcripts: ReadonlyArray<Transcript>) => void;
37
+ subscribe: (listener: () => void) => () => void;
38
+ transcripts: () => ReadonlyArray<Transcript>;
39
+ };
40
+ export declare const createVoiceCallPlayer: (options?: VoiceCallPlayerOptions) => VoiceCallPlayer;
41
+ export declare const formatVoiceCallPlayerTimestamp: (ms: number) => string;
package/dist/index.d.ts CHANGED
@@ -110,6 +110,12 @@ export { createInMemoryVoiceCallQuota } from "./callQuota";
110
110
  export type { CreateInMemoryVoiceCallQuotaOptions, VoiceCallQuota, VoiceCallQuotaRejection, VoiceCallQuotaResult, VoiceCallQuotaTier, VoiceCallReservation, } from "./callQuota";
111
111
  export { createVoiceBearerAuthVerifier, createVoiceHMACAuthVerifier, createVoiceRouteAuth, } from "./routeAuth";
112
112
  export type { VoiceRouteAuthDecision, VoiceRouteAuthInput, VoiceRouteAuthOptions, VoiceRouteAuthVerifier, } from "./routeAuth";
113
+ export { createVoiceCallPlayer, formatVoiceCallPlayerTimestamp, } from "./client/callPlayer";
114
+ export type { VoiceCallPlayer, VoiceCallPlayerOptions, VoiceCallPlayerState, } from "./client/callPlayer";
115
+ export { provisionTelnyxPhoneNumber, provisionTwilioPhoneNumber, } from "./phoneProvisioning";
116
+ export type { TelnyxProvisionInput, TwilioProvisionInput, VoicePhoneNumber, } from "./phoneProvisioning";
117
+ export { createVoiceWebhookFanout } from "./webhookFanout";
118
+ export type { VoiceWebhookFanout, VoiceWebhookFanoutEvent, VoiceWebhookFanoutOptions, VoiceWebhookFanoutReport, VoiceWebhookSink, VoiceWebhookSinkDeliveryResult, } from "./webhookFanout";
113
119
  export { BROWSER_NOISE_SUPPRESSOR_PRESETS, applyBrowserNoiseSuppression, } from "./client/browserNoiseSuppression";
114
120
  export type { BrowserNoiseSuppressorHandle, BrowserNoiseSuppressorOptions, BrowserNoiseSuppressorPreset, } from "./client/browserNoiseSuppression";
115
121
  export { buildVoiceHTMXAttributes, wrapVoiceHTMLInHTMXContainer, wrapVoiceHTMLWithHTMXPolling, } from "./client/htmxAttributes";
package/dist/index.js CHANGED
@@ -36781,6 +36781,397 @@ var createVoiceRouteAuth = (options) => {
36781
36781
  }
36782
36782
  });
36783
36783
  };
36784
+ // src/client/callPlayer.ts
36785
+ var cloneState = (state) => ({
36786
+ ...state
36787
+ });
36788
+ var normalizeTranscriptTimes = (transcripts, baseEpoch) => {
36789
+ if (typeof baseEpoch !== "number") {
36790
+ return transcripts;
36791
+ }
36792
+ return transcripts.map((transcript) => {
36793
+ const adjusted = { ...transcript };
36794
+ if (typeof adjusted.startedAtMs === "number" && adjusted.startedAtMs >= baseEpoch) {
36795
+ adjusted.startedAtMs = adjusted.startedAtMs - baseEpoch;
36796
+ }
36797
+ if (typeof adjusted.endedAtMs === "number" && adjusted.endedAtMs >= baseEpoch) {
36798
+ adjusted.endedAtMs = adjusted.endedAtMs - baseEpoch;
36799
+ }
36800
+ return adjusted;
36801
+ });
36802
+ };
36803
+ var findActiveTranscript = (transcripts, positionMs) => {
36804
+ let candidate;
36805
+ for (let index = 0;index < transcripts.length; index += 1) {
36806
+ const transcript = transcripts[index];
36807
+ if (typeof transcript.startedAtMs !== "number")
36808
+ continue;
36809
+ if (transcript.startedAtMs > positionMs)
36810
+ break;
36811
+ if (typeof transcript.endedAtMs === "number" && transcript.endedAtMs < positionMs) {
36812
+ continue;
36813
+ }
36814
+ candidate = { id: transcript.id, index };
36815
+ }
36816
+ return candidate ?? {};
36817
+ };
36818
+ var createVoiceCallPlayer = (options = {}) => {
36819
+ let transcripts = normalizeTranscriptTimes(options.transcripts ?? [], options.recordingStartedAtEpochMs);
36820
+ let state = {
36821
+ audioUrl: options.audioUrl,
36822
+ buffered: 0,
36823
+ currentTimeMs: 0,
36824
+ durationMs: 0,
36825
+ isPlaying: false,
36826
+ isReady: false,
36827
+ playbackRate: options.initialPlaybackRate ?? 1
36828
+ };
36829
+ const listeners = new Set;
36830
+ const notify = () => {
36831
+ for (const listener of listeners)
36832
+ listener();
36833
+ };
36834
+ const update = (next) => {
36835
+ state = { ...state, ...next };
36836
+ notify();
36837
+ };
36838
+ const refreshActive = () => {
36839
+ const { id, index } = findActiveTranscript(transcripts, state.currentTimeMs);
36840
+ if (id !== state.activeTranscriptId || index !== state.activeTranscriptIndex) {
36841
+ state = {
36842
+ ...state,
36843
+ activeTranscriptId: id,
36844
+ activeTranscriptIndex: index
36845
+ };
36846
+ notify();
36847
+ }
36848
+ };
36849
+ return {
36850
+ getState: () => cloneState(state),
36851
+ pause: () => {
36852
+ if (!state.isPlaying)
36853
+ return;
36854
+ update({ isPlaying: false });
36855
+ },
36856
+ play: async () => {
36857
+ update({ isPlaying: true });
36858
+ },
36859
+ reset: () => {
36860
+ state = {
36861
+ audioUrl: state.audioUrl,
36862
+ buffered: 0,
36863
+ currentTimeMs: 0,
36864
+ durationMs: 0,
36865
+ isPlaying: false,
36866
+ isReady: false,
36867
+ playbackRate: 1
36868
+ };
36869
+ notify();
36870
+ },
36871
+ seekMs: (positionMs) => {
36872
+ const clamped = Math.max(0, Math.min(state.durationMs || Number.POSITIVE_INFINITY, positionMs));
36873
+ update({ currentTimeMs: clamped });
36874
+ refreshActive();
36875
+ },
36876
+ seekToTranscript: (transcriptId) => {
36877
+ const found = transcripts.find((t) => t.id === transcriptId);
36878
+ if (!found || typeof found.startedAtMs !== "number") {
36879
+ return;
36880
+ }
36881
+ update({ currentTimeMs: Math.max(0, found.startedAtMs) });
36882
+ refreshActive();
36883
+ },
36884
+ setAudioUrl: (url) => {
36885
+ update({ audioUrl: url, isReady: false });
36886
+ },
36887
+ setBuffered: (seconds) => {
36888
+ update({ buffered: Math.max(0, seconds) });
36889
+ },
36890
+ setDuration: (durationMs) => {
36891
+ update({ durationMs: Math.max(0, durationMs) });
36892
+ },
36893
+ setError: (error) => {
36894
+ update({ error });
36895
+ },
36896
+ setPlaybackRate: (rate5) => {
36897
+ update({ playbackRate: Math.max(0.25, Math.min(4, rate5)) });
36898
+ },
36899
+ setPlaying: (playing) => {
36900
+ if (playing === state.isPlaying)
36901
+ return;
36902
+ update({ isPlaying: playing });
36903
+ },
36904
+ setReady: (ready) => {
36905
+ update({ isReady: ready });
36906
+ },
36907
+ setTime: (positionMs) => {
36908
+ const next = Math.max(0, positionMs);
36909
+ if (next === state.currentTimeMs)
36910
+ return;
36911
+ update({ currentTimeMs: next });
36912
+ refreshActive();
36913
+ },
36914
+ setTranscripts: (next) => {
36915
+ transcripts = normalizeTranscriptTimes(next, options.recordingStartedAtEpochMs);
36916
+ refreshActive();
36917
+ },
36918
+ subscribe: (listener) => {
36919
+ listeners.add(listener);
36920
+ return () => {
36921
+ listeners.delete(listener);
36922
+ };
36923
+ },
36924
+ transcripts: () => transcripts
36925
+ };
36926
+ };
36927
+ var formatVoiceCallPlayerTimestamp = (ms) => {
36928
+ const seconds = Math.max(0, Math.floor(ms / 1000));
36929
+ const minutes = Math.floor(seconds / 60);
36930
+ const remaining = seconds % 60;
36931
+ return `${String(minutes).padStart(2, "0")}:${String(remaining).padStart(2, "0")}`;
36932
+ };
36933
+ // src/phoneProvisioning.ts
36934
+ var requireAuth = (input) => {
36935
+ if (!input.accountSid || !input.authToken) {
36936
+ throw new Error("Twilio provisioning requires accountSid + authToken");
36937
+ }
36938
+ };
36939
+ var toBasicAuth = (sid, token) => `Basic ${btoa(`${sid}:${token}`)}`;
36940
+ var searchTwilioCandidate = async (input) => {
36941
+ const fetchImpl = input.fetch ?? globalThis.fetch.bind(globalThis);
36942
+ const country = input.countryCode ?? "US";
36943
+ const url = new URL(`https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(input.accountSid)}/AvailablePhoneNumbers/${encodeURIComponent(country)}/Local.json`);
36944
+ if (input.areaCode)
36945
+ url.searchParams.set("AreaCode", input.areaCode);
36946
+ if (input.contains)
36947
+ url.searchParams.set("Contains", input.contains);
36948
+ url.searchParams.set("PageSize", "5");
36949
+ const response = await fetchImpl(url, {
36950
+ headers: {
36951
+ accept: "application/json",
36952
+ authorization: toBasicAuth(input.accountSid, input.authToken)
36953
+ }
36954
+ });
36955
+ if (!response.ok) {
36956
+ const body = await response.text().catch(() => "");
36957
+ throw new Error(`Twilio AvailablePhoneNumbers failed: ${response.status} ${response.statusText} ${body.slice(0, 200)}`);
36958
+ }
36959
+ const payload = await response.json();
36960
+ const candidate = payload.available_phone_numbers?.[0]?.phone_number;
36961
+ if (!candidate) {
36962
+ throw new Error("Twilio returned no available phone numbers for the query");
36963
+ }
36964
+ return candidate;
36965
+ };
36966
+ var provisionTwilioPhoneNumber = async (input) => {
36967
+ requireAuth(input);
36968
+ const fetchImpl = input.fetch ?? globalThis.fetch.bind(globalThis);
36969
+ const phoneNumber = await searchTwilioCandidate(input);
36970
+ const body = new URLSearchParams;
36971
+ body.set("PhoneNumber", phoneNumber);
36972
+ body.set("VoiceUrl", input.voiceUrl);
36973
+ if (input.friendlyName)
36974
+ body.set("FriendlyName", input.friendlyName);
36975
+ if (input.statusCallbackUrl)
36976
+ body.set("StatusCallback", input.statusCallbackUrl);
36977
+ if (input.smsUrl)
36978
+ body.set("SmsUrl", input.smsUrl);
36979
+ const purchaseUrl = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(input.accountSid)}/IncomingPhoneNumbers.json`;
36980
+ const response = await fetchImpl(purchaseUrl, {
36981
+ body: body.toString(),
36982
+ headers: {
36983
+ accept: "application/json",
36984
+ authorization: toBasicAuth(input.accountSid, input.authToken),
36985
+ "content-type": "application/x-www-form-urlencoded"
36986
+ },
36987
+ method: "POST"
36988
+ });
36989
+ if (!response.ok) {
36990
+ const text = await response.text().catch(() => "");
36991
+ throw new Error(`Twilio IncomingPhoneNumbers POST failed: ${response.status} ${response.statusText} ${text.slice(0, 200)}`);
36992
+ }
36993
+ const result = await response.json();
36994
+ return {
36995
+ phoneNumber: result.phone_number,
36996
+ provider: "twilio",
36997
+ providerNumberId: result.sid,
36998
+ raw: result
36999
+ };
37000
+ };
37001
+ var provisionTelnyxPhoneNumber = async (input) => {
37002
+ if (!input.apiKey) {
37003
+ throw new Error("Telnyx provisioning requires apiKey");
37004
+ }
37005
+ const fetchImpl = input.fetch ?? globalThis.fetch.bind(globalThis);
37006
+ const searchUrl = new URL("https://api.telnyx.com/v2/available_phone_numbers");
37007
+ searchUrl.searchParams.set("filter[country_code]", input.countryCode ?? "US");
37008
+ searchUrl.searchParams.set("filter[features]", "voice");
37009
+ if (input.areaCode) {
37010
+ searchUrl.searchParams.set("filter[national_destination_code]", input.areaCode);
37011
+ }
37012
+ searchUrl.searchParams.set("filter[limit]", "5");
37013
+ const searchResponse = await fetchImpl(searchUrl, {
37014
+ headers: {
37015
+ accept: "application/json",
37016
+ authorization: `Bearer ${input.apiKey}`
37017
+ }
37018
+ });
37019
+ if (!searchResponse.ok) {
37020
+ const text = await searchResponse.text().catch(() => "");
37021
+ throw new Error(`Telnyx available_phone_numbers failed: ${searchResponse.status} ${text.slice(0, 200)}`);
37022
+ }
37023
+ const searchPayload = await searchResponse.json();
37024
+ const candidate = searchPayload.data?.[0]?.phone_number;
37025
+ if (!candidate) {
37026
+ throw new Error("Telnyx returned no available phone numbers for the query");
37027
+ }
37028
+ const orderBody = {
37029
+ phone_numbers: [{ phone_number: candidate }]
37030
+ };
37031
+ if (input.connectionId)
37032
+ orderBody.connection_id = input.connectionId;
37033
+ if (input.messagingProfileId)
37034
+ orderBody.messaging_profile_id = input.messagingProfileId;
37035
+ const orderResponse = await fetchImpl("https://api.telnyx.com/v2/number_orders", {
37036
+ body: JSON.stringify(orderBody),
37037
+ headers: {
37038
+ accept: "application/json",
37039
+ authorization: `Bearer ${input.apiKey}`,
37040
+ "content-type": "application/json"
37041
+ },
37042
+ method: "POST"
37043
+ });
37044
+ if (!orderResponse.ok) {
37045
+ const text = await orderResponse.text().catch(() => "");
37046
+ throw new Error(`Telnyx number_orders POST failed: ${orderResponse.status} ${text.slice(0, 200)}`);
37047
+ }
37048
+ const orderResult = await orderResponse.json();
37049
+ const orderedNumber = orderResult.data?.phone_numbers?.[0]?.phone_number ?? candidate;
37050
+ const phoneNumberId = orderResult.data?.phone_numbers?.[0]?.id ?? orderResult.data?.id ?? "";
37051
+ if (phoneNumberId) {
37052
+ const updateResponse = await fetchImpl(`https://api.telnyx.com/v2/phone_numbers/${encodeURIComponent(phoneNumberId)}`, {
37053
+ body: JSON.stringify({
37054
+ voice: { webhook_url: input.voiceWebhookUrl }
37055
+ }),
37056
+ headers: {
37057
+ accept: "application/json",
37058
+ authorization: `Bearer ${input.apiKey}`,
37059
+ "content-type": "application/json"
37060
+ },
37061
+ method: "PATCH"
37062
+ });
37063
+ if (!updateResponse.ok) {
37064
+ const text = await updateResponse.text().catch(() => "");
37065
+ throw new Error(`Telnyx phone_numbers PATCH failed: ${updateResponse.status} ${text.slice(0, 200)}`);
37066
+ }
37067
+ }
37068
+ return {
37069
+ phoneNumber: orderedNumber,
37070
+ provider: "telnyx",
37071
+ providerNumberId: phoneNumberId,
37072
+ raw: orderResult
37073
+ };
37074
+ };
37075
+ // src/webhookFanout.ts
37076
+ var deliverOnce = async (input) => {
37077
+ const startedAt = Date.now();
37078
+ const timeoutMs = input.sink.timeoutMs ?? 1e4;
37079
+ const controller = new AbortController;
37080
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
37081
+ const headers = {
37082
+ "content-type": "application/json",
37083
+ ...input.sink.headers
37084
+ };
37085
+ if (input.sink.signingSecret) {
37086
+ const timestamp = String(Date.now());
37087
+ headers["x-absolutejs-timestamp"] = timestamp;
37088
+ headers["x-absolutejs-signature"] = await signVoiceWebhookBody({
37089
+ body: input.body,
37090
+ secret: input.sink.signingSecret,
37091
+ timestamp
37092
+ });
37093
+ }
37094
+ try {
37095
+ const response = await input.fetchImpl(input.sink.url, {
37096
+ body: input.body,
37097
+ headers,
37098
+ method: "POST",
37099
+ signal: controller.signal
37100
+ });
37101
+ const durationMs = Date.now() - startedAt;
37102
+ if (!response.ok) {
37103
+ return {
37104
+ attempt: 0,
37105
+ durationMs,
37106
+ error: `HTTP ${response.status}`,
37107
+ ok: false,
37108
+ sinkId: input.sink.id,
37109
+ status: response.status
37110
+ };
37111
+ }
37112
+ return {
37113
+ attempt: 0,
37114
+ durationMs,
37115
+ ok: true,
37116
+ sinkId: input.sink.id,
37117
+ status: response.status
37118
+ };
37119
+ } catch (error) {
37120
+ return {
37121
+ attempt: 0,
37122
+ durationMs: Date.now() - startedAt,
37123
+ error: error instanceof Error ? error.message : String(error),
37124
+ ok: false,
37125
+ sinkId: input.sink.id
37126
+ };
37127
+ } finally {
37128
+ clearTimeout(timer);
37129
+ }
37130
+ };
37131
+ var sleep6 = (ms) => new Promise((resolve2) => {
37132
+ setTimeout(resolve2, ms);
37133
+ });
37134
+ var deliverWithRetry = async (input) => {
37135
+ const maxRetries = input.sink.maxRetries ?? 3;
37136
+ const backoffMs = input.sink.backoffMs ?? 1000;
37137
+ let last;
37138
+ for (let attempt = 1;attempt <= maxRetries; attempt += 1) {
37139
+ last = await deliverOnce(input);
37140
+ last.attempt = attempt;
37141
+ if (last.ok) {
37142
+ return last;
37143
+ }
37144
+ if (attempt < maxRetries) {
37145
+ await sleep6(backoffMs * attempt);
37146
+ }
37147
+ }
37148
+ return last ?? {
37149
+ attempt: 0,
37150
+ durationMs: 0,
37151
+ error: "no attempts ran",
37152
+ ok: false,
37153
+ sinkId: input.sink.id
37154
+ };
37155
+ };
37156
+ var createVoiceWebhookFanout = (options) => {
37157
+ const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
37158
+ return {
37159
+ deliver: async (event) => {
37160
+ const body = JSON.stringify({
37161
+ payload: event.payload,
37162
+ type: event.type
37163
+ });
37164
+ const matching = options.sinks.filter((sink) => sink.acceptEvent ? sink.acceptEvent(event) : true);
37165
+ const deliveries = await Promise.all(matching.map((sink) => deliverWithRetry({ body, fetchImpl, sink })));
37166
+ const succeeded = deliveries.filter((d) => d.ok).length;
37167
+ return {
37168
+ deliveries,
37169
+ failed: deliveries.length - succeeded,
37170
+ succeeded
37171
+ };
37172
+ }
37173
+ };
37174
+ };
36784
37175
  // src/client/browserNoiseSuppression.ts
36785
37176
  var isBrowser = () => typeof window !== "undefined" && typeof window.AudioContext !== "undefined";
36786
37177
  var applyBrowserNoiseSuppression = async (options) => {
@@ -39812,7 +40203,7 @@ var getMessageToolCalls = (message) => {
39812
40203
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
39813
40204
  };
39814
40205
  var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
39815
- var sleep6 = (ms) => new Promise((resolve2) => {
40206
+ var sleep7 = (ms) => new Promise((resolve2) => {
39816
40207
  setTimeout(resolve2, ms);
39817
40208
  });
39818
40209
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -40526,7 +40917,7 @@ var createGeminiVoiceAssistantModel = (options) => {
40526
40917
  break;
40527
40918
  }
40528
40919
  const retryAfter = Number(response.headers.get("retry-after"));
40529
- await sleep6(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
40920
+ await sleep7(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
40530
40921
  }
40531
40922
  if (!response) {
40532
40923
  throw new Error("Gemini voice assistant model failed: no response");
@@ -47861,6 +48252,8 @@ export {
47861
48252
  purgeVoiceRetentionStore,
47862
48253
  pruneVoiceTraceEvents,
47863
48254
  pruneVoiceIncidentBundleArtifacts,
48255
+ provisionTwilioPhoneNumber,
48256
+ provisionTelnyxPhoneNumber,
47864
48257
  parseVoiceTelephonyWebhookEvent,
47865
48258
  parseVoiceSessionSnapshot,
47866
48259
  normalizeVoiceProofTrendReport,
@@ -47890,6 +48283,7 @@ export {
47890
48283
  getDefaultVoiceTelephonyBenchmarkScenarios,
47891
48284
  fromVapiAssistantConfig,
47892
48285
  formatVoiceProofTrendAge,
48286
+ formatVoiceCallPlayerTimestamp,
47893
48287
  filterVoiceTraceEvents,
47894
48288
  filterVoiceAuditEvents,
47895
48289
  fetchVoiceProofTarget,
@@ -47963,6 +48357,7 @@ export {
47963
48357
  createVoiceWorkflowContractHandler,
47964
48358
  createVoiceWorkflowContract,
47965
48359
  createVoiceWebhookHandoffAdapter,
48360
+ createVoiceWebhookFanout,
47966
48361
  createVoiceWebhookDeliveryWorkerLoop,
47967
48362
  createVoiceWebhookDeliveryWorker,
47968
48363
  createVoiceWebhookDeliverySink,
@@ -48258,6 +48653,7 @@ export {
48258
48653
  createVoiceCallReviewRecorder,
48259
48654
  createVoiceCallReviewFromSession,
48260
48655
  createVoiceCallReviewFromLiveTelephonyReport,
48656
+ createVoiceCallPlayer,
48261
48657
  createVoiceCallDebuggerRoutes,
48262
48658
  createVoiceCallCompletedEvent,
48263
48659
  createVoiceCRMActivitySink,
@@ -0,0 +1,29 @@
1
+ export type VoicePhoneNumber = {
2
+ phoneNumber: string;
3
+ provider: "telnyx" | "twilio" | (string & {});
4
+ providerNumberId: string;
5
+ raw: unknown;
6
+ };
7
+ export type TwilioProvisionInput = {
8
+ accountSid: string;
9
+ areaCode?: string;
10
+ authToken: string;
11
+ contains?: string;
12
+ countryCode?: string;
13
+ fetch?: typeof fetch;
14
+ friendlyName?: string;
15
+ smsUrl?: string;
16
+ statusCallbackUrl?: string;
17
+ voiceUrl: string;
18
+ };
19
+ export declare const provisionTwilioPhoneNumber: (input: TwilioProvisionInput) => Promise<VoicePhoneNumber>;
20
+ export type TelnyxProvisionInput = {
21
+ apiKey: string;
22
+ areaCode?: string;
23
+ connectionId?: string;
24
+ countryCode?: string;
25
+ fetch?: typeof fetch;
26
+ messagingProfileId?: string;
27
+ voiceWebhookUrl: string;
28
+ };
29
+ export declare const provisionTelnyxPhoneNumber: (input: TelnyxProvisionInput) => Promise<VoicePhoneNumber>;
@@ -0,0 +1,11 @@
1
+ import { type VoiceCallPlayer as VoiceCallPlayerHandle, type VoiceCallPlayerOptions } from "../client/callPlayer";
2
+ import type { Transcript } from "../types";
3
+ export type VoiceCallPlayerProps = VoiceCallPlayerOptions & {
4
+ audioUrl?: string;
5
+ className?: string;
6
+ onError?: (error: string) => void;
7
+ player?: VoiceCallPlayerHandle;
8
+ title?: string;
9
+ transcripts?: ReadonlyArray<Transcript>;
10
+ };
11
+ export declare const VoiceCallPlayer: ({ audioUrl, className, onError, player: playerProp, recordingStartedAtEpochMs, title, transcripts, }: VoiceCallPlayerProps) => import("react/jsx-runtime").JSX.Element;
@@ -37,6 +37,8 @@ export { useVoiceStream } from "./useVoiceStream";
37
37
  export { useVoiceController } from "./useVoiceController";
38
38
  export { VoiceWidget } from "./VoiceWidget";
39
39
  export type { VoiceWidgetLabels, VoiceWidgetProps, VoiceWidgetTheme, } from "./VoiceWidget";
40
+ export { VoiceCallPlayer } from "./VoiceCallPlayer";
41
+ export type { VoiceCallPlayerProps } from "./VoiceCallPlayer";
40
42
  export { VoiceCostDashboard } from "./VoiceCostDashboard";
41
43
  export type { VoiceCostDashboardProps } from "./VoiceCostDashboard";
42
44
  export { VoiceLiveCallViewer } from "./VoiceLiveCallViewer";