@absolutejs/voice 0.0.22-beta.504 → 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.
- package/dist/angular/index.d.ts +2 -0
- package/dist/angular/index.js +463 -277
- package/dist/angular/voice-call-player.service.d.ts +19 -0
- package/dist/assistantExperiment.d.ts +41 -0
- package/dist/client/callPlayer.d.ts +41 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +398 -2
- package/dist/phoneProvisioning.d.ts +29 -0
- package/dist/react/VoiceCallPlayer.d.ts +11 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +433 -55
- package/dist/svelte/createVoiceCallPlayer.d.ts +33 -0
- package/dist/svelte/index.d.ts +2 -0
- package/dist/svelte/index.js +379 -190
- package/dist/vue/VoiceCallPlayer.d.ts +40 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +412 -69
- package/dist/webhookFanout.d.ts +48 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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";
|