@openclaw/voice-call 2026.5.12 → 2026.5.14-beta.2
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/{config-Beumk4ED.js → config-C8gX5Cik.js} +1 -1
- package/dist/{config-compat-BSV2aOY6.js → config-compat-DJqJ8NzH.js} +1 -1
- package/dist/{guarded-json-api-C5TNOqhE.js → guarded-json-api-D_aM8XmA.js} +7 -2
- package/dist/index.js +3 -3
- package/dist/{plivo-AW4Op9oA.js → plivo--HUS8UBT.js} +2 -3
- package/dist/{realtime-handler-B_aqJXZj.js → realtime-handler-Cj_3954k.js} +233 -60
- package/dist/{response-generator-splfS1mU.js → response-generator-ZeNpwsyL.js} +2 -2
- package/dist/{runtime-entry-CQfEI6TJ.js → runtime-entry-ox4PGoxT.js} +146 -33
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +1 -1
- package/dist/{telnyx-DPwKir7b.js → telnyx-Df3IfYAS.js} +40 -22
- package/dist/{twilio-D-QbSGSk.js → twilio-B3zpyWY5.js} +8 -5
- package/package.json +5 -5
- package/dist/call-status-CuIeqfac.js +0 -32
- package/dist/http-headers-B5L5gMpK.js +0 -10
- package/dist/response-model-CyF5K80p.js +0 -12
- package/dist/voice-mapping-DMm-YvxM.js +0 -37
- /package/dist/{mock-0CEjFJi5.js → mock-BvTP8Rmx.js} +0 -0
- /package/dist/{realtime-transcription.runtime-B2h70y2W.js → realtime-transcription.runtime-CuOCFrMc.js} +0 -0
- /package/dist/{realtime-voice.runtime-Bkh4nvLn.js → realtime-voice.runtime-BM0lAf0B.js} +0 -0
|
@@ -611,7 +611,7 @@ function validateProviderConfig(config) {
|
|
|
611
611
|
}
|
|
612
612
|
if (config.realtime.enabled && config.inboundPolicy === "disabled") errors.push("plugins.entries.voice-call.config.inboundPolicy must not be \"disabled\" when realtime.enabled is true");
|
|
613
613
|
if (config.realtime.enabled && config.streaming.enabled) errors.push("plugins.entries.voice-call.config.realtime.enabled and plugins.entries.voice-call.config.streaming.enabled cannot both be true");
|
|
614
|
-
if (config.realtime.enabled && config.provider && config.provider !== "twilio") errors.push("plugins.entries.voice-call.config.provider must be \"twilio\" when realtime.enabled is true");
|
|
614
|
+
if (config.realtime.enabled && config.provider && config.provider !== "twilio" && config.provider !== "telnyx") errors.push("plugins.entries.voice-call.config.provider must be \"twilio\" or \"telnyx\" when realtime.enabled is true");
|
|
615
615
|
return {
|
|
616
616
|
valid: errors.length === 0,
|
|
617
617
|
errors
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as VoiceCallConfigSchema } from "./config-
|
|
1
|
+
import { t as VoiceCallConfigSchema } from "./config-C8gX5Cik.js";
|
|
2
2
|
import { asOptionalRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
//#region extensions/voice-call/src/config-compat.ts
|
|
4
4
|
const VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION = "2026.6.0";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import {
|
|
3
|
+
import { a as getHeader } from "./runtime-entry-ox4PGoxT.js";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
6
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -577,7 +577,12 @@ async function guardedJsonApiRequest(params) {
|
|
|
577
577
|
throw new Error(`${params.errorPrefix}: ${response.status} ${errorText}`);
|
|
578
578
|
}
|
|
579
579
|
const text = await response.text();
|
|
580
|
-
|
|
580
|
+
if (!text) return;
|
|
581
|
+
try {
|
|
582
|
+
return JSON.parse(text);
|
|
583
|
+
} catch {
|
|
584
|
+
throw new Error(`${params.errorPrefix}: malformed JSON response`);
|
|
585
|
+
}
|
|
581
586
|
} finally {
|
|
582
587
|
await release();
|
|
583
588
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-
|
|
4
|
-
import {
|
|
5
|
-
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-
|
|
3
|
+
import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-C8gX5Cik.js";
|
|
4
|
+
import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-ox4PGoxT.js";
|
|
5
|
+
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-DJqJ8NzH.js";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
7
|
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
8
8
|
import { normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
3
|
-
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5TNOqhE.js";
|
|
1
|
+
import { a as getHeader, m as escapeXml } from "./runtime-entry-ox4PGoxT.js";
|
|
2
|
+
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-D_aM8XmA.js";
|
|
4
3
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
4
|
import crypto from "node:crypto";
|
|
6
5
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -13,7 +13,7 @@ const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
|
|
|
13
13
|
const PCM16_MAX_AMPLITUDE = 32768;
|
|
14
14
|
const MULAW_LINEAR_SAMPLES = new Int16Array(256);
|
|
15
15
|
for (let i = 0; i < MULAW_LINEAR_SAMPLES.length; i += 1) MULAW_LINEAR_SAMPLES[i] = decodeMulawSample(i);
|
|
16
|
-
var
|
|
16
|
+
var RealtimeAudioPacer = class {
|
|
17
17
|
constructor(params) {
|
|
18
18
|
this.params = params;
|
|
19
19
|
this.queue = [];
|
|
@@ -53,10 +53,7 @@ var RealtimeTwilioAudioPacer = class {
|
|
|
53
53
|
this.clearTimer();
|
|
54
54
|
this.queue = [];
|
|
55
55
|
this.queuedAudioBytes = 0;
|
|
56
|
-
this.params.
|
|
57
|
-
event: "clear",
|
|
58
|
-
streamSid: this.params.streamSid
|
|
59
|
-
});
|
|
56
|
+
this.params.send(this.params.serializer.clear());
|
|
60
57
|
return clearedAudioBytes;
|
|
61
58
|
}
|
|
62
59
|
close() {
|
|
@@ -86,17 +83,9 @@ var RealtimeTwilioAudioPacer = class {
|
|
|
86
83
|
let sent = true;
|
|
87
84
|
if (item.type === "audio") {
|
|
88
85
|
this.queuedAudioBytes = Math.max(0, this.queuedAudioBytes - item.chunk.length);
|
|
89
|
-
sent = this.params.
|
|
90
|
-
event: "media",
|
|
91
|
-
streamSid: this.params.streamSid,
|
|
92
|
-
media: { payload: item.chunk.toString("base64") }
|
|
93
|
-
});
|
|
86
|
+
sent = this.params.send(this.params.serializer.media(item.chunk.toString("base64")));
|
|
94
87
|
delayMs = item.durationMs || TELEPHONY_CHUNK_MS;
|
|
95
|
-
} else sent = this.params.
|
|
96
|
-
event: "mark",
|
|
97
|
-
streamSid: this.params.streamSid,
|
|
98
|
-
mark: { name: item.name }
|
|
99
|
-
});
|
|
88
|
+
} else sent = this.params.send(this.params.serializer.mark(item.name));
|
|
100
89
|
if (!sent) {
|
|
101
90
|
this.queue = [];
|
|
102
91
|
this.queuedAudioBytes = 0;
|
|
@@ -148,6 +137,156 @@ function decodeMulawSample(value) {
|
|
|
148
137
|
return sign ? -sample : sample;
|
|
149
138
|
}
|
|
150
139
|
//#endregion
|
|
140
|
+
//#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
|
|
141
|
+
function parseTimestampMs(value) {
|
|
142
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
143
|
+
if (typeof value === "string") {
|
|
144
|
+
const parsed = Number.parseInt(value, 10);
|
|
145
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function tryParseJson(rawMessage) {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(rawMessage);
|
|
151
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
152
|
+
} catch {}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function normalizeBase64ForCompare(value) {
|
|
156
|
+
return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
|
|
157
|
+
}
|
|
158
|
+
function isValidBase64Payload(value) {
|
|
159
|
+
return normalizeBase64ForCompare(Buffer.from(value, "base64").toString("base64")) === normalizeBase64ForCompare(value);
|
|
160
|
+
}
|
|
161
|
+
var TwilioStreamFrameAdapter = class {
|
|
162
|
+
constructor() {
|
|
163
|
+
this.providerName = "twilio";
|
|
164
|
+
this.streamSid = "";
|
|
165
|
+
}
|
|
166
|
+
parseInbound(rawMessage) {
|
|
167
|
+
const msg = tryParseJson(rawMessage);
|
|
168
|
+
if (!msg) return { kind: "ignored" };
|
|
169
|
+
const event = msg.event;
|
|
170
|
+
if (event === "start") {
|
|
171
|
+
const startData = typeof msg.start === "object" && msg.start !== null ? msg.start : void 0;
|
|
172
|
+
const streamSid = typeof startData?.streamSid === "string" ? startData.streamSid : "";
|
|
173
|
+
const callSid = typeof startData?.callSid === "string" ? startData.callSid : "";
|
|
174
|
+
if (!streamSid || !callSid) return { kind: "ignored" };
|
|
175
|
+
this.streamSid = streamSid;
|
|
176
|
+
return {
|
|
177
|
+
kind: "start",
|
|
178
|
+
streamId: streamSid,
|
|
179
|
+
providerCallId: callSid
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (event === "media") {
|
|
183
|
+
const mediaData = typeof msg.media === "object" && msg.media !== null ? msg.media : void 0;
|
|
184
|
+
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
185
|
+
if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
|
|
186
|
+
return {
|
|
187
|
+
kind: "media",
|
|
188
|
+
payloadBase64: payload,
|
|
189
|
+
timestampMs: parseTimestampMs(mediaData?.timestamp),
|
|
190
|
+
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (event === "mark") {
|
|
194
|
+
const markData = typeof msg.mark === "object" && msg.mark !== null ? msg.mark : void 0;
|
|
195
|
+
return {
|
|
196
|
+
kind: "mark",
|
|
197
|
+
name: typeof markData?.name === "string" ? markData.name : void 0
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (event === "stop") return { kind: "stop" };
|
|
201
|
+
return { kind: "ignored" };
|
|
202
|
+
}
|
|
203
|
+
serializeMedia(payloadBase64) {
|
|
204
|
+
return JSON.stringify({
|
|
205
|
+
event: "media",
|
|
206
|
+
streamSid: this.streamSid,
|
|
207
|
+
media: { payload: payloadBase64 }
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
serializeClear() {
|
|
211
|
+
return JSON.stringify({
|
|
212
|
+
event: "clear",
|
|
213
|
+
streamSid: this.streamSid
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
serializeMark(name) {
|
|
217
|
+
return JSON.stringify({
|
|
218
|
+
event: "mark",
|
|
219
|
+
streamSid: this.streamSid,
|
|
220
|
+
mark: { name }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var TelnyxStreamFrameAdapter = class {
|
|
225
|
+
constructor() {
|
|
226
|
+
this.providerName = "telnyx";
|
|
227
|
+
}
|
|
228
|
+
parseInbound(rawMessage) {
|
|
229
|
+
const msg = tryParseJson(rawMessage);
|
|
230
|
+
if (!msg) return { kind: "ignored" };
|
|
231
|
+
const event = msg.event;
|
|
232
|
+
const topLevelStreamId = typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : void 0;
|
|
233
|
+
if (event === "start") {
|
|
234
|
+
const startData = typeof msg.start === "object" && msg.start !== null ? msg.start : void 0;
|
|
235
|
+
const providerCallId = typeof startData?.call_control_id === "string" && startData.call_control_id ? startData.call_control_id : void 0;
|
|
236
|
+
if (!topLevelStreamId || !providerCallId) return { kind: "ignored" };
|
|
237
|
+
return {
|
|
238
|
+
kind: "start",
|
|
239
|
+
streamId: topLevelStreamId,
|
|
240
|
+
providerCallId
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (event === "media") {
|
|
244
|
+
const mediaData = typeof msg.media === "object" && msg.media !== null ? msg.media : void 0;
|
|
245
|
+
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
246
|
+
if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
|
|
247
|
+
return {
|
|
248
|
+
kind: "media",
|
|
249
|
+
payloadBase64: payload,
|
|
250
|
+
timestampMs: parseTimestampMs(mediaData?.timestamp),
|
|
251
|
+
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (event === "mark") {
|
|
255
|
+
const markData = typeof msg.mark === "object" && msg.mark !== null ? msg.mark : void 0;
|
|
256
|
+
return {
|
|
257
|
+
kind: "mark",
|
|
258
|
+
name: typeof markData?.name === "string" ? markData.name : void 0
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (event === "stop") return { kind: "stop" };
|
|
262
|
+
if (event === "error") {
|
|
263
|
+
const errorData = typeof msg.payload === "object" && msg.payload !== null ? msg.payload : void 0;
|
|
264
|
+
return {
|
|
265
|
+
kind: "error",
|
|
266
|
+
code: typeof errorData?.code === "string" || typeof errorData?.code === "number" ? String(errorData.code) : void 0,
|
|
267
|
+
title: typeof errorData?.title === "string" ? errorData.title : void 0,
|
|
268
|
+
detail: typeof errorData?.detail === "string" ? errorData.detail : void 0
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { kind: "ignored" };
|
|
272
|
+
}
|
|
273
|
+
serializeMedia(payloadBase64) {
|
|
274
|
+
return JSON.stringify({
|
|
275
|
+
event: "media",
|
|
276
|
+
media: { payload: payloadBase64 }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
serializeClear() {
|
|
280
|
+
return JSON.stringify({ event: "clear" });
|
|
281
|
+
}
|
|
282
|
+
serializeMark(name) {
|
|
283
|
+
return JSON.stringify({
|
|
284
|
+
event: "mark",
|
|
285
|
+
mark: { name }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
//#endregion
|
|
151
290
|
//#region extensions/voice-call/src/webhook/realtime-handler.ts
|
|
152
291
|
const STREAM_TOKEN_TTL_MS = 3e4;
|
|
153
292
|
const DEFAULT_HOST = "localhost:8443";
|
|
@@ -318,23 +457,29 @@ var RealtimeCallHandler = class {
|
|
|
318
457
|
return `${this.publicPathPrefix}${normalizePath(this.config.streamPath ?? "/voice/stream/realtime")}`;
|
|
319
458
|
}
|
|
320
459
|
buildTwiMLPayload(req, params) {
|
|
321
|
-
const host = this.publicOrigin || req.headers.host || DEFAULT_HOST;
|
|
322
460
|
const rawDirection = params?.get("Direction");
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
461
|
+
const previousOrigin = this.publicOrigin;
|
|
462
|
+
if (!previousOrigin) this.publicOrigin = req.headers.host ?? DEFAULT_HOST;
|
|
463
|
+
try {
|
|
464
|
+
const { streamUrl } = this.issueStreamSession({
|
|
465
|
+
providerName: "twilio",
|
|
466
|
+
from: params?.get("From") ?? void 0,
|
|
467
|
+
to: params?.get("To") ?? void 0,
|
|
468
|
+
direction: rawDirection?.startsWith("outbound") ? "outbound" : "inbound"
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
statusCode: 200,
|
|
472
|
+
headers: { "Content-Type": "text/xml" },
|
|
473
|
+
body: `<?xml version="1.0" encoding="UTF-8"?>
|
|
332
474
|
<Response>
|
|
333
475
|
<Connect>
|
|
334
|
-
<Stream url="${
|
|
476
|
+
<Stream url="${streamUrl}" />
|
|
335
477
|
</Connect>
|
|
336
478
|
</Response>`
|
|
337
|
-
|
|
479
|
+
};
|
|
480
|
+
} finally {
|
|
481
|
+
this.publicOrigin = previousOrigin;
|
|
482
|
+
}
|
|
338
483
|
}
|
|
339
484
|
handleWebSocketUpgrade(request, socket, head) {
|
|
340
485
|
const token = new URL(request.url ?? "/", "wss://localhost").pathname.split("/").pop() ?? null;
|
|
@@ -344,6 +489,7 @@ var RealtimeCallHandler = class {
|
|
|
344
489
|
socket.destroy();
|
|
345
490
|
return;
|
|
346
491
|
}
|
|
492
|
+
const adapter = (callerMeta.providerName ?? "twilio") === "telnyx" ? new TelnyxStreamFrameAdapter() : new TwilioStreamFrameAdapter();
|
|
347
493
|
new WebSocketServer({
|
|
348
494
|
noServer: true,
|
|
349
495
|
maxPayload: MAX_REALTIME_MESSAGE_BYTES
|
|
@@ -356,43 +502,44 @@ var RealtimeCallHandler = class {
|
|
|
356
502
|
let lastMediaGapWarnAt = 0;
|
|
357
503
|
ws.on("message", (data) => {
|
|
358
504
|
try {
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
505
|
+
const frame = adapter.parseInbound(data.toString());
|
|
506
|
+
if (frame.kind === "ignored") return;
|
|
507
|
+
if (frame.kind === "start") {
|
|
508
|
+
if (initialized) return;
|
|
361
509
|
initialized = true;
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
const callSid = typeof startData?.callSid === "string" ? startData.callSid : "unknown";
|
|
365
|
-
activeCallSid = callSid;
|
|
366
|
-
const nextBridge = this.handleCall(streamSid, callSid, ws, callerMeta);
|
|
510
|
+
activeCallSid = frame.providerCallId;
|
|
511
|
+
const nextBridge = this.handleCall(frame.streamId, frame.providerCallId, ws, callerMeta, adapter);
|
|
367
512
|
if (!nextBridge) return;
|
|
368
513
|
bridge = nextBridge;
|
|
369
514
|
return;
|
|
370
515
|
}
|
|
371
516
|
if (!bridge) return;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const audio = Buffer.from(mediaData.payload, "base64");
|
|
517
|
+
if (frame.kind === "media") {
|
|
518
|
+
const audio = Buffer.from(frame.payloadBase64, "base64");
|
|
375
519
|
bridge.sendAudio(audio);
|
|
376
|
-
|
|
377
|
-
if (Number.isFinite(mediaTimestamp)) {
|
|
520
|
+
if (frame.timestampMs !== void 0) {
|
|
378
521
|
if (lastMediaTimestamp !== void 0) {
|
|
379
|
-
const gapMs =
|
|
522
|
+
const gapMs = frame.timestampMs - lastMediaTimestamp;
|
|
380
523
|
const now = Date.now();
|
|
381
524
|
if ((gapMs > 120 || gapMs < 0) && now - lastMediaGapWarnAt > 5e3) {
|
|
382
525
|
lastMediaGapWarnAt = now;
|
|
383
|
-
console.warn(`[voice-call] realtime media timestamp gap providerCallId=${activeCallSid} gapMs=${gapMs} timestamp=${
|
|
526
|
+
console.warn(`[voice-call] realtime media timestamp gap providerCallId=${activeCallSid} gapMs=${gapMs} timestamp=${frame.timestampMs}`);
|
|
384
527
|
}
|
|
385
528
|
}
|
|
386
|
-
lastMediaTimestamp =
|
|
387
|
-
bridge.setMediaTimestamp(
|
|
529
|
+
lastMediaTimestamp = frame.timestampMs;
|
|
530
|
+
bridge.setMediaTimestamp(frame.timestampMs);
|
|
388
531
|
}
|
|
389
532
|
return;
|
|
390
533
|
}
|
|
391
|
-
if (
|
|
534
|
+
if (frame.kind === "mark") {
|
|
392
535
|
bridge.acknowledgeMark();
|
|
393
536
|
return;
|
|
394
537
|
}
|
|
395
|
-
if (
|
|
538
|
+
if (frame.kind === "error") {
|
|
539
|
+
console.error(`[voice-call] realtime WS error frame providerCallId=${activeCallSid} code=${frame.code ?? "?"} title=${frame.title ?? ""} detail=${frame.detail ?? ""}`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (frame.kind === "stop") {
|
|
396
543
|
stopReceived = true;
|
|
397
544
|
this.closeTelephonyBridge(activeCallSid, bridge, "completed");
|
|
398
545
|
}
|
|
@@ -428,6 +575,19 @@ var RealtimeCallHandler = class {
|
|
|
428
575
|
};
|
|
429
576
|
}
|
|
430
577
|
}
|
|
578
|
+
issueStreamSession(request = {}) {
|
|
579
|
+
const token = this.issueStreamToken({
|
|
580
|
+
providerName: request.providerName ?? "twilio",
|
|
581
|
+
callId: request.callId,
|
|
582
|
+
from: request.from,
|
|
583
|
+
to: request.to,
|
|
584
|
+
direction: request.direction
|
|
585
|
+
});
|
|
586
|
+
return {
|
|
587
|
+
token,
|
|
588
|
+
streamUrl: `wss://${this.publicOrigin || DEFAULT_HOST}${this.getStreamPathPattern()}/${token}`
|
|
589
|
+
};
|
|
590
|
+
}
|
|
431
591
|
issueStreamToken(meta = {}) {
|
|
432
592
|
const token = randomUUID();
|
|
433
593
|
this.pendingStreamTokens.set(token, {
|
|
@@ -445,10 +605,12 @@ var RealtimeCallHandler = class {
|
|
|
445
605
|
return {
|
|
446
606
|
from: entry.from,
|
|
447
607
|
to: entry.to,
|
|
448
|
-
direction: entry.direction
|
|
608
|
+
direction: entry.direction,
|
|
609
|
+
providerName: entry.providerName,
|
|
610
|
+
callId: entry.callId
|
|
449
611
|
};
|
|
450
612
|
}
|
|
451
|
-
handleCall(streamSid, callSid, ws, callerMeta) {
|
|
613
|
+
handleCall(streamSid, callSid, ws, callerMeta, adapter) {
|
|
452
614
|
const registration = this.registerCallInManager(callSid, callerMeta);
|
|
453
615
|
if (!registration) {
|
|
454
616
|
ws.close(1008, "Caller rejected by policy");
|
|
@@ -508,14 +670,14 @@ var RealtimeCallHandler = class {
|
|
|
508
670
|
callEndEmitted = true;
|
|
509
671
|
this.endCallInManager(callSid, callId, reason);
|
|
510
672
|
};
|
|
511
|
-
const
|
|
673
|
+
const sendString = (message) => {
|
|
512
674
|
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
513
675
|
if (ws.bufferedAmount > MAX_REALTIME_WS_BUFFERED_BYTES) {
|
|
514
676
|
console.warn(`[voice-call] realtime outbound websocket backpressure before send callId=${callId} providerCallId=${callSid} bufferedBytes=${ws.bufferedAmount}`);
|
|
515
677
|
ws.close(1013, "Backpressure: send buffer exceeded");
|
|
516
678
|
return false;
|
|
517
679
|
}
|
|
518
|
-
ws.send(
|
|
680
|
+
ws.send(message);
|
|
519
681
|
if (ws.bufferedAmount > MAX_REALTIME_WS_BUFFERED_BYTES) {
|
|
520
682
|
console.warn(`[voice-call] realtime outbound websocket backpressure after send callId=${callId} providerCallId=${callSid} bufferedBytes=${ws.bufferedAmount}`);
|
|
521
683
|
ws.close(1013, "Backpressure: send buffer exceeded");
|
|
@@ -523,9 +685,13 @@ var RealtimeCallHandler = class {
|
|
|
523
685
|
}
|
|
524
686
|
return true;
|
|
525
687
|
};
|
|
526
|
-
const audioPacer = new
|
|
527
|
-
|
|
528
|
-
|
|
688
|
+
const audioPacer = new RealtimeAudioPacer({
|
|
689
|
+
send: sendString,
|
|
690
|
+
serializer: {
|
|
691
|
+
media: (payload) => adapter.serializeMedia(payload),
|
|
692
|
+
clear: () => adapter.serializeClear(),
|
|
693
|
+
mark: (name) => adapter.serializeMark(name)
|
|
694
|
+
},
|
|
529
695
|
onBackpressure: () => {
|
|
530
696
|
console.warn(`[voice-call] realtime paced audio backpressure callId=${callId} providerCallId=${callSid}`);
|
|
531
697
|
if (ws.readyState === WebSocket.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
|
|
@@ -915,20 +1081,14 @@ var RealtimeCallHandler = class {
|
|
|
915
1081
|
...callerMeta.from ? { from: callerMeta.from } : {},
|
|
916
1082
|
...callerMeta.to ? { to: callerMeta.to } : {}
|
|
917
1083
|
};
|
|
918
|
-
this.
|
|
919
|
-
id: `realtime-initiated-${callSid}`,
|
|
920
|
-
callId: callSid,
|
|
921
|
-
type: "call.initiated",
|
|
922
|
-
...baseFields
|
|
923
|
-
});
|
|
924
|
-
const callRecord = this.manager.getCallByProviderCallId(callSid);
|
|
1084
|
+
const callRecord = this.resolveRealtimeCall(callSid, callerMeta, baseFields);
|
|
925
1085
|
if (!callRecord) return null;
|
|
926
1086
|
const initialGreeting = this.extractInitialGreeting(callRecord);
|
|
927
1087
|
console.log(`[voice-call] Realtime call ${callRecord.callId} initial greeting ${initialGreeting ? "queued" : "absent"}`);
|
|
928
1088
|
if (callRecord.metadata) delete callRecord.metadata.initialMessage;
|
|
929
1089
|
this.manager.processEvent({
|
|
930
1090
|
id: `realtime-answered-${callSid}`,
|
|
931
|
-
callId:
|
|
1091
|
+
callId: callRecord.callId,
|
|
932
1092
|
type: "call.answered",
|
|
933
1093
|
...baseFields
|
|
934
1094
|
});
|
|
@@ -937,6 +1097,19 @@ var RealtimeCallHandler = class {
|
|
|
937
1097
|
initialGreetingInstructions: buildGreetingInstructions(this.config.instructions, initialGreeting)
|
|
938
1098
|
};
|
|
939
1099
|
}
|
|
1100
|
+
resolveRealtimeCall(callSid, callerMeta, baseFields) {
|
|
1101
|
+
if (callerMeta.callId) {
|
|
1102
|
+
const call = this.manager.getCall(callerMeta.callId);
|
|
1103
|
+
return call?.providerCallId === callSid ? call : null;
|
|
1104
|
+
}
|
|
1105
|
+
this.manager.processEvent({
|
|
1106
|
+
id: `realtime-initiated-${callSid}`,
|
|
1107
|
+
callId: callSid,
|
|
1108
|
+
type: "call.initiated",
|
|
1109
|
+
...baseFields
|
|
1110
|
+
});
|
|
1111
|
+
return this.manager.getCallByProviderCallId(callSid) ?? null;
|
|
1112
|
+
}
|
|
940
1113
|
extractInitialGreeting(call) {
|
|
941
1114
|
return typeof call.metadata?.initialMessage === "string" ? call.metadata.initialMessage : void 0;
|
|
942
1115
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as resolveVoiceCallSessionKey } from "./config-
|
|
2
|
-
import {
|
|
1
|
+
import { o as resolveVoiceCallSessionKey } from "./config-C8gX5Cik.js";
|
|
2
|
+
import { f as resolveVoiceResponseModel } from "./runtime-entry-ox4PGoxT.js";
|
|
3
3
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-
|
|
4
|
-
import { n as mapVoiceToPolly, t as escapeXml } from "./voice-mapping-DMm-YvxM.js";
|
|
5
|
-
import { t as resolveVoiceResponseModel } from "./response-model-CyF5K80p.js";
|
|
6
|
-
import { a as convertPcmToMulaw8k, t as isProviderStatusTerminal } from "./call-status-CuIeqfac.js";
|
|
7
|
-
import { t as getHeader } from "./http-headers-B5L5gMpK.js";
|
|
3
|
+
import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-C8gX5Cik.js";
|
|
8
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
9
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
10
|
-
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
11
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultPolicyInstructions, consultRealtimeVoiceAgent, createTalkSessionController, recordTalkObservabilityEvent, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow, resolveRealtimeVoiceFastContextConsult } from "openclaw/plugin-sdk/realtime-voice";
|
|
6
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultPolicyInstructions, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createTalkSessionController, recordTalkObservabilityEvent, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow, resolveRealtimeVoiceFastContextConsult } from "openclaw/plugin-sdk/realtime-voice";
|
|
12
8
|
import { z } from "zod";
|
|
13
9
|
import fs from "node:fs";
|
|
14
10
|
import os from "node:os";
|
|
@@ -351,6 +347,41 @@ function resolvePreferredTtsVoice(config) {
|
|
|
351
347
|
return resolveProviderVoiceSetting(config.tts?.providers?.[providerId]);
|
|
352
348
|
}
|
|
353
349
|
//#endregion
|
|
350
|
+
//#region extensions/voice-call/src/voice-mapping.ts
|
|
351
|
+
/**
|
|
352
|
+
* Escape XML special characters for TwiML and other XML responses.
|
|
353
|
+
*/
|
|
354
|
+
function escapeXml(text) {
|
|
355
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Map of OpenAI voice names to similar Twilio Polly voices.
|
|
359
|
+
*/
|
|
360
|
+
const OPENAI_TO_POLLY_MAP = {
|
|
361
|
+
alloy: "Polly.Joanna",
|
|
362
|
+
echo: "Polly.Matthew",
|
|
363
|
+
fable: "Polly.Amy",
|
|
364
|
+
onyx: "Polly.Brian",
|
|
365
|
+
nova: "Polly.Salli",
|
|
366
|
+
shimmer: "Polly.Kimberly"
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* Default Polly voice when no mapping is found.
|
|
370
|
+
*/
|
|
371
|
+
const DEFAULT_POLLY_VOICE = "Polly.Joanna";
|
|
372
|
+
/**
|
|
373
|
+
* Map OpenAI voice names to Twilio Polly equivalents.
|
|
374
|
+
* Falls through if already a valid Polly/Google voice.
|
|
375
|
+
*
|
|
376
|
+
* @param voice - OpenAI voice name (alloy, echo, etc.) or Polly voice name
|
|
377
|
+
* @returns Polly voice name suitable for Twilio TwiML
|
|
378
|
+
*/
|
|
379
|
+
function mapVoiceToPolly(voice) {
|
|
380
|
+
if (!voice) return DEFAULT_POLLY_VOICE;
|
|
381
|
+
if (voice.startsWith("Polly.") || voice.startsWith("Google.")) return voice;
|
|
382
|
+
return OPENAI_TO_POLLY_MAP[normalizeLowercaseStringOrEmpty(voice)] || "Polly.Joanna";
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
354
385
|
//#region extensions/voice-call/src/manager/twiml.ts
|
|
355
386
|
function generateNotifyTwiml(message, voice) {
|
|
356
387
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -485,13 +516,24 @@ async function initiateCall(ctx, to, sessionKey, options) {
|
|
|
485
516
|
preConnectTwiml = generateDtmfRedirectTwiml(dtmfSequence, ctx.webhookUrl);
|
|
486
517
|
console.log(`[voice-call] Using pre-connect DTMF TwiML for call ${callId} (digits=${dtmfSequence.length}, initialMessage=${initialMessage ? "yes" : "no"})`);
|
|
487
518
|
}
|
|
519
|
+
const streamSession = ctx.config.realtime?.enabled && ctx.provider.name === "telnyx" && ctx.streamSessionIssuer ? ctx.streamSessionIssuer({
|
|
520
|
+
providerName: "telnyx",
|
|
521
|
+
callId,
|
|
522
|
+
from,
|
|
523
|
+
to,
|
|
524
|
+
direction: "outbound"
|
|
525
|
+
}) : void 0;
|
|
488
526
|
const result = await ctx.provider.initiateCall({
|
|
489
527
|
callId,
|
|
490
528
|
from,
|
|
491
529
|
to,
|
|
492
530
|
webhookUrl: ctx.webhookUrl,
|
|
493
531
|
inlineTwiml,
|
|
494
|
-
preConnectTwiml
|
|
532
|
+
preConnectTwiml,
|
|
533
|
+
...streamSession ? {
|
|
534
|
+
streamUrl: streamSession.streamUrl,
|
|
535
|
+
streamAuthToken: streamSession.token
|
|
536
|
+
} : {}
|
|
495
537
|
});
|
|
496
538
|
callRecord.providerCallId = result.providerCallId;
|
|
497
539
|
ctx.providerCallIdMap.set(result.providerCallId, callId);
|
|
@@ -830,13 +872,26 @@ function processEvent(ctx, event) {
|
|
|
830
872
|
switch (event.type) {
|
|
831
873
|
case "call.initiated":
|
|
832
874
|
transitionState(call, "initiated");
|
|
833
|
-
if (call.direction === "inbound" && call.providerCallId && ctx.provider?.answerCall)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
875
|
+
if (call.direction === "inbound" && call.providerCallId && ctx.provider?.answerCall) {
|
|
876
|
+
const inboundStreamSession = ctx.config.realtime?.enabled && ctx.provider.name === "telnyx" && ctx.streamSessionIssuer ? ctx.streamSessionIssuer({
|
|
877
|
+
providerName: "telnyx",
|
|
878
|
+
callId: call.callId,
|
|
879
|
+
from: call.from,
|
|
880
|
+
to: call.to,
|
|
881
|
+
direction: "inbound"
|
|
882
|
+
}) : void 0;
|
|
883
|
+
ctx.provider.answerCall({
|
|
884
|
+
callId: call.callId,
|
|
885
|
+
providerCallId: call.providerCallId,
|
|
886
|
+
...inboundStreamSession ? {
|
|
887
|
+
streamUrl: inboundStreamSession.streamUrl,
|
|
888
|
+
streamAuthToken: inboundStreamSession.token
|
|
889
|
+
} : {}
|
|
890
|
+
}).catch((err) => {
|
|
891
|
+
const message = formatErrorMessage(err);
|
|
892
|
+
console.warn(`[voice-call] Failed to answer inbound call ${call.providerCallId}:`, message);
|
|
893
|
+
});
|
|
894
|
+
}
|
|
840
895
|
break;
|
|
841
896
|
case "call.ringing":
|
|
842
897
|
transitionState(call, "ringing");
|
|
@@ -1110,7 +1165,8 @@ var CallManager = class {
|
|
|
1110
1165
|
initialMessageInFlight: this.initialMessageInFlight,
|
|
1111
1166
|
onCallAnswered: (call) => {
|
|
1112
1167
|
this.maybeSpeakInitialMessageOnAnswered(call);
|
|
1113
|
-
}
|
|
1168
|
+
},
|
|
1169
|
+
streamSessionIssuer: this.streamSessionIssuer
|
|
1114
1170
|
};
|
|
1115
1171
|
}
|
|
1116
1172
|
/**
|
|
@@ -1249,6 +1305,27 @@ async function resolveRealtimeFastContextConsult(params) {
|
|
|
1249
1305
|
});
|
|
1250
1306
|
}
|
|
1251
1307
|
//#endregion
|
|
1308
|
+
//#region extensions/voice-call/src/response-model.ts
|
|
1309
|
+
function resolveVoiceResponseModel(params) {
|
|
1310
|
+
const modelRef = params.voiceConfig.responseModel ?? `${params.agentRuntime.defaults.provider}/${params.agentRuntime.defaults.model}`;
|
|
1311
|
+
const slashIndex = modelRef.indexOf("/");
|
|
1312
|
+
return {
|
|
1313
|
+
modelRef,
|
|
1314
|
+
provider: slashIndex === -1 ? params.agentRuntime.defaults.provider : modelRef.slice(0, slashIndex),
|
|
1315
|
+
model: slashIndex === -1 ? modelRef : modelRef.slice(slashIndex + 1)
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
//#endregion
|
|
1319
|
+
//#region extensions/voice-call/src/telephony-audio.ts
|
|
1320
|
+
/**
|
|
1321
|
+
* Chunk audio buffer into 20ms frames for streaming (8kHz mono mu-law).
|
|
1322
|
+
*/
|
|
1323
|
+
function chunkAudio(audio, chunkSize = 160) {
|
|
1324
|
+
return (function* () {
|
|
1325
|
+
for (let i = 0; i < audio.length; i += chunkSize) yield audio.subarray(i, Math.min(i + chunkSize, audio.length));
|
|
1326
|
+
})();
|
|
1327
|
+
}
|
|
1328
|
+
//#endregion
|
|
1252
1329
|
//#region extensions/voice-call/src/telephony-tts.ts
|
|
1253
1330
|
const TELEPHONY_DEFAULT_TTS_TIMEOUT_MS = 8e3;
|
|
1254
1331
|
function createTelephonyTtsProvider(params) {
|
|
@@ -1730,6 +1807,14 @@ function resolveWebhookExposureStatus(config) {
|
|
|
1730
1807
|
};
|
|
1731
1808
|
}
|
|
1732
1809
|
//#endregion
|
|
1810
|
+
//#region extensions/voice-call/src/http-headers.ts
|
|
1811
|
+
function getHeader(headers, name) {
|
|
1812
|
+
const target = normalizeLowercaseStringOrEmpty(name);
|
|
1813
|
+
const value = headers[target] ?? Object.entries(headers).find(([key]) => normalizeLowercaseStringOrEmpty(key) === target)?.[1];
|
|
1814
|
+
if (Array.isArray(value)) return value[0];
|
|
1815
|
+
return value;
|
|
1816
|
+
}
|
|
1817
|
+
//#endregion
|
|
1733
1818
|
//#region extensions/voice-call/src/media-stream.ts
|
|
1734
1819
|
const DEFAULT_PRE_START_TIMEOUT_MS = 5e3;
|
|
1735
1820
|
const DEFAULT_MAX_PENDING_CONNECTIONS = 32;
|
|
@@ -1748,6 +1833,14 @@ function normalizeWsMessageData(data) {
|
|
|
1748
1833
|
if (Array.isArray(data)) return Buffer.concat(data);
|
|
1749
1834
|
return Buffer.from(data);
|
|
1750
1835
|
}
|
|
1836
|
+
function parseTwilioMediaMessage(data) {
|
|
1837
|
+
const raw = normalizeWsMessageData(data);
|
|
1838
|
+
try {
|
|
1839
|
+
return JSON.parse(raw.toString("utf8"));
|
|
1840
|
+
} catch (cause) {
|
|
1841
|
+
throw new Error("Twilio media stream message was malformed JSON", { cause });
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1751
1844
|
/**
|
|
1752
1845
|
* Manages WebSocket connections for Twilio media streams.
|
|
1753
1846
|
*/
|
|
@@ -1823,8 +1916,7 @@ var MediaStreamHandler = class {
|
|
|
1823
1916
|
}
|
|
1824
1917
|
ws.on("message", async (data) => {
|
|
1825
1918
|
try {
|
|
1826
|
-
const
|
|
1827
|
-
const message = JSON.parse(raw.toString("utf8"));
|
|
1919
|
+
const message = parseTwilioMediaMessage(data);
|
|
1828
1920
|
switch (message.event) {
|
|
1829
1921
|
case "connected":
|
|
1830
1922
|
console.log("[MediaStream] Twilio connected");
|
|
@@ -2357,6 +2449,25 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
|
2357
2449
|
}
|
|
2358
2450
|
};
|
|
2359
2451
|
//#endregion
|
|
2452
|
+
//#region extensions/voice-call/src/providers/shared/call-status.ts
|
|
2453
|
+
const TERMINAL_PROVIDER_STATUS_TO_END_REASON = {
|
|
2454
|
+
completed: "completed",
|
|
2455
|
+
failed: "failed",
|
|
2456
|
+
busy: "busy",
|
|
2457
|
+
"no-answer": "no-answer",
|
|
2458
|
+
canceled: "hangup-bot"
|
|
2459
|
+
};
|
|
2460
|
+
function normalizeProviderStatus(status) {
|
|
2461
|
+
const normalized = normalizeOptionalLowercaseString(status);
|
|
2462
|
+
return normalized && normalized.length > 0 ? normalized : "unknown";
|
|
2463
|
+
}
|
|
2464
|
+
function mapProviderStatusToEndReason(status) {
|
|
2465
|
+
return TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalizeProviderStatus(status)] ?? null;
|
|
2466
|
+
}
|
|
2467
|
+
function isProviderStatusTerminal(status) {
|
|
2468
|
+
return mapProviderStatusToEndReason(status) !== null;
|
|
2469
|
+
}
|
|
2470
|
+
//#endregion
|
|
2360
2471
|
//#region extensions/voice-call/src/webhook/stale-call-reaper.ts
|
|
2361
2472
|
const CHECK_INTERVAL_MS = 3e4;
|
|
2362
2473
|
function startStaleCallReaper(params) {
|
|
@@ -2390,11 +2501,11 @@ const TRANSCRIPT_LOG_MAX_CHARS = 200;
|
|
|
2390
2501
|
let realtimeTranscriptionRuntimePromise;
|
|
2391
2502
|
let responseGeneratorModulePromise;
|
|
2392
2503
|
function loadRealtimeTranscriptionRuntime() {
|
|
2393
|
-
realtimeTranscriptionRuntimePromise ??= import("./realtime-transcription.runtime-
|
|
2504
|
+
realtimeTranscriptionRuntimePromise ??= import("./realtime-transcription.runtime-CuOCFrMc.js");
|
|
2394
2505
|
return realtimeTranscriptionRuntimePromise;
|
|
2395
2506
|
}
|
|
2396
2507
|
function loadResponseGeneratorModule() {
|
|
2397
|
-
responseGeneratorModulePromise ??= import("./response-generator-
|
|
2508
|
+
responseGeneratorModulePromise ??= import("./response-generator-ZeNpwsyL.js");
|
|
2398
2509
|
return responseGeneratorModulePromise;
|
|
2399
2510
|
}
|
|
2400
2511
|
function sanitizeTranscriptForLog(value) {
|
|
@@ -2418,8 +2529,8 @@ function appendRecentTalkEventMetadata(call, event) {
|
|
|
2418
2529
|
recentTalkEvents: recent.slice(-10)
|
|
2419
2530
|
};
|
|
2420
2531
|
}
|
|
2421
|
-
function buildRequestUrl(requestUrl
|
|
2422
|
-
return new URL$1(requestUrl ?? "/",
|
|
2532
|
+
function buildRequestUrl(requestUrl) {
|
|
2533
|
+
return new URL$1(requestUrl ?? "/", "http://localhost");
|
|
2423
2534
|
}
|
|
2424
2535
|
function normalizeProxyIp(value) {
|
|
2425
2536
|
const trimmed = value?.trim();
|
|
@@ -2745,7 +2856,7 @@ var VoiceCallWebhookServer = class {
|
|
|
2745
2856
|
}
|
|
2746
2857
|
getUpgradePathname(request) {
|
|
2747
2858
|
try {
|
|
2748
|
-
return buildRequestUrl(request.url
|
|
2859
|
+
return buildRequestUrl(request.url).pathname;
|
|
2749
2860
|
} catch {
|
|
2750
2861
|
return null;
|
|
2751
2862
|
}
|
|
@@ -2768,7 +2879,7 @@ var VoiceCallWebhookServer = class {
|
|
|
2768
2879
|
this.writeWebhookResponse(res, payload);
|
|
2769
2880
|
}
|
|
2770
2881
|
async runWebhookPipeline(req, webhookPath) {
|
|
2771
|
-
const url = buildRequestUrl(req.url
|
|
2882
|
+
const url = buildRequestUrl(req.url);
|
|
2772
2883
|
if (url.pathname === "/voice/hold-music") return {
|
|
2773
2884
|
statusCode: 200,
|
|
2774
2885
|
headers: { "Content-Type": "text/xml" },
|
|
@@ -2902,7 +3013,7 @@ var VoiceCallWebhookServer = class {
|
|
|
2902
3013
|
}
|
|
2903
3014
|
isRealtimeWebSocketUpgrade(req) {
|
|
2904
3015
|
try {
|
|
2905
|
-
const pathname = buildRequestUrl(req.url
|
|
3016
|
+
const pathname = buildRequestUrl(req.url).pathname;
|
|
2906
3017
|
const pattern = this.realtimeHandler?.getStreamPathPattern();
|
|
2907
3018
|
return Boolean(pattern && pathname.startsWith(pattern));
|
|
2908
3019
|
} catch {
|
|
@@ -3011,27 +3122,27 @@ let mockProviderPromise;
|
|
|
3011
3122
|
let realtimeVoiceRuntimePromise;
|
|
3012
3123
|
let realtimeHandlerPromise;
|
|
3013
3124
|
function loadTelnyxProvider() {
|
|
3014
|
-
telnyxProviderPromise ??= import("./telnyx-
|
|
3125
|
+
telnyxProviderPromise ??= import("./telnyx-Df3IfYAS.js");
|
|
3015
3126
|
return telnyxProviderPromise;
|
|
3016
3127
|
}
|
|
3017
3128
|
function loadTwilioProvider() {
|
|
3018
|
-
twilioProviderPromise ??= import("./twilio-
|
|
3129
|
+
twilioProviderPromise ??= import("./twilio-B3zpyWY5.js");
|
|
3019
3130
|
return twilioProviderPromise;
|
|
3020
3131
|
}
|
|
3021
3132
|
function loadPlivoProvider() {
|
|
3022
|
-
plivoProviderPromise ??= import("./plivo
|
|
3133
|
+
plivoProviderPromise ??= import("./plivo--HUS8UBT.js");
|
|
3023
3134
|
return plivoProviderPromise;
|
|
3024
3135
|
}
|
|
3025
3136
|
function loadMockProvider() {
|
|
3026
|
-
mockProviderPromise ??= import("./mock-
|
|
3137
|
+
mockProviderPromise ??= import("./mock-BvTP8Rmx.js");
|
|
3027
3138
|
return mockProviderPromise;
|
|
3028
3139
|
}
|
|
3029
3140
|
function loadRealtimeVoiceRuntime() {
|
|
3030
|
-
realtimeVoiceRuntimePromise ??= import("./realtime-voice.runtime-
|
|
3141
|
+
realtimeVoiceRuntimePromise ??= import("./realtime-voice.runtime-BM0lAf0B.js");
|
|
3031
3142
|
return realtimeVoiceRuntimePromise;
|
|
3032
3143
|
}
|
|
3033
3144
|
function loadRealtimeHandler() {
|
|
3034
|
-
realtimeHandlerPromise ??= import("./realtime-handler-
|
|
3145
|
+
realtimeHandlerPromise ??= import("./realtime-handler-Cj_3954k.js");
|
|
3035
3146
|
return realtimeHandlerPromise;
|
|
3036
3147
|
}
|
|
3037
3148
|
function resolveVoiceCallConsultSessionKey(call) {
|
|
@@ -3249,8 +3360,10 @@ async function createVoiceCallRuntime(params) {
|
|
|
3249
3360
|
if (!publicUrl && config.tailscale?.mode !== "off") publicUrl = await setupTailscaleExposure(config);
|
|
3250
3361
|
const webhookUrl = publicUrl ?? localUrl;
|
|
3251
3362
|
if (providerRequiresPublicWebhook(provider.name) && isProviderUnreachableWebhookUrl(webhookUrl)) throw new Error(`[voice-call] ${provider.name} requires a publicly reachable webhook URL. Refusing to use local-only webhook ${webhookUrl}. Set plugins.entries.voice-call.config.publicUrl or enable tunnel/tailscale exposure.`);
|
|
3252
|
-
if (publicUrl
|
|
3363
|
+
if (publicUrl) provider.setPublicUrl?.(publicUrl);
|
|
3253
3364
|
if (publicUrl && realtimeProvider) webhookServer.getRealtimeHandler()?.setPublicUrl(publicUrl);
|
|
3365
|
+
const realtimeHandler = webhookServer.getRealtimeHandler();
|
|
3366
|
+
if (realtimeHandler) manager.streamSessionIssuer = (request) => realtimeHandler.issueStreamSession(request);
|
|
3254
3367
|
if (provider.name === "twilio" && config.streaming?.enabled) {
|
|
3255
3368
|
const twilioProvider = provider;
|
|
3256
3369
|
if (ttsRuntime?.textToSpeechTelephony) try {
|
|
@@ -3293,4 +3406,4 @@ async function createVoiceCallRuntime(params) {
|
|
|
3293
3406
|
}
|
|
3294
3407
|
}
|
|
3295
3408
|
//#endregion
|
|
3296
|
-
export {
|
|
3409
|
+
export { getHeader as a, getTailscaleSelfInfo as c, chunkAudio as d, resolveVoiceResponseModel as f, mapVoiceToPolly as h, normalizeProviderStatus as i, setupTailscaleExposureRoute as l, escapeXml as m, isProviderStatusTerminal as n, resolveWebhookExposureStatus as o, resolveUserPath as p, mapProviderStatusToEndReason as r, cleanupTailscaleExposureRoute as s, createVoiceCallRuntime as t, TELEPHONY_DEFAULT_TTS_TIMEOUT_MS as u };
|
package/dist/runtime-entry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createVoiceCallRuntime } from "./runtime-entry-
|
|
1
|
+
import { t as createVoiceCallRuntime } from "./runtime-entry-ox4PGoxT.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
package/dist/setup-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-
|
|
1
|
+
import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-DJqJ8NzH.js";
|
|
2
2
|
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
4
|
//#region extensions/voice-call/setup-api.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-D_aM8XmA.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -10,6 +10,14 @@ function normalizeTelnyxDirection(direction) {
|
|
|
10
10
|
default: return;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
function normalizeBase64ForCompare(value) {
|
|
14
|
+
return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
|
|
15
|
+
}
|
|
16
|
+
function decodeClientStateBase64(value) {
|
|
17
|
+
const buffer = Buffer.from(value, "base64");
|
|
18
|
+
if (normalizeBase64ForCompare(buffer.toString("base64")) !== normalizeBase64ForCompare(value)) return null;
|
|
19
|
+
return buffer.toString("utf8");
|
|
20
|
+
}
|
|
13
21
|
var TelnyxProvider = class {
|
|
14
22
|
constructor(config, options = {}) {
|
|
15
23
|
this.name = "telnyx";
|
|
@@ -79,11 +87,7 @@ var TelnyxProvider = class {
|
|
|
79
87
|
*/
|
|
80
88
|
normalizeEvent(data, dedupeKey) {
|
|
81
89
|
let callId = "";
|
|
82
|
-
if (data.payload?.client_state)
|
|
83
|
-
callId = Buffer.from(data.payload.client_state, "base64").toString("utf8");
|
|
84
|
-
} catch {
|
|
85
|
-
callId = data.payload.client_state;
|
|
86
|
-
}
|
|
90
|
+
if (data.payload?.client_state) callId = decodeClientStateBase64(data.payload.client_state) ?? data.payload.client_state;
|
|
87
91
|
if (!callId) callId = data.payload?.call_control_id || "";
|
|
88
92
|
const baseEvent = {
|
|
89
93
|
id: data.id || crypto.randomUUID(),
|
|
@@ -134,6 +138,8 @@ var TelnyxProvider = class {
|
|
|
134
138
|
type: "call.dtmf",
|
|
135
139
|
digits: data.payload?.digit || ""
|
|
136
140
|
};
|
|
141
|
+
case "streaming.started":
|
|
142
|
+
case "streaming.stopped": return null;
|
|
137
143
|
default: return null;
|
|
138
144
|
}
|
|
139
145
|
}
|
|
@@ -163,20 +169,19 @@ var TelnyxProvider = class {
|
|
|
163
169
|
return "completed";
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Initiate an outbound call via Telnyx API.
|
|
168
|
-
*/
|
|
169
172
|
async initiateCall(input) {
|
|
173
|
+
const body = {
|
|
174
|
+
connection_id: this.connectionId,
|
|
175
|
+
to: input.to,
|
|
176
|
+
from: input.from,
|
|
177
|
+
webhook_url: input.webhookUrl,
|
|
178
|
+
webhook_url_method: "POST",
|
|
179
|
+
client_state: Buffer.from(input.callId).toString("base64"),
|
|
180
|
+
timeout_secs: 30,
|
|
181
|
+
...input.streamUrl ? buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken) : {}
|
|
182
|
+
};
|
|
170
183
|
return {
|
|
171
|
-
providerCallId: (await this.apiRequest("/calls",
|
|
172
|
-
connection_id: this.connectionId,
|
|
173
|
-
to: input.to,
|
|
174
|
-
from: input.from,
|
|
175
|
-
webhook_url: input.webhookUrl,
|
|
176
|
-
webhook_url_method: "POST",
|
|
177
|
-
client_state: Buffer.from(input.callId).toString("base64"),
|
|
178
|
-
timeout_secs: 30
|
|
179
|
-
})).data.call_control_id,
|
|
184
|
+
providerCallId: (await this.apiRequest("/calls", body)).data.call_control_id,
|
|
180
185
|
status: "initiated"
|
|
181
186
|
};
|
|
182
187
|
}
|
|
@@ -186,11 +191,12 @@ var TelnyxProvider = class {
|
|
|
186
191
|
async hangupCall(input) {
|
|
187
192
|
await this.apiRequest(`/calls/${input.providerCallId}/actions/hangup`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
|
|
188
193
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Answer an inbound Telnyx Call Control leg.
|
|
191
|
-
*/
|
|
192
194
|
async answerCall(input) {
|
|
193
|
-
|
|
195
|
+
const body = {
|
|
196
|
+
command_id: `openclaw-answer-${input.callId}`,
|
|
197
|
+
...input.streamUrl ? buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken) : {}
|
|
198
|
+
};
|
|
199
|
+
await this.apiRequest(`/calls/${input.providerCallId}/actions/answer`, body);
|
|
194
200
|
}
|
|
195
201
|
/**
|
|
196
202
|
* Play TTS audio via Telnyx speak action.
|
|
@@ -256,5 +262,17 @@ var TelnyxProvider = class {
|
|
|
256
262
|
}
|
|
257
263
|
}
|
|
258
264
|
};
|
|
265
|
+
function buildTelnyxStreamingFields(streamUrl, streamAuthToken) {
|
|
266
|
+
return {
|
|
267
|
+
stream_url: streamUrl,
|
|
268
|
+
stream_track: "inbound_track",
|
|
269
|
+
stream_codec: "PCMU",
|
|
270
|
+
stream_bidirectional_mode: "rtp",
|
|
271
|
+
stream_bidirectional_codec: "PCMU",
|
|
272
|
+
stream_bidirectional_sampling_rate: 8e3,
|
|
273
|
+
stream_bidirectional_target_legs: "self",
|
|
274
|
+
...streamAuthToken ? { stream_auth_token: streamAuthToken } : {}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
259
277
|
//#endregion
|
|
260
278
|
export { TelnyxProvider };
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as getHeader } from "./http-headers-B5L5gMpK.js";
|
|
6
|
-
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5TNOqhE.js";
|
|
3
|
+
import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-ox4PGoxT.js";
|
|
4
|
+
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-D_aM8XmA.js";
|
|
7
5
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
6
|
import crypto from "node:crypto";
|
|
9
7
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
@@ -61,7 +59,12 @@ async function twilioApiRequest(params) {
|
|
|
61
59
|
throw new TwilioApiError(response.status, errorText);
|
|
62
60
|
}
|
|
63
61
|
const text = await response.text();
|
|
64
|
-
|
|
62
|
+
if (!text) return;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(text);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error("Twilio API returned malformed JSON.");
|
|
67
|
+
}
|
|
65
68
|
} finally {
|
|
66
69
|
await release();
|
|
67
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.14-beta.2",
|
|
4
4
|
"description": "OpenClaw voice-call plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"commander": "14.0.3",
|
|
12
12
|
"typebox": "1.1.38",
|
|
13
|
-
"ws": "8.20.
|
|
13
|
+
"ws": "8.20.1",
|
|
14
14
|
"zod": "4.4.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"openclaw": "workspace:*"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"openclaw": ">=2026.5.
|
|
21
|
+
"openclaw": ">=2026.5.14-beta.2"
|
|
22
22
|
},
|
|
23
23
|
"peerDependenciesMeta": {
|
|
24
24
|
"openclaw": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"minHostVersion": ">=2026.4.10"
|
|
36
36
|
},
|
|
37
37
|
"compat": {
|
|
38
|
-
"pluginApi": ">=2026.5.
|
|
38
|
+
"pluginApi": ">=2026.5.14-beta.2"
|
|
39
39
|
},
|
|
40
40
|
"build": {
|
|
41
|
-
"openclawVersion": "2026.5.
|
|
41
|
+
"openclawVersion": "2026.5.14-beta.2"
|
|
42
42
|
},
|
|
43
43
|
"release": {
|
|
44
44
|
"publishToClawHub": true,
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
-
import { convertPcmToMulaw8k } from "openclaw/plugin-sdk/realtime-voice";
|
|
3
|
-
//#region extensions/voice-call/src/telephony-audio.ts
|
|
4
|
-
/**
|
|
5
|
-
* Chunk audio buffer into 20ms frames for streaming (8kHz mono mu-law).
|
|
6
|
-
*/
|
|
7
|
-
function chunkAudio(audio, chunkSize = 160) {
|
|
8
|
-
return (function* () {
|
|
9
|
-
for (let i = 0; i < audio.length; i += chunkSize) yield audio.subarray(i, Math.min(i + chunkSize, audio.length));
|
|
10
|
-
})();
|
|
11
|
-
}
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region extensions/voice-call/src/providers/shared/call-status.ts
|
|
14
|
-
const TERMINAL_PROVIDER_STATUS_TO_END_REASON = {
|
|
15
|
-
completed: "completed",
|
|
16
|
-
failed: "failed",
|
|
17
|
-
busy: "busy",
|
|
18
|
-
"no-answer": "no-answer",
|
|
19
|
-
canceled: "hangup-bot"
|
|
20
|
-
};
|
|
21
|
-
function normalizeProviderStatus(status) {
|
|
22
|
-
const normalized = normalizeOptionalLowercaseString(status);
|
|
23
|
-
return normalized && normalized.length > 0 ? normalized : "unknown";
|
|
24
|
-
}
|
|
25
|
-
function mapProviderStatusToEndReason(status) {
|
|
26
|
-
return TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalizeProviderStatus(status)] ?? null;
|
|
27
|
-
}
|
|
28
|
-
function isProviderStatusTerminal(status) {
|
|
29
|
-
return mapProviderStatusToEndReason(status) !== null;
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
export { convertPcmToMulaw8k as a, chunkAudio as i, mapProviderStatusToEndReason as n, normalizeProviderStatus as r, isProviderStatusTerminal as t };
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
-
//#region extensions/voice-call/src/http-headers.ts
|
|
3
|
-
function getHeader(headers, name) {
|
|
4
|
-
const target = normalizeLowercaseStringOrEmpty(name);
|
|
5
|
-
const value = headers[target] ?? Object.entries(headers).find(([key]) => normalizeLowercaseStringOrEmpty(key) === target)?.[1];
|
|
6
|
-
if (Array.isArray(value)) return value[0];
|
|
7
|
-
return value;
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
|
10
|
-
export { getHeader as t };
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
//#region extensions/voice-call/src/response-model.ts
|
|
2
|
-
function resolveVoiceResponseModel(params) {
|
|
3
|
-
const modelRef = params.voiceConfig.responseModel ?? `${params.agentRuntime.defaults.provider}/${params.agentRuntime.defaults.model}`;
|
|
4
|
-
const slashIndex = modelRef.indexOf("/");
|
|
5
|
-
return {
|
|
6
|
-
modelRef,
|
|
7
|
-
provider: slashIndex === -1 ? params.agentRuntime.defaults.provider : modelRef.slice(0, slashIndex),
|
|
8
|
-
model: slashIndex === -1 ? modelRef : modelRef.slice(slashIndex + 1)
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
export { resolveVoiceResponseModel as t };
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
-
//#region extensions/voice-call/src/voice-mapping.ts
|
|
3
|
-
/**
|
|
4
|
-
* Escape XML special characters for TwiML and other XML responses.
|
|
5
|
-
*/
|
|
6
|
-
function escapeXml(text) {
|
|
7
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Map of OpenAI voice names to similar Twilio Polly voices.
|
|
11
|
-
*/
|
|
12
|
-
const OPENAI_TO_POLLY_MAP = {
|
|
13
|
-
alloy: "Polly.Joanna",
|
|
14
|
-
echo: "Polly.Matthew",
|
|
15
|
-
fable: "Polly.Amy",
|
|
16
|
-
onyx: "Polly.Brian",
|
|
17
|
-
nova: "Polly.Salli",
|
|
18
|
-
shimmer: "Polly.Kimberly"
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Default Polly voice when no mapping is found.
|
|
22
|
-
*/
|
|
23
|
-
const DEFAULT_POLLY_VOICE = "Polly.Joanna";
|
|
24
|
-
/**
|
|
25
|
-
* Map OpenAI voice names to Twilio Polly equivalents.
|
|
26
|
-
* Falls through if already a valid Polly/Google voice.
|
|
27
|
-
*
|
|
28
|
-
* @param voice - OpenAI voice name (alloy, echo, etc.) or Polly voice name
|
|
29
|
-
* @returns Polly voice name suitable for Twilio TwiML
|
|
30
|
-
*/
|
|
31
|
-
function mapVoiceToPolly(voice) {
|
|
32
|
-
if (!voice) return DEFAULT_POLLY_VOICE;
|
|
33
|
-
if (voice.startsWith("Polly.") || voice.startsWith("Google.")) return voice;
|
|
34
|
-
return OPENAI_TO_POLLY_MAP[normalizeLowercaseStringOrEmpty(voice)] || "Polly.Joanna";
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
37
|
-
export { mapVoiceToPolly as n, escapeXml as t };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|