@absolutejs/voice 0.0.22-beta.81 → 0.0.22-beta.83

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
@@ -15,6 +15,7 @@ export { applyVoiceTelephonyOutcome, createMemoryVoiceTelephonyWebhookIdempotenc
15
15
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
16
16
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
17
17
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, resolveVoiceProviderRoutingPolicyPreset, createVoiceProviderRouter } from './modelAdapters';
18
+ export { createOpenAIVoiceTTS } from './openaiTTS';
18
19
  export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
19
20
  export { createVoiceProviderCapabilityHTMLHandler, createVoiceProviderCapabilityJSONHandler, createVoiceProviderCapabilityRoutes, renderVoiceProviderCapabilityHTML, summarizeVoiceProviderCapabilities } from './providerCapabilities';
20
21
  export { buildVoiceOpsConsoleReport, createVoiceOpsConsoleRoutes, renderVoiceOpsConsoleHTML } from './opsConsoleRoutes';
@@ -53,6 +54,7 @@ export type { VoiceEvalBaselineComparison, VoiceEvalBaselineComparisonOptions, V
53
54
  export type { VoiceWorkflowContract, VoiceWorkflowContractDefinition, VoiceWorkflowContractField, VoiceWorkflowContractFieldMatch, VoiceWorkflowContractPresetName, VoiceWorkflowContractPresetOptions, VoiceWorkflowContractTracePayload, VoiceWorkflowContractValidation, VoiceWorkflowContractValidationIssue, VoiceWorkflowOutcome } from './workflowContract';
54
55
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
55
56
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterPolicyPreset, VoiceProviderRouterPolicyWeights, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceProviderRouterStrategy, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
57
+ export type { OpenAIVoiceTTSOptions, OpenAIVoiceTTSVoice } from './openaiTTS';
56
58
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
57
59
  export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
58
60
  export type { VoiceTurnQualityHTMLHandlerOptions, VoiceTurnQualityItem, VoiceTurnQualityOptions, VoiceTurnQualityReport, VoiceTurnQualityRoutesOptions, VoiceTurnQualityStatus } from './turnQuality';
@@ -82,13 +84,13 @@ export type { VoiceSQLiteRuntimeStorage, VoiceSQLiteStoreOptions } from './sqlit
82
84
  export type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMap, VoiceExternalObjectMapStore, VoiceOpsTaskAgeBucket, VoiceOpsTaskAnalyticsOptions, VoiceOpsTaskAnalyticsSummary, VoiceOpsTaskAssignmentRule, VoiceOpsTaskAssignmentRuleCondition, VoiceOpsTaskAssignmentRules, VoiceOpsTaskAssigneeAnalytics, VoiceOpsDispositionTaskPolicies, VoiceOpsSLABreachPolicy, VoiceIntegrationDeliveryStatus, VoiceIntegrationEvent, VoiceIntegrationEventStore, VoiceIntegrationSinkDelivery, VoiceIntegrationEventType, VoiceIntegrationWebhookConfig, VoiceOpsTask, VoiceOpsTaskHistoryEntry, VoiceOpsTaskKind, VoiceOpsTaskPolicy, VoiceOpsTaskPriority, VoiceOpsTaskStatus, VoiceOpsTaskStore, VoiceOpsTaskSummary, VoiceOpsTaskWorkerAnalytics } from './ops';
83
85
  export { createTwilioMediaStreamBridge, createTwilioVoiceRoutes, createTwilioVoiceResponse, decodeTwilioMulawBase64, encodeTwilioMulawBase64, transcodePCMToTwilioOutboundPayload, transcodeTwilioInboundPayloadToPCM16 } from './telephony/twilio';
84
86
  export { evaluateVoiceTelephonyContract } from './telephony/contract';
85
- export { createTelnyxVoiceResponse, createTelnyxVoiceRoutes, verifyVoiceTelnyxWebhookSignature } from './telephony/telnyx';
86
- export { createPlivoVoiceResponse, createPlivoVoiceRoutes, signVoicePlivoWebhook, verifyVoicePlivoWebhookSignature } from './telephony/plivo';
87
+ export { createTelnyxMediaStreamBridge, createTelnyxVoiceResponse, createTelnyxVoiceRoutes, verifyVoiceTelnyxWebhookSignature } from './telephony/telnyx';
88
+ export { createPlivoMediaStreamBridge, createPlivoVoiceResponse, createPlivoVoiceRoutes, signVoicePlivoWebhook, verifyVoicePlivoWebhookSignature } from './telephony/plivo';
87
89
  export { createVoiceTelephonyCarrierMatrix, createVoiceTelephonyCarrierMatrixRoutes, renderVoiceTelephonyCarrierMatrixHTML } from './telephony/matrix';
88
90
  export type { TwilioInboundMessage, TwilioMediaStreamBridge, TwilioMediaStreamBridgeOptions, TwilioMediaStreamSocket, TwilioOutboundClearMessage, TwilioOutboundMarkMessage, TwilioOutboundMediaMessage, TwilioOutboundMessage, TwilioVoiceRouteParameters, TwilioVoiceResponseOptions, TwilioVoiceSmokeCheck, TwilioVoiceSmokeOptions, TwilioVoiceSmokeReport, TwilioVoiceSetupOptions, TwilioVoiceSetupStatus, TwilioVoiceRoutesOptions } from './telephony/twilio';
89
91
  export type { VoiceTelephonyContractIssue, VoiceTelephonyContractOptions, VoiceTelephonyContractReport, VoiceTelephonyContractRequirement, VoiceTelephonyProvider, VoiceTelephonySetupStatus, VoiceTelephonySmokeCheck, VoiceTelephonySmokeReport } from './telephony/contract';
90
- export type { TelnyxVoiceResponseOptions, TelnyxVoiceRoutesOptions, TelnyxVoiceSetupOptions, TelnyxVoiceSetupStatus, TelnyxVoiceSmokeCheck, TelnyxVoiceSmokeOptions, TelnyxVoiceSmokeReport } from './telephony/telnyx';
91
- export type { PlivoVoiceResponseOptions, PlivoVoiceRoutesOptions, PlivoVoiceSetupOptions, PlivoVoiceSetupStatus, PlivoVoiceSmokeCheck, PlivoVoiceSmokeOptions, PlivoVoiceSmokeReport } from './telephony/plivo';
92
+ export type { TelnyxInboundMessage, TelnyxMediaPayload, TelnyxMediaStreamBridge, TelnyxMediaStreamBridgeOptions, TelnyxMediaStreamSocket, TelnyxOutboundClearMessage, TelnyxOutboundMarkMessage, TelnyxOutboundMediaMessage, TelnyxOutboundMessage, TelnyxVoiceResponseOptions, TelnyxVoiceRoutesOptions, TelnyxVoiceSetupOptions, TelnyxVoiceSetupStatus, TelnyxVoiceSmokeCheck, TelnyxVoiceSmokeOptions, TelnyxVoiceSmokeReport } from './telephony/telnyx';
93
+ export type { PlivoInboundMessage, PlivoMediaStreamBridge, PlivoMediaStreamBridgeOptions, PlivoMediaStreamSocket, PlivoOutboundCheckpointMessage, PlivoOutboundClearAudioMessage, PlivoOutboundMessage, PlivoOutboundPlayAudioMessage, PlivoVoiceResponseOptions, PlivoVoiceRoutesOptions, PlivoVoiceSetupOptions, PlivoVoiceSetupStatus, PlivoVoiceSmokeCheck, PlivoVoiceSmokeOptions, PlivoVoiceSmokeReport } from './telephony/plivo';
92
94
  export type { VoiceTelephonyCarrierMatrix, VoiceTelephonyCarrierMatrixEntry, VoiceTelephonyCarrierMatrixInput, VoiceTelephonyCarrierMatrixOptions, VoiceTelephonyCarrierMatrixRoutesOptions, VoiceTelephonyCarrierMatrixStatus } from './telephony/matrix';
93
95
  export { shapeTelephonyAssistantText } from './telephony/response';
94
96
  export type { TelephonyResponseShapeMode, TelephonyResponseShapeOptions } from './telephony/response';
package/dist/index.js CHANGED
@@ -12424,6 +12424,136 @@ var createGeminiVoiceAssistantModel = (options) => {
12424
12424
  }
12425
12425
  };
12426
12426
  };
12427
+ // src/openaiTTS.ts
12428
+ var OPENAI_PCM24_FORMAT = {
12429
+ channels: 1,
12430
+ container: "raw",
12431
+ encoding: "pcm_s16le",
12432
+ sampleRateHz: 24000
12433
+ };
12434
+ var resolveInstructions = async (instructions, input) => {
12435
+ if (typeof instructions === "function") {
12436
+ return instructions(input);
12437
+ }
12438
+ return instructions;
12439
+ };
12440
+ var createTTSHTTPError = (response) => new Error(`OpenAI voice TTS failed: HTTP ${response.status}`);
12441
+ var emit = async (listeners, event, payload) => {
12442
+ for (const handler of listeners[event]) {
12443
+ await Promise.resolve(handler(payload));
12444
+ }
12445
+ };
12446
+ var createOpenAIVoiceTTS = (options) => {
12447
+ const fetchImpl = options.fetch ?? globalThis.fetch;
12448
+ const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
12449
+ const model = options.model ?? "gpt-4o-mini-tts";
12450
+ const voice2 = options.voice ?? "coral";
12451
+ return {
12452
+ kind: "tts",
12453
+ open: (openOptions) => {
12454
+ const listeners = {
12455
+ audio: new Set,
12456
+ close: new Set,
12457
+ error: new Set
12458
+ };
12459
+ const abortController = new AbortController;
12460
+ const signalAbort = () => abortController.abort();
12461
+ openOptions.signal?.addEventListener("abort", signalAbort, { once: true });
12462
+ let closed = false;
12463
+ return {
12464
+ close: async (reason) => {
12465
+ if (closed) {
12466
+ return;
12467
+ }
12468
+ closed = true;
12469
+ abortController.abort();
12470
+ openOptions.signal?.removeEventListener("abort", signalAbort);
12471
+ await emit(listeners, "close", {
12472
+ reason,
12473
+ type: "close"
12474
+ });
12475
+ },
12476
+ on: (event, handler) => {
12477
+ listeners[event].add(handler);
12478
+ return () => {
12479
+ listeners[event].delete(handler);
12480
+ };
12481
+ },
12482
+ send: async (text) => {
12483
+ if (closed || !text.trim()) {
12484
+ return;
12485
+ }
12486
+ try {
12487
+ const instructions = await resolveInstructions(options.instructions, {
12488
+ lexicon: openOptions.lexicon,
12489
+ sessionId: openOptions.sessionId,
12490
+ text
12491
+ });
12492
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/audio/speech`, {
12493
+ body: JSON.stringify({
12494
+ input: text,
12495
+ instructions,
12496
+ model,
12497
+ response_format: "pcm",
12498
+ speed: options.speed,
12499
+ voice: voice2
12500
+ }),
12501
+ headers: {
12502
+ authorization: `Bearer ${options.apiKey}`,
12503
+ "content-type": "application/json"
12504
+ },
12505
+ method: "POST",
12506
+ signal: abortController.signal
12507
+ });
12508
+ if (!response.ok) {
12509
+ throw createTTSHTTPError(response);
12510
+ }
12511
+ if (!response.body) {
12512
+ const chunk = new Uint8Array(await response.arrayBuffer());
12513
+ if (!closed && chunk.byteLength > 0) {
12514
+ await emit(listeners, "audio", {
12515
+ chunk,
12516
+ format: OPENAI_PCM24_FORMAT,
12517
+ receivedAt: Date.now(),
12518
+ type: "audio"
12519
+ });
12520
+ }
12521
+ return;
12522
+ }
12523
+ const reader = response.body.getReader();
12524
+ try {
12525
+ while (!closed) {
12526
+ const { done, value } = await reader.read();
12527
+ if (done) {
12528
+ break;
12529
+ }
12530
+ if (value.byteLength > 0) {
12531
+ await emit(listeners, "audio", {
12532
+ chunk: new Uint8Array(value),
12533
+ format: OPENAI_PCM24_FORMAT,
12534
+ receivedAt: Date.now(),
12535
+ type: "audio"
12536
+ });
12537
+ }
12538
+ }
12539
+ } finally {
12540
+ reader.releaseLock();
12541
+ }
12542
+ } catch (error) {
12543
+ if (closed || abortController.signal.aborted) {
12544
+ return;
12545
+ }
12546
+ await emit(listeners, "error", {
12547
+ error: error instanceof Error ? error : new Error(String(error)),
12548
+ recoverable: true,
12549
+ type: "error"
12550
+ });
12551
+ }
12552
+ }
12553
+ };
12554
+ }
12555
+ };
12556
+ };
12427
12557
  // src/providerAdapters.ts
12428
12558
  class VoiceIOProviderTimeoutError extends Error {
12429
12559
  provider;
@@ -12611,11 +12741,11 @@ var createResolver = (options) => {
12611
12741
  selectedProvider: preferred
12612
12742
  };
12613
12743
  };
12614
- const emit = async (event, input) => {
12744
+ const emit2 = async (event, input) => {
12615
12745
  await options.onProviderEvent?.(event, input);
12616
12746
  };
12617
12747
  return {
12618
- emit,
12748
+ emit: emit2,
12619
12749
  getSuppressionRemainingMs,
12620
12750
  providerIds,
12621
12751
  recordError,
@@ -16028,6 +16158,114 @@ var createTelnyxVoiceResponse = (options) => {
16028
16158
  ].filter((value) => Boolean(value)).join(" ");
16029
16159
  return `<?xml version="1.0" encoding="UTF-8"?><Response><Start><Stream ${attributes} /></Start></Response>`;
16030
16160
  };
16161
+ var parseTelnyxMessage = (raw) => {
16162
+ if (typeof raw !== "string") {
16163
+ return raw;
16164
+ }
16165
+ return JSON.parse(raw);
16166
+ };
16167
+ var normalizeTelnyxTrack = (track) => track === "outbound" || track === "outbound_track" ? "outbound" : "inbound";
16168
+ var telnyxToTwilioMessage = (message) => {
16169
+ switch (message.event) {
16170
+ case "connected":
16171
+ return {
16172
+ event: "connected",
16173
+ version: message.version
16174
+ };
16175
+ case "start": {
16176
+ const streamSid = message.stream_id ?? "telnyx-stream";
16177
+ return {
16178
+ event: "start",
16179
+ start: {
16180
+ callSid: message.start?.call_control_id ?? message.start?.call_session_id ?? message.start?.call_leg_id,
16181
+ customParameters: {
16182
+ ...message.start?.custom_parameters ?? {},
16183
+ ...message.start?.call_session_id ? { sessionId: message.start.call_session_id } : {}
16184
+ },
16185
+ mediaFormat: {
16186
+ channels: message.start?.media_format?.channels,
16187
+ encoding: message.start?.media_format?.encoding,
16188
+ sampleRate: message.start?.media_format?.sample_rate
16189
+ },
16190
+ streamSid
16191
+ },
16192
+ streamSid
16193
+ };
16194
+ }
16195
+ case "media": {
16196
+ const streamSid = message.stream_id ?? "telnyx-stream";
16197
+ return {
16198
+ event: "media",
16199
+ media: {
16200
+ chunk: message.media.chunk,
16201
+ payload: message.media.payload,
16202
+ timestamp: message.media.timestamp,
16203
+ track: normalizeTelnyxTrack(message.media.track)
16204
+ },
16205
+ streamSid
16206
+ };
16207
+ }
16208
+ case "mark":
16209
+ return {
16210
+ event: "mark",
16211
+ mark: message.mark,
16212
+ streamSid: message.stream_id ?? "telnyx-stream"
16213
+ };
16214
+ case "stop":
16215
+ return {
16216
+ event: "stop",
16217
+ stop: {
16218
+ callSid: message.stop?.call_control_id
16219
+ },
16220
+ streamSid: message.stream_id ?? "telnyx-stream"
16221
+ };
16222
+ case "dtmf":
16223
+ case "error":
16224
+ return null;
16225
+ }
16226
+ };
16227
+ var createTelnyxTwilioSocketAdapter = (socket) => ({
16228
+ close: (code, reason) => socket.close(code, reason),
16229
+ send: async (data) => {
16230
+ const message = JSON.parse(data);
16231
+ const telnyxMessage = message.event === "media" ? {
16232
+ event: "media",
16233
+ media: {
16234
+ payload: message.media.payload
16235
+ }
16236
+ } : message.event === "clear" ? {
16237
+ event: "clear"
16238
+ } : message.event === "mark" ? {
16239
+ event: "mark",
16240
+ mark: message.mark
16241
+ } : null;
16242
+ if (telnyxMessage) {
16243
+ await Promise.resolve(socket.send(JSON.stringify(telnyxMessage)));
16244
+ }
16245
+ }
16246
+ });
16247
+ var createTelnyxMediaStreamBridge = (socket, options) => {
16248
+ const bridge = createTwilioMediaStreamBridge(createTelnyxTwilioSocketAdapter(socket), {
16249
+ ...options,
16250
+ onVoiceMessage: options.onVoiceMessage ? (input) => options.onVoiceMessage?.({
16251
+ callControlId: input.callSid,
16252
+ message: input.message,
16253
+ sessionId: input.sessionId,
16254
+ streamId: input.streamSid
16255
+ }) : undefined
16256
+ });
16257
+ return {
16258
+ close: bridge.close,
16259
+ getSessionId: bridge.getSessionId,
16260
+ getStreamId: bridge.getStreamSid,
16261
+ handleMessage: async (raw) => {
16262
+ const message = telnyxToTwilioMessage(parseTelnyxMessage(raw));
16263
+ if (message) {
16264
+ await bridge.handleMessage(message);
16265
+ }
16266
+ }
16267
+ };
16268
+ };
16031
16269
  var decodeBase64 = (value) => Uint8Array.from(Buffer4.from(value, "base64"));
16032
16270
  var verifyVoiceTelnyxWebhookSignature = async (input) => {
16033
16271
  if (!input.publicKey) {
@@ -16182,6 +16420,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
16182
16420
  const webhookPath = options.webhook?.path ?? "/api/voice/telnyx/webhook";
16183
16421
  const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/telnyx/setup";
16184
16422
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/telnyx/smoke";
16423
+ const bridges = new WeakMap;
16185
16424
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
16186
16425
  const verify = options.webhook?.verify ?? (options.webhook?.publicKey ? (input) => verifyVoiceTelnyxWebhookSignature({
16187
16426
  body: input.rawBody,
@@ -16219,6 +16458,31 @@ var createTelnyxVoiceRoutes = (options = {}) => {
16219
16458
  "content-type": "text/xml; charset=utf-8"
16220
16459
  }
16221
16460
  });
16461
+ }).ws(streamPath, {
16462
+ close: async (ws, _code, reason) => {
16463
+ const bridge = bridges.get(ws);
16464
+ bridges.delete(ws);
16465
+ await bridge?.close(reason);
16466
+ },
16467
+ message: async (ws, raw) => {
16468
+ if (!options.bridge) {
16469
+ ws.close(1011, "Telnyx media bridge is not configured.");
16470
+ return;
16471
+ }
16472
+ let bridge = bridges.get(ws);
16473
+ if (!bridge) {
16474
+ bridge = createTelnyxMediaStreamBridge({
16475
+ close: (code, reason) => {
16476
+ ws.close(code, reason);
16477
+ },
16478
+ send: (data) => {
16479
+ ws.send(data);
16480
+ }
16481
+ }, options.bridge);
16482
+ bridges.set(ws, bridge);
16483
+ }
16484
+ await bridge.handleMessage(raw);
16485
+ }
16222
16486
  }).use(createVoiceTelephonyWebhookRoutes({
16223
16487
  ...options.webhook ?? {},
16224
16488
  context: options.context,
@@ -16327,6 +16591,123 @@ var createPlivoVoiceResponse = (options) => {
16327
16591
  const openTag = attributes ? `<Stream ${attributes}>` : "<Stream>";
16328
16592
  return `<?xml version="1.0" encoding="UTF-8"?><Response>${openTag}${escapeXml4(options.streamUrl)}</Stream></Response>`;
16329
16593
  };
16594
+ var parsePlivoMessage = (raw) => {
16595
+ if (typeof raw !== "string") {
16596
+ return raw;
16597
+ }
16598
+ return JSON.parse(raw);
16599
+ };
16600
+ var parsePlivoExtraHeaders = (headers) => {
16601
+ if (!headers) {
16602
+ return {};
16603
+ }
16604
+ return Object.fromEntries(headers.split(/[;,]/).map((header) => header.trim()).filter(Boolean).map((header) => {
16605
+ const separator = header.indexOf("=");
16606
+ if (separator === -1) {
16607
+ return [header, ""];
16608
+ }
16609
+ return [
16610
+ header.slice(0, separator).trim(),
16611
+ header.slice(separator + 1).trim()
16612
+ ];
16613
+ }).filter((entry) => (entry[0] ?? "").length > 0));
16614
+ };
16615
+ var plivoToTwilioMessage = (message) => {
16616
+ switch (message.event) {
16617
+ case "start": {
16618
+ const streamSid = message.streamId ?? message.start?.streamId ?? "plivo-stream";
16619
+ const callSid = message.start?.callId ?? message.start?.callUuid;
16620
+ const customParameters = parsePlivoExtraHeaders(message.start?.extra_headers);
16621
+ return {
16622
+ event: "start",
16623
+ start: {
16624
+ callSid,
16625
+ customParameters,
16626
+ mediaFormat: message.start?.mediaFormat,
16627
+ streamSid
16628
+ },
16629
+ streamSid
16630
+ };
16631
+ }
16632
+ case "media": {
16633
+ const streamSid = message.streamId ?? "plivo-stream";
16634
+ return {
16635
+ event: "media",
16636
+ media: {
16637
+ payload: message.media.payload,
16638
+ timestamp: message.media.timestamp,
16639
+ track: message.media.track ?? "inbound"
16640
+ },
16641
+ streamSid
16642
+ };
16643
+ }
16644
+ case "playedStream":
16645
+ return {
16646
+ event: "mark",
16647
+ mark: {
16648
+ name: message.name
16649
+ },
16650
+ streamSid: message.streamId ?? "plivo-stream"
16651
+ };
16652
+ case "stop":
16653
+ return {
16654
+ event: "stop",
16655
+ stop: {
16656
+ callSid: message.stop?.callId ?? message.stop?.callUuid
16657
+ },
16658
+ streamSid: message.streamId ?? "plivo-stream"
16659
+ };
16660
+ case "clearedAudio":
16661
+ case "dtmf":
16662
+ return null;
16663
+ }
16664
+ };
16665
+ var createPlivoTwilioSocketAdapter = (socket) => ({
16666
+ close: (code, reason) => socket.close(code, reason),
16667
+ send: async (data) => {
16668
+ const message = JSON.parse(data);
16669
+ const plivoMessage = message.event === "media" ? {
16670
+ event: "playAudio",
16671
+ media: {
16672
+ contentType: "audio/x-mulaw",
16673
+ payload: message.media.payload,
16674
+ sampleRate: 8000
16675
+ }
16676
+ } : message.event === "clear" ? {
16677
+ event: "clearAudio",
16678
+ streamId: message.streamSid
16679
+ } : message.event === "mark" ? {
16680
+ event: "checkpoint",
16681
+ name: message.mark.name,
16682
+ streamId: message.streamSid
16683
+ } : null;
16684
+ if (plivoMessage) {
16685
+ await Promise.resolve(socket.send(JSON.stringify(plivoMessage)));
16686
+ }
16687
+ }
16688
+ });
16689
+ var createPlivoMediaStreamBridge = (socket, options) => {
16690
+ const bridge = createTwilioMediaStreamBridge(createPlivoTwilioSocketAdapter(socket), {
16691
+ ...options,
16692
+ onVoiceMessage: options.onVoiceMessage ? (input) => options.onVoiceMessage?.({
16693
+ callId: input.callSid,
16694
+ message: input.message,
16695
+ sessionId: input.sessionId,
16696
+ streamId: input.streamSid
16697
+ }) : undefined
16698
+ });
16699
+ return {
16700
+ close: bridge.close,
16701
+ getSessionId: bridge.getSessionId,
16702
+ getStreamId: bridge.getStreamSid,
16703
+ handleMessage: async (raw) => {
16704
+ const message = plivoToTwilioMessage(parsePlivoMessage(raw));
16705
+ if (message) {
16706
+ await bridge.handleMessage(message);
16707
+ }
16708
+ }
16709
+ };
16710
+ };
16330
16711
  var toBase642 = (bytes) => Buffer5.from(new Uint8Array(bytes)).toString("base64");
16331
16712
  var timingSafeEqual3 = (left, right) => {
16332
16713
  const encoder = new TextEncoder;
@@ -16507,6 +16888,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
16507
16888
  const webhookPath = options.webhook?.path ?? "/api/voice/plivo/webhook";
16508
16889
  const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/plivo/setup";
16509
16890
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/plivo/smoke";
16891
+ const bridges = new WeakMap;
16510
16892
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
16511
16893
  const verificationUrl = options.webhook?.verificationUrl;
16512
16894
  const verify = options.webhook?.verify ?? (options.webhook?.authToken ? (input) => verifyVoicePlivoWebhookSignature({
@@ -16548,6 +16930,31 @@ var createPlivoVoiceRoutes = (options = {}) => {
16548
16930
  "content-type": "text/xml; charset=utf-8"
16549
16931
  }
16550
16932
  });
16933
+ }).ws(streamPath, {
16934
+ close: async (ws, _code, reason) => {
16935
+ const bridge = bridges.get(ws);
16936
+ bridges.delete(ws);
16937
+ await bridge?.close(reason);
16938
+ },
16939
+ message: async (ws, raw) => {
16940
+ if (!options.bridge) {
16941
+ ws.close(1011, "Plivo media bridge is not configured.");
16942
+ return;
16943
+ }
16944
+ let bridge = bridges.get(ws);
16945
+ if (!bridge) {
16946
+ bridge = createPlivoMediaStreamBridge({
16947
+ close: (code, reason) => {
16948
+ ws.close(code, reason);
16949
+ },
16950
+ send: (data) => {
16951
+ ws.send(data);
16952
+ }
16953
+ }, options.bridge);
16954
+ bridges.set(ws, bridge);
16955
+ }
16956
+ await bridge.handleMessage(raw);
16957
+ }
16551
16958
  }).use(createVoiceTelephonyWebhookRoutes({
16552
16959
  ...options.webhook ?? {},
16553
16960
  context: options.context,
@@ -17013,6 +17420,7 @@ export {
17013
17420
  createTwilioMediaStreamBridge,
17014
17421
  createTelnyxVoiceRoutes,
17015
17422
  createTelnyxVoiceResponse,
17423
+ createTelnyxMediaStreamBridge,
17016
17424
  createStoredVoiceOpsTask,
17017
17425
  createStoredVoiceIntegrationEvent,
17018
17426
  createStoredVoiceExternalObjectMap,
@@ -17020,7 +17428,9 @@ export {
17020
17428
  createRiskyTurnCorrectionHandler,
17021
17429
  createPlivoVoiceRoutes,
17022
17430
  createPlivoVoiceResponse,
17431
+ createPlivoMediaStreamBridge,
17023
17432
  createPhraseHintCorrectionHandler,
17433
+ createOpenAIVoiceTTS,
17024
17434
  createOpenAIVoiceAssistantModel,
17025
17435
  createMemoryVoiceTelephonyWebhookIdempotencyStore,
17026
17436
  createJSONVoiceAssistantModel,
@@ -0,0 +1,18 @@
1
+ import type { TTSAdapter, TTSAdapterOpenOptions, VoiceLexiconEntry } from './types';
2
+ export type OpenAIVoiceTTSVoice = 'alloy' | 'ash' | 'ballad' | 'cedar' | 'coral' | 'echo' | 'fable' | 'marin' | 'nova' | 'onyx' | 'sage' | 'shimmer' | 'verse' | (string & {});
3
+ export type OpenAIVoiceTTSOptions = {
4
+ apiKey: string;
5
+ baseUrl?: string;
6
+ fetch?: typeof fetch;
7
+ instructions?: string | ((input: {
8
+ lexicon?: VoiceLexiconEntry[];
9
+ sessionId: string;
10
+ text: string;
11
+ }) => Promise<string | undefined> | string | undefined);
12
+ model?: 'gpt-4o-mini-tts' | 'tts-1' | 'tts-1-hd' | (string & {});
13
+ speed?: number;
14
+ voice?: OpenAIVoiceTTSVoice | {
15
+ id: string;
16
+ };
17
+ };
18
+ export declare const createOpenAIVoiceTTS: (options: OpenAIVoiceTTSOptions) => TTSAdapter<TTSAdapterOpenOptions>;
@@ -1,7 +1,95 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type VoiceTelephonyContractReport, type VoiceTelephonySetupStatus, type VoiceTelephonySmokeCheck, type VoiceTelephonySmokeReport } from './contract';
3
3
  import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions, type VoiceTelephonyWebhookVerificationResult } from '../telephonyOutcome';
4
- import type { VoiceSessionRecord } from '../types';
4
+ import type { VoiceServerMessage, VoiceSessionRecord } from '../types';
5
+ import { type TwilioMediaStreamBridgeOptions } from './twilio';
6
+ export type PlivoInboundMessage = {
7
+ event: 'start';
8
+ sequenceNumber?: number;
9
+ start?: {
10
+ callId?: string;
11
+ callUuid?: string;
12
+ extra_headers?: string;
13
+ mediaFormat?: {
14
+ channels?: number;
15
+ encoding?: string;
16
+ sampleRate?: number;
17
+ };
18
+ streamId?: string;
19
+ };
20
+ streamId?: string;
21
+ } | {
22
+ event: 'media';
23
+ media: {
24
+ payload: string;
25
+ timestamp?: string;
26
+ track?: 'inbound' | 'outbound';
27
+ };
28
+ sequenceNumber?: number;
29
+ streamId?: string;
30
+ } | {
31
+ event: 'dtmf';
32
+ dtmf?: {
33
+ digit?: string;
34
+ timestamp?: string;
35
+ track?: string;
36
+ };
37
+ sequenceNumber?: number;
38
+ streamId?: string;
39
+ } | {
40
+ event: 'playedStream';
41
+ name?: string;
42
+ sequenceNumber?: number;
43
+ streamId?: string;
44
+ } | {
45
+ event: 'clearedAudio';
46
+ sequenceNumber?: number;
47
+ streamId?: string;
48
+ } | {
49
+ event: 'stop';
50
+ sequenceNumber?: number;
51
+ stop?: {
52
+ callId?: string;
53
+ callUuid?: string;
54
+ };
55
+ streamId?: string;
56
+ };
57
+ export type PlivoOutboundPlayAudioMessage = {
58
+ event: 'playAudio';
59
+ media: {
60
+ contentType: 'audio/x-mulaw';
61
+ payload: string;
62
+ sampleRate: 8000;
63
+ };
64
+ };
65
+ export type PlivoOutboundClearAudioMessage = {
66
+ event: 'clearAudio';
67
+ streamId?: string;
68
+ };
69
+ export type PlivoOutboundCheckpointMessage = {
70
+ event: 'checkpoint';
71
+ name: string;
72
+ streamId?: string;
73
+ };
74
+ export type PlivoOutboundMessage = PlivoOutboundPlayAudioMessage | PlivoOutboundClearAudioMessage | PlivoOutboundCheckpointMessage;
75
+ export type PlivoMediaStreamSocket = {
76
+ close: (code?: number, reason?: string) => void | Promise<void>;
77
+ send: (data: string) => void | Promise<void>;
78
+ };
79
+ export type PlivoMediaStreamBridgeOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Omit<TwilioMediaStreamBridgeOptions<TContext, TSession, TResult>, 'onVoiceMessage'> & {
80
+ onVoiceMessage?: (input: {
81
+ callId?: string;
82
+ message: VoiceServerMessage<TResult>;
83
+ sessionId: string;
84
+ streamId?: string;
85
+ }) => Promise<void> | void;
86
+ };
87
+ export type PlivoMediaStreamBridge = {
88
+ close: (reason?: string) => Promise<void>;
89
+ getSessionId: () => string | null;
90
+ getStreamId: () => string | null;
91
+ handleMessage: (raw: string | PlivoInboundMessage) => Promise<void>;
92
+ };
5
93
  export type PlivoVoiceResponseOptions = {
6
94
  audioTrack?: 'both' | 'inbound' | 'outbound';
7
95
  bidirectional?: boolean;
@@ -53,6 +141,7 @@ export type PlivoVoiceRoutesOptions<TContext = unknown, TSession extends VoiceSe
53
141
  streamPath: string;
54
142
  }) => Promise<string> | string);
55
143
  };
144
+ bridge?: PlivoMediaStreamBridgeOptions<TContext, TSession, TResult>;
56
145
  context?: TContext;
57
146
  name?: string;
58
147
  outcomePolicy?: VoiceTelephonyOutcomePolicy;
@@ -70,6 +159,7 @@ export type PlivoVoiceRoutesOptions<TContext = unknown, TSession extends VoiceSe
70
159
  };
71
160
  };
72
161
  export declare const createPlivoVoiceResponse: (options: PlivoVoiceResponseOptions) => string;
162
+ export declare const createPlivoMediaStreamBridge: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(socket: PlivoMediaStreamSocket, options: PlivoMediaStreamBridgeOptions<TContext, TSession, TResult>) => PlivoMediaStreamBridge;
73
163
  export declare const signVoicePlivoWebhook: (input: {
74
164
  authToken: string;
75
165
  body?: unknown;
@@ -121,6 +211,16 @@ export declare const createPlivoVoiceRoutes: <TContext = unknown, TSession exten
121
211
  };
122
212
  };
123
213
  };
214
+ } & {
215
+ [x: string]: {
216
+ subscribe: {
217
+ body: unknown;
218
+ params: {};
219
+ query: unknown;
220
+ headers: unknown;
221
+ response: {};
222
+ };
223
+ };
124
224
  } & {
125
225
  [x: string]: {
126
226
  post: {
@@ -1,7 +1,103 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type VoiceTelephonyContractReport, type VoiceTelephonySetupStatus, type VoiceTelephonySmokeCheck, type VoiceTelephonySmokeReport } from './contract';
3
3
  import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions, type VoiceTelephonyWebhookVerificationResult } from '../telephonyOutcome';
4
- import type { VoiceSessionRecord } from '../types';
4
+ import type { VoiceServerMessage, VoiceSessionRecord } from '../types';
5
+ import { type TwilioMediaStreamBridgeOptions } from './twilio';
6
+ export type TelnyxMediaPayload = {
7
+ chunk?: string;
8
+ payload: string;
9
+ timestamp?: string;
10
+ track?: 'inbound' | 'outbound' | 'inbound_track' | 'outbound_track';
11
+ };
12
+ export type TelnyxInboundMessage = {
13
+ event: 'connected';
14
+ version?: string;
15
+ } | {
16
+ event: 'start';
17
+ sequence_number?: string;
18
+ start?: {
19
+ call_control_id?: string;
20
+ call_leg_id?: string;
21
+ call_session_id?: string;
22
+ custom_parameters?: Record<string, string>;
23
+ media_format?: {
24
+ channels?: number;
25
+ encoding?: string;
26
+ sample_rate?: number;
27
+ };
28
+ user_id?: string;
29
+ };
30
+ stream_id?: string;
31
+ } | {
32
+ event: 'media';
33
+ media: TelnyxMediaPayload;
34
+ sequence_number?: string;
35
+ stream_id?: string;
36
+ } | {
37
+ event: 'mark';
38
+ mark?: {
39
+ name?: string;
40
+ };
41
+ sequence_number?: string;
42
+ stream_id?: string;
43
+ } | {
44
+ event: 'dtmf';
45
+ dtmf?: {
46
+ digit?: string;
47
+ };
48
+ sequence_number?: string;
49
+ stream_id?: string;
50
+ } | {
51
+ event: 'error';
52
+ payload?: {
53
+ code?: number;
54
+ detail?: string;
55
+ title?: string;
56
+ };
57
+ stream_id?: string;
58
+ } | {
59
+ event: 'stop';
60
+ sequence_number?: string;
61
+ stop?: {
62
+ call_control_id?: string;
63
+ user_id?: string;
64
+ };
65
+ stream_id?: string;
66
+ };
67
+ export type TelnyxOutboundMediaMessage = {
68
+ event: 'media';
69
+ media: {
70
+ payload: string;
71
+ };
72
+ };
73
+ export type TelnyxOutboundClearMessage = {
74
+ event: 'clear';
75
+ };
76
+ export type TelnyxOutboundMarkMessage = {
77
+ event: 'mark';
78
+ mark: {
79
+ name: string;
80
+ };
81
+ };
82
+ export type TelnyxOutboundMessage = TelnyxOutboundMediaMessage | TelnyxOutboundClearMessage | TelnyxOutboundMarkMessage;
83
+ export type TelnyxMediaStreamSocket = {
84
+ close: (code?: number, reason?: string) => void | Promise<void>;
85
+ send: (data: string) => void | Promise<void>;
86
+ };
87
+ export type TelnyxMediaStreamBridgeOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Omit<TwilioMediaStreamBridgeOptions<TContext, TSession, TResult>, 'onVoiceMessage'> & {
88
+ onVoiceMessage?: (input: {
89
+ callControlId?: string;
90
+ message: VoiceServerMessage<TResult>;
91
+ sessionId: string;
92
+ streamId?: string;
93
+ }) => Promise<void> | void;
94
+ };
95
+ export type TelnyxMediaStreamBridge = {
96
+ close: (reason?: string) => Promise<void>;
97
+ getSessionId: () => string | null;
98
+ getStreamId: () => string | null;
99
+ handleMessage: (raw: string | TelnyxInboundMessage) => Promise<void>;
100
+ };
5
101
  export type TelnyxVoiceResponseOptions = {
6
102
  bidirectionalCodec?: 'AMR-WB' | 'G722' | 'OPUS' | 'PCMA' | 'PCMU';
7
103
  bidirectionalMode?: 'mp3' | 'rtp';
@@ -38,6 +134,7 @@ export type TelnyxVoiceSmokeOptions = {
38
134
  title?: string;
39
135
  };
40
136
  export type TelnyxVoiceRoutesOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
137
+ bridge?: TelnyxMediaStreamBridgeOptions<TContext, TSession, TResult>;
41
138
  context?: TContext;
42
139
  name?: string;
43
140
  outcomePolicy?: VoiceTelephonyOutcomePolicy;
@@ -61,6 +158,7 @@ export type TelnyxVoiceRoutesOptions<TContext = unknown, TSession extends VoiceS
61
158
  };
62
159
  };
63
160
  export declare const createTelnyxVoiceResponse: (options: TelnyxVoiceResponseOptions) => string;
161
+ export declare const createTelnyxMediaStreamBridge: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(socket: TelnyxMediaStreamSocket, options: TelnyxMediaStreamBridgeOptions<TContext, TSession, TResult>) => TelnyxMediaStreamBridge;
64
162
  export declare const verifyVoiceTelnyxWebhookSignature: (input: {
65
163
  body: string;
66
164
  headers: Headers;
@@ -106,6 +204,16 @@ export declare const createTelnyxVoiceRoutes: <TContext = unknown, TSession exte
106
204
  };
107
205
  };
108
206
  };
207
+ } & {
208
+ [x: string]: {
209
+ subscribe: {
210
+ body: unknown;
211
+ params: {};
212
+ query: unknown;
213
+ headers: unknown;
214
+ response: {};
215
+ };
216
+ };
109
217
  } & {
110
218
  [x: string]: {
111
219
  post: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.81",
3
+ "version": "0.0.22-beta.83",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",