botium-connector-voip 0.0.18 → 0.0.20
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.
|
@@ -5,6 +5,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var ws = require('ws');
|
|
6
6
|
var lodash = require('lodash');
|
|
7
7
|
var axios = require('axios');
|
|
8
|
+
var http = require('http');
|
|
9
|
+
var https = require('https');
|
|
8
10
|
var debug$4 = require('debug');
|
|
9
11
|
var path = require('path');
|
|
10
12
|
var musicMetadata = require('music-metadata');
|
|
@@ -14,6 +16,8 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
|
|
|
14
16
|
var ws__default = /*#__PURE__*/_interopDefaultLegacy(ws);
|
|
15
17
|
var lodash__default = /*#__PURE__*/_interopDefaultLegacy(lodash);
|
|
16
18
|
var axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
|
|
19
|
+
var http__default = /*#__PURE__*/_interopDefaultLegacy(http);
|
|
20
|
+
var https__default = /*#__PURE__*/_interopDefaultLegacy(https);
|
|
17
21
|
var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug$4);
|
|
18
22
|
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
19
23
|
var musicMetadata__default = /*#__PURE__*/_interopDefaultLegacy(musicMetadata);
|
|
@@ -37,6 +41,9 @@ const Capabilities = {
|
|
|
37
41
|
VOIP_TTS_BODY: 'VOIP_TTS_BODY',
|
|
38
42
|
VOIP_TTS_HEADERS: 'VOIP_TTS_HEADERS',
|
|
39
43
|
VOIP_TTS_TIMEOUT: 'VOIP_TTS_TIMEOUT',
|
|
44
|
+
VOIP_TTS_CACHE_ENABLE: 'VOIP_TTS_CACHE_ENABLE',
|
|
45
|
+
VOIP_TTS_CACHE_SIZE: 'VOIP_TTS_CACHE_SIZE',
|
|
46
|
+
VOIP_TTS_PREFETCH_ENABLE: 'VOIP_TTS_PREFETCH_ENABLE',
|
|
40
47
|
VOIP_WORKER_URL: 'VOIP_WORKER_URL',
|
|
41
48
|
VOIP_WORKER_APIKEY: 'VOIP_WORKER_APIKEY',
|
|
42
49
|
VOIP_SIP_POOL_CALLER_ENABLE: 'VOIP_SIP_POOL_CALLER_ENABLE',
|
|
@@ -70,6 +77,9 @@ const Defaults = {
|
|
|
70
77
|
VOIP_STT_TIMEOUT: 10000,
|
|
71
78
|
VOIP_TTS_METHOD: 'GET',
|
|
72
79
|
VOIP_TTS_TIMEOUT: 10000,
|
|
80
|
+
VOIP_TTS_CACHE_ENABLE: true,
|
|
81
|
+
VOIP_TTS_CACHE_SIZE: 50,
|
|
82
|
+
VOIP_TTS_PREFETCH_ENABLE: true,
|
|
73
83
|
VOIP_STT_MESSAGE_HANDLING: 'ORIGINAL',
|
|
74
84
|
VOIP_STT_MESSAGE_HANDLING_TIMEOUT: 2500,
|
|
75
85
|
VOIP_STT_MESSAGE_HANDLING_DELIMITER: '. ',
|
|
@@ -84,6 +94,12 @@ const Defaults = {
|
|
|
84
94
|
VOIP_USE_GLOBAL_VOIP_WORKER: false,
|
|
85
95
|
VOIP_SIP_PROTOCOL: 'TCP'
|
|
86
96
|
};
|
|
97
|
+
const TTS_HTTP_AGENT = new http__default["default"].Agent({
|
|
98
|
+
keepAlive: true
|
|
99
|
+
});
|
|
100
|
+
const TTS_HTTPS_AGENT = new https__default["default"].Agent({
|
|
101
|
+
keepAlive: true
|
|
102
|
+
});
|
|
87
103
|
class BotiumConnectorVoip {
|
|
88
104
|
constructor({
|
|
89
105
|
queueBotSays,
|
|
@@ -97,9 +113,18 @@ class BotiumConnectorVoip {
|
|
|
97
113
|
this.sentencesBuilding = 0;
|
|
98
114
|
this.sentencesFinal = 0;
|
|
99
115
|
this.sentenceBuilding = false;
|
|
116
|
+
this.ttsCache = new Map();
|
|
117
|
+
this.ttsCacheEnabled = false;
|
|
118
|
+
this.ttsCacheMaxEntries = 0;
|
|
119
|
+
this.ttsPrefetchEnabled = false;
|
|
120
|
+
|
|
121
|
+
// For debugging latency between incoming STT (bot says) and outgoing audio (sendAudio)
|
|
122
|
+
this._lastBotSaysQueuedAt = null;
|
|
123
|
+
this._lastBotSaysText = null;
|
|
100
124
|
}
|
|
101
125
|
async Validate() {
|
|
102
126
|
debug$3('Validate called');
|
|
127
|
+
this.caps = Object.assign({}, Defaults, this.caps);
|
|
103
128
|
debug$3(this.caps.VOIP_STT_MESSAGE_HANDLING);
|
|
104
129
|
if (this.caps.VOIP_TTS_URL) {
|
|
105
130
|
this.axiosTtsParams = {
|
|
@@ -107,7 +132,9 @@ class BotiumConnectorVoip {
|
|
|
107
132
|
params: this._getParams(Capabilities.VOIP_TTS_PARAMS),
|
|
108
133
|
method: this.caps.VOIP_TTS_METHOD,
|
|
109
134
|
timeout: this.caps.VOIP_TTS_TIMEOUT,
|
|
110
|
-
headers: this._getHeaders(Capabilities.VOIP_TTS_HEADERS)
|
|
135
|
+
headers: this._getHeaders(Capabilities.VOIP_TTS_HEADERS),
|
|
136
|
+
httpAgent: TTS_HTTP_AGENT,
|
|
137
|
+
httpsAgent: TTS_HTTPS_AGENT
|
|
111
138
|
};
|
|
112
139
|
try {
|
|
113
140
|
const {
|
|
@@ -125,7 +152,10 @@ class BotiumConnectorVoip {
|
|
|
125
152
|
throw new Error(`Checking TTS Status failed - ${this._getAxiosErrOutput(err)}`);
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
|
-
|
|
155
|
+
const cacheSize = parseInt(this.caps[Capabilities.VOIP_TTS_CACHE_SIZE], 10);
|
|
156
|
+
this.ttsCacheEnabled = !!this.caps[Capabilities.VOIP_TTS_CACHE_ENABLE];
|
|
157
|
+
this.ttsCacheMaxEntries = lodash__default["default"].isFinite(cacheSize) && cacheSize > 0 ? cacheSize : 0;
|
|
158
|
+
this.ttsPrefetchEnabled = !!this.caps[Capabilities.VOIP_TTS_PREFETCH_ENABLE];
|
|
129
159
|
}
|
|
130
160
|
async Start() {
|
|
131
161
|
debug$3('Start called');
|
|
@@ -135,7 +165,14 @@ class BotiumConnectorVoip {
|
|
|
135
165
|
this.end = false;
|
|
136
166
|
this.connected = false;
|
|
137
167
|
this.convoStep = null;
|
|
168
|
+
this._lastBotSaysQueuedAt = null;
|
|
169
|
+
this._lastBotSaysText = null;
|
|
170
|
+
if (this.ttsCache) {
|
|
171
|
+
this.ttsCache.clear();
|
|
172
|
+
}
|
|
138
173
|
const sendBotMsg = botMsg => {
|
|
174
|
+
this._lastBotSaysQueuedAt = Date.now();
|
|
175
|
+
this._lastBotSaysText = botMsg && botMsg.messageText ? String(botMsg.messageText) : null;
|
|
139
176
|
setTimeout(() => this.queueBotSays(botMsg), 0);
|
|
140
177
|
};
|
|
141
178
|
const joinBotMsg = (botMsgs, joinLastPrevMsg) => {
|
|
@@ -235,6 +272,35 @@ class BotiumConnectorVoip {
|
|
|
235
272
|
}
|
|
236
273
|
this.eventEmitter.on('CONVO_STEP_NEXT', (container, convoStep) => {
|
|
237
274
|
this.convoStep = convoStep;
|
|
275
|
+
this._maybePrefetchTts(convoStep);
|
|
276
|
+
// For PSST: send join silence duration per step to VOIP worker (controls PSST silence trigger)
|
|
277
|
+
try {
|
|
278
|
+
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'PSST' && this.ws && this.ws.readyState === ws__default["default"].OPEN) {
|
|
279
|
+
const joinLogicHook = this._getJoinLogicHook(convoStep);
|
|
280
|
+
let silenceMs = null;
|
|
281
|
+
if (joinLogicHook && joinLogicHook.args && joinLogicHook.args.length > 0) {
|
|
282
|
+
silenceMs = parseInt(joinLogicHook.args[0], 10);
|
|
283
|
+
}
|
|
284
|
+
// Fallback to global timeout if no per-step hook is set
|
|
285
|
+
if (!lodash__default["default"].isFinite(silenceMs) || silenceMs <= 0) {
|
|
286
|
+
silenceMs = parseInt(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT], 10);
|
|
287
|
+
}
|
|
288
|
+
// PSST: treat like JOIN, but subtract 500ms from timeout
|
|
289
|
+
if (lodash__default["default"].isFinite(silenceMs) && silenceMs > 0) {
|
|
290
|
+
silenceMs = Math.max(0, silenceMs - 500);
|
|
291
|
+
}
|
|
292
|
+
if (lodash__default["default"].isFinite(silenceMs) && silenceMs > 0 && this.sessionId) {
|
|
293
|
+
debug$3(`PSST: sending silenceDurationMs=${silenceMs} for sessionId=${this.sessionId}`);
|
|
294
|
+
this.ws.send(JSON.stringify({
|
|
295
|
+
METHOD: 'setSttSilenceDuration',
|
|
296
|
+
sessionId: this.sessionId,
|
|
297
|
+
silenceDurationMs: silenceMs
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
debug$3(`Failed sending PSST silence duration to VOIP worker: ${err.message || err}`);
|
|
303
|
+
}
|
|
238
304
|
});
|
|
239
305
|
this.silence = null;
|
|
240
306
|
this.msgCount = 0;
|
|
@@ -243,6 +309,18 @@ class BotiumConnectorVoip {
|
|
|
243
309
|
this.silenceTimeout = null;
|
|
244
310
|
this.wsOpened = true;
|
|
245
311
|
debug$3(`Websocket connection to ${wsEndpoint} opened.`);
|
|
312
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
313
|
+
const isPsst = sttHandling === 'PSST';
|
|
314
|
+
const sttLegacy = true;
|
|
315
|
+
let sttUrl = this.caps[Capabilities.VOIP_STT_URL_STREAM];
|
|
316
|
+
if (isPsst && lodash__default["default"].isString(sttUrl)) {
|
|
317
|
+
const replaced = sttUrl.replace('/api/sttstream/', '/api/stt/');
|
|
318
|
+
if (replaced !== sttUrl) {
|
|
319
|
+
sttUrl = replaced;
|
|
320
|
+
} else {
|
|
321
|
+
debug$3(`PSST: Could not derive /api/stt url from ${sttUrl}, using as-is`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
246
324
|
const request = {
|
|
247
325
|
METHOD: 'initCall',
|
|
248
326
|
SIP_CALLER_AUTO: this.caps[Capabilities.VOIP_SIP_POOL_CALLER_ENABLE],
|
|
@@ -263,8 +341,9 @@ class BotiumConnectorVoip {
|
|
|
263
341
|
ICE_TURN_PASSWORD: this.caps[Capabilities.VOIP_ICE_TURN_PASSWORD],
|
|
264
342
|
ICE_TURN_PROTOCOL: this.caps[Capabilities.VOIP_ICE_TURN_PROTOCOL] || 'TCP',
|
|
265
343
|
MIN_SILENCE_DURATION: this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT_ENABLE] ? this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT] : null,
|
|
344
|
+
STT_LEGACY: sttLegacy,
|
|
266
345
|
STT_CONFIG: {
|
|
267
|
-
stt_url:
|
|
346
|
+
stt_url: sttUrl,
|
|
268
347
|
stt_params: this.caps[Capabilities.VOIP_STT_PARAMS_STREAM],
|
|
269
348
|
stt_body: this.caps[Capabilities.VOIP_STT_BODY_STREAM] || null
|
|
270
349
|
},
|
|
@@ -287,12 +366,16 @@ class BotiumConnectorVoip {
|
|
|
287
366
|
const parsedData = JSON.parse(data);
|
|
288
367
|
const parsedDataLog = lodash__default["default"].cloneDeep(parsedData);
|
|
289
368
|
parsedDataLog.fullRecord = '<full_record_buffer>';
|
|
290
|
-
|
|
291
|
-
// debug(JSON.stringify(parsedDataLog, null, 2))
|
|
292
|
-
|
|
369
|
+
debug$3(JSON.stringify(parsedDataLog, null, 2));
|
|
293
370
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
294
371
|
this.sessionId = parsedData.voipConfig.sessionId;
|
|
295
372
|
}
|
|
373
|
+
|
|
374
|
+
// if sessionId is not the same as the one in the callinfo, return
|
|
375
|
+
if (parsedData && parsedData.voipConfig && parsedData.voipConfig.sessionId && parsedData.voipConfig.sessionId !== this.sessionId) {
|
|
376
|
+
debug$3('sessionId mismatch, returning');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
296
379
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'unauthorized') {
|
|
297
380
|
reject(new Error('Error: Cannot open a call: SIP Authorization failed'));
|
|
298
381
|
}
|
|
@@ -350,12 +433,25 @@ class BotiumConnectorVoip {
|
|
|
350
433
|
if (!lodash__default["default"].isNil(parsedData.data.start)) {
|
|
351
434
|
const silenceDuration = parsedData.data.start - this.prevData.data.end;
|
|
352
435
|
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
353
|
-
const
|
|
436
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
437
|
+
const isPsst = sttHandling === 'PSST';
|
|
438
|
+
const isJoinMethod = sttHandling === 'JOIN' || isPsst;
|
|
439
|
+
const toJoinTimeoutMs = ms => {
|
|
440
|
+
const parsed = parseInt(ms, 10);
|
|
441
|
+
if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
|
|
442
|
+
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
443
|
+
};
|
|
354
444
|
let matched = false;
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
445
|
+
if (!lodash__default["default"].isNil(joinLogicHook) && isJoinMethod) {
|
|
446
|
+
const joinTimeoutMs = toJoinTimeoutMs(joinLogicHook && joinLogicHook.args && joinLogicHook.args[0]);
|
|
447
|
+
if (lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
448
|
+
matched = true;
|
|
449
|
+
}
|
|
450
|
+
} else if (lodash__default["default"].isNil(joinLogicHook) && isJoinMethod) {
|
|
451
|
+
const joinTimeoutMs = toJoinTimeoutMs(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
|
|
452
|
+
if (lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
453
|
+
matched = true;
|
|
454
|
+
}
|
|
359
455
|
}
|
|
360
456
|
if (matched && this.botMsgs.length > 0) {
|
|
361
457
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
@@ -370,6 +466,7 @@ class BotiumConnectorVoip {
|
|
|
370
466
|
const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
|
|
371
467
|
const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
|
|
372
468
|
debug$3(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
|
|
469
|
+
// ORIGINAL: emit final message immediately, unless join hooks are active.
|
|
373
470
|
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
374
471
|
let botMsg = {
|
|
375
472
|
messageText: parsedData.data.message
|
|
@@ -425,7 +522,7 @@ class BotiumConnectorVoip {
|
|
|
425
522
|
}
|
|
426
523
|
this.botMsgs = [];
|
|
427
524
|
}
|
|
428
|
-
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'JOIN' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'CONCAT' || !lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
525
|
+
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'JOIN' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'PSST' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'CONCAT' || !lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
429
526
|
const botMsg = {
|
|
430
527
|
messageText: parsedData.data.message,
|
|
431
528
|
sourceData: parsedData
|
|
@@ -433,15 +530,27 @@ class BotiumConnectorVoip {
|
|
|
433
530
|
this.prevData = parsedData;
|
|
434
531
|
if (successfulConfidenceScore) {
|
|
435
532
|
this.botMsgs.push(botMsg);
|
|
533
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
534
|
+
const isPsst = sttHandling === 'PSST';
|
|
535
|
+
const toJoinTimeoutMs = ms => {
|
|
536
|
+
const parsed = parseInt(ms, 10);
|
|
537
|
+
if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
|
|
538
|
+
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
539
|
+
};
|
|
540
|
+
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
541
|
+
let joinTimeoutMs = toJoinTimeoutMs(lodash__default["default"].get(joinLogicHook, 'args[0]'));
|
|
542
|
+
if (!lodash__default["default"].isFinite(joinTimeoutMs) || joinTimeoutMs <= 0) {
|
|
543
|
+
joinTimeoutMs = toJoinTimeoutMs(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
|
|
544
|
+
}
|
|
436
545
|
this.silenceTimeout = setTimeout(() => {
|
|
437
546
|
if (this.botMsgs.length > 0) {
|
|
438
|
-
debug$3('Silence Duration Timeout (PSST):',
|
|
547
|
+
debug$3('Silence Duration Timeout (JOIN/PSST):', joinTimeoutMs, 'ms');
|
|
439
548
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
440
549
|
this.firstMsg = false;
|
|
441
550
|
this.joinLastPrevMsg = this.botMsgs[this.botMsgs.length - 1];
|
|
442
551
|
this.botMsgs = [];
|
|
443
552
|
}
|
|
444
|
-
},
|
|
553
|
+
}, joinTimeoutMs || 0);
|
|
445
554
|
}
|
|
446
555
|
}
|
|
447
556
|
}
|
|
@@ -468,23 +577,32 @@ class BotiumConnectorVoip {
|
|
|
468
577
|
});
|
|
469
578
|
this.ws.send(request);
|
|
470
579
|
} else if (msg && msg.messageText) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
580
|
+
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
581
|
+
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
582
|
+
if (dtmfMatch && dtmfMatch[1]) {
|
|
583
|
+
const digits = dtmfMatch[1];
|
|
584
|
+
debug$3(`Sending DTMF from messageText: ${digits}`);
|
|
585
|
+
const request = JSON.stringify({
|
|
586
|
+
METHOD: 'sendDtmf',
|
|
587
|
+
digits,
|
|
588
|
+
sessionId: this.sessionId
|
|
589
|
+
});
|
|
590
|
+
this.ws.send(request);
|
|
591
|
+
return resolve();
|
|
592
|
+
}
|
|
593
|
+
if (!this.axiosTtsParams) {
|
|
594
|
+
if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
|
|
595
|
+
return reject(new Error('TTS not configured, only audio input supported'));
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
599
|
+
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
482
600
|
msg.sourceData = ttsRequest;
|
|
483
|
-
let
|
|
601
|
+
let ttsResult = null;
|
|
484
602
|
try {
|
|
485
|
-
|
|
603
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
486
604
|
} catch (err) {
|
|
487
|
-
reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
605
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
488
606
|
}
|
|
489
607
|
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
490
608
|
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
@@ -495,22 +613,28 @@ class BotiumConnectorVoip {
|
|
|
495
613
|
apiKey
|
|
496
614
|
});
|
|
497
615
|
}
|
|
498
|
-
if (Buffer.isBuffer(
|
|
499
|
-
duration =
|
|
616
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
617
|
+
duration = parseFloat(ttsResult.duration) || 0;
|
|
618
|
+
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
500
619
|
const request = JSON.stringify({
|
|
501
620
|
METHOD: 'sendAudio',
|
|
502
621
|
PESQ: false,
|
|
503
622
|
sessionId: this.sessionId,
|
|
504
|
-
b64_buffer:
|
|
623
|
+
b64_buffer: b64Buffer
|
|
505
624
|
});
|
|
625
|
+
if (this._lastBotSaysQueuedAt) {
|
|
626
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
627
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
628
|
+
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
629
|
+
}
|
|
506
630
|
msg.attachments.push({
|
|
507
631
|
name: 'tts.wav',
|
|
508
632
|
mimeType: 'audio/wav',
|
|
509
|
-
base64:
|
|
633
|
+
base64: b64Buffer
|
|
510
634
|
});
|
|
511
635
|
this.ws.send(request);
|
|
512
636
|
} else {
|
|
513
|
-
reject(new Error(
|
|
637
|
+
return reject(new Error('TTS failed, response is empty'));
|
|
514
638
|
}
|
|
515
639
|
}
|
|
516
640
|
}
|
|
@@ -520,6 +644,11 @@ class BotiumConnectorVoip {
|
|
|
520
644
|
sessionId: this.sessionId,
|
|
521
645
|
b64_buffer: msg.media[0].buffer.toString('base64')
|
|
522
646
|
});
|
|
647
|
+
if (this._lastBotSaysQueuedAt) {
|
|
648
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
649
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
650
|
+
debug$3(`Latency (bot says -> sendAudio/MEDIA): ${latencyMs} ms (last bot: ${botText})`);
|
|
651
|
+
}
|
|
523
652
|
this.ws.send(request);
|
|
524
653
|
msg.attachments.push({
|
|
525
654
|
name: msg.media[0].mediaUri,
|
|
@@ -571,6 +700,126 @@ class BotiumConnectorVoip {
|
|
|
571
700
|
this.firstSttInfoReceived = false;
|
|
572
701
|
}
|
|
573
702
|
}
|
|
703
|
+
_isTtsCacheEnabled() {
|
|
704
|
+
return this.ttsCacheEnabled && this.ttsCacheMaxEntries > 0;
|
|
705
|
+
}
|
|
706
|
+
_touchTtsCache(cacheKey, entry) {
|
|
707
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
708
|
+
this.ttsCache.delete(cacheKey);
|
|
709
|
+
this.ttsCache.set(cacheKey, entry);
|
|
710
|
+
}
|
|
711
|
+
_setTtsCacheEntry(cacheKey, entry) {
|
|
712
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
713
|
+
this.ttsCache.set(cacheKey, entry);
|
|
714
|
+
this._trimTtsCache();
|
|
715
|
+
}
|
|
716
|
+
_deleteTtsCacheEntry(cacheKey) {
|
|
717
|
+
if (this.ttsCache) {
|
|
718
|
+
this.ttsCache.delete(cacheKey);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
_trimTtsCache() {
|
|
722
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
723
|
+
while (this.ttsCache.size > this.ttsCacheMaxEntries) {
|
|
724
|
+
const oldestKey = this.ttsCache.keys().next().value;
|
|
725
|
+
this.ttsCache.delete(oldestKey);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
_shouldPrefetchTtsText(text) {
|
|
729
|
+
if (!lodash__default["default"].isString(text)) return false;
|
|
730
|
+
if (!text.trim()) return false;
|
|
731
|
+
return !/\$[A-Za-z]\w*/.test(text);
|
|
732
|
+
}
|
|
733
|
+
_maybePrefetchTts(convoStep) {
|
|
734
|
+
if (!this.ttsPrefetchEnabled || !this._isTtsCacheEnabled()) return;
|
|
735
|
+
if (!convoStep || convoStep.sender !== 'me') return;
|
|
736
|
+
if (!this._shouldPrefetchTtsText(convoStep.messageText)) return;
|
|
737
|
+
const ttsRequest = this._buildTtsRequest(convoStep.messageText);
|
|
738
|
+
if (!ttsRequest) return;
|
|
739
|
+
this._getTtsAudio(ttsRequest, convoStep.messageText).catch(err => {
|
|
740
|
+
debug$3(`TTS prefetch failed - ${this._getAxiosErrOutput(err)}`);
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
_buildTtsRequest(text) {
|
|
744
|
+
if (!this.axiosTtsParams) return null;
|
|
745
|
+
return {
|
|
746
|
+
...this.axiosTtsParams,
|
|
747
|
+
params: {
|
|
748
|
+
...(this.axiosTtsParams.params || {}),
|
|
749
|
+
text
|
|
750
|
+
},
|
|
751
|
+
data: this._getBody(Capabilities.VOIP_TTS_BODY),
|
|
752
|
+
responseType: 'arraybuffer'
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
_getTtsCacheKey(ttsRequest) {
|
|
756
|
+
const cacheKeyObj = {
|
|
757
|
+
url: ttsRequest.url,
|
|
758
|
+
method: ttsRequest.method,
|
|
759
|
+
params: ttsRequest.params,
|
|
760
|
+
data: ttsRequest.data
|
|
761
|
+
};
|
|
762
|
+
try {
|
|
763
|
+
return JSON.stringify(cacheKeyObj);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
return `${ttsRequest.url}|${ttsRequest.method}|${lodash__default["default"].get(ttsRequest, 'params.text', '')}`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async _fetchTts(ttsRequest, text) {
|
|
769
|
+
const ttsStart = Date.now();
|
|
770
|
+
const ttsResponse = await axios__default["default"](ttsRequest);
|
|
771
|
+
const ttsMs = Date.now() - ttsStart;
|
|
772
|
+
const textLen = lodash__default["default"].isString(text) ? text.length : 0;
|
|
773
|
+
debug$3(`TTS response ${ttsMs} ms (chars: ${textLen})`);
|
|
774
|
+
if (!ttsResponse || !Buffer.isBuffer(ttsResponse.data)) {
|
|
775
|
+
throw new Error(`TTS failed, response is: ${this._getAxiosShortenedOutput(ttsResponse && ttsResponse.data)}`);
|
|
776
|
+
}
|
|
777
|
+
const durationHeader = ttsResponse.headers && ttsResponse.headers['content-duration'];
|
|
778
|
+
return {
|
|
779
|
+
buffer: ttsResponse.data,
|
|
780
|
+
duration: durationHeader
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
async _getTtsAudio(ttsRequest, text) {
|
|
784
|
+
if (!ttsRequest) throw new Error('TTS request not configured');
|
|
785
|
+
const cacheKey = this._getTtsCacheKey(ttsRequest);
|
|
786
|
+
if (this._isTtsCacheEnabled()) {
|
|
787
|
+
const cached = this.ttsCache.get(cacheKey);
|
|
788
|
+
if (cached) {
|
|
789
|
+
if (cached.state === 'ready') {
|
|
790
|
+
this._touchTtsCache(cacheKey, cached);
|
|
791
|
+
debug$3(`TTS cache hit (chars: ${lodash__default["default"].isString(text) ? text.length : 0})`);
|
|
792
|
+
return {
|
|
793
|
+
buffer: cached.buffer,
|
|
794
|
+
duration: cached.duration
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (cached.state === 'pending') {
|
|
798
|
+
return cached.promise;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const fetchPromise = this._fetchTts(ttsRequest, text).then(result => {
|
|
803
|
+
if (this._isTtsCacheEnabled()) {
|
|
804
|
+
this._setTtsCacheEntry(cacheKey, {
|
|
805
|
+
state: 'ready',
|
|
806
|
+
buffer: result.buffer,
|
|
807
|
+
duration: result.duration
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return result;
|
|
811
|
+
}).catch(err => {
|
|
812
|
+
this._deleteTtsCacheEntry(cacheKey);
|
|
813
|
+
throw err;
|
|
814
|
+
});
|
|
815
|
+
if (this._isTtsCacheEnabled()) {
|
|
816
|
+
this._setTtsCacheEntry(cacheKey, {
|
|
817
|
+
state: 'pending',
|
|
818
|
+
promise: fetchPromise
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
return fetchPromise;
|
|
822
|
+
}
|
|
574
823
|
_getParams(capParams) {
|
|
575
824
|
if (this.caps[capParams]) {
|
|
576
825
|
if (lodash__default["default"].isString(this.caps[capParams])) return JSON.parse(this.caps[capParams]);else return this.caps[capParams];
|