botium-connector-voip 0.0.19 → 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
|
},
|
|
@@ -354,12 +433,25 @@ class BotiumConnectorVoip {
|
|
|
354
433
|
if (!lodash__default["default"].isNil(parsedData.data.start)) {
|
|
355
434
|
const silenceDuration = parsedData.data.start - this.prevData.data.end;
|
|
356
435
|
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
357
|
-
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
|
+
};
|
|
358
444
|
let matched = false;
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
}
|
|
363
455
|
}
|
|
364
456
|
if (matched && this.botMsgs.length > 0) {
|
|
365
457
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
@@ -374,6 +466,7 @@ class BotiumConnectorVoip {
|
|
|
374
466
|
const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
|
|
375
467
|
const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
|
|
376
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.
|
|
377
470
|
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
378
471
|
let botMsg = {
|
|
379
472
|
messageText: parsedData.data.message
|
|
@@ -429,7 +522,7 @@ class BotiumConnectorVoip {
|
|
|
429
522
|
}
|
|
430
523
|
this.botMsgs = [];
|
|
431
524
|
}
|
|
432
|
-
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))) {
|
|
433
526
|
const botMsg = {
|
|
434
527
|
messageText: parsedData.data.message,
|
|
435
528
|
sourceData: parsedData
|
|
@@ -437,15 +530,27 @@ class BotiumConnectorVoip {
|
|
|
437
530
|
this.prevData = parsedData;
|
|
438
531
|
if (successfulConfidenceScore) {
|
|
439
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
|
+
}
|
|
440
545
|
this.silenceTimeout = setTimeout(() => {
|
|
441
546
|
if (this.botMsgs.length > 0) {
|
|
442
|
-
debug$3('Silence Duration Timeout (PSST):',
|
|
547
|
+
debug$3('Silence Duration Timeout (JOIN/PSST):', joinTimeoutMs, 'ms');
|
|
443
548
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
444
549
|
this.firstMsg = false;
|
|
445
550
|
this.joinLastPrevMsg = this.botMsgs[this.botMsgs.length - 1];
|
|
446
551
|
this.botMsgs = [];
|
|
447
552
|
}
|
|
448
|
-
},
|
|
553
|
+
}, joinTimeoutMs || 0);
|
|
449
554
|
}
|
|
450
555
|
}
|
|
451
556
|
}
|
|
@@ -472,23 +577,32 @@ class BotiumConnectorVoip {
|
|
|
472
577
|
});
|
|
473
578
|
this.ws.send(request);
|
|
474
579
|
} else if (msg && msg.messageText) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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'));
|
|
486
600
|
msg.sourceData = ttsRequest;
|
|
487
|
-
let
|
|
601
|
+
let ttsResult = null;
|
|
488
602
|
try {
|
|
489
|
-
|
|
603
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
490
604
|
} catch (err) {
|
|
491
|
-
reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
605
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
492
606
|
}
|
|
493
607
|
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
494
608
|
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
@@ -499,22 +613,28 @@ class BotiumConnectorVoip {
|
|
|
499
613
|
apiKey
|
|
500
614
|
});
|
|
501
615
|
}
|
|
502
|
-
if (Buffer.isBuffer(
|
|
503
|
-
duration =
|
|
616
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
617
|
+
duration = parseFloat(ttsResult.duration) || 0;
|
|
618
|
+
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
504
619
|
const request = JSON.stringify({
|
|
505
620
|
METHOD: 'sendAudio',
|
|
506
621
|
PESQ: false,
|
|
507
622
|
sessionId: this.sessionId,
|
|
508
|
-
b64_buffer:
|
|
623
|
+
b64_buffer: b64Buffer
|
|
509
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
|
+
}
|
|
510
630
|
msg.attachments.push({
|
|
511
631
|
name: 'tts.wav',
|
|
512
632
|
mimeType: 'audio/wav',
|
|
513
|
-
base64:
|
|
633
|
+
base64: b64Buffer
|
|
514
634
|
});
|
|
515
635
|
this.ws.send(request);
|
|
516
636
|
} else {
|
|
517
|
-
reject(new Error(
|
|
637
|
+
return reject(new Error('TTS failed, response is empty'));
|
|
518
638
|
}
|
|
519
639
|
}
|
|
520
640
|
}
|
|
@@ -524,6 +644,11 @@ class BotiumConnectorVoip {
|
|
|
524
644
|
sessionId: this.sessionId,
|
|
525
645
|
b64_buffer: msg.media[0].buffer.toString('base64')
|
|
526
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
|
+
}
|
|
527
652
|
this.ws.send(request);
|
|
528
653
|
msg.attachments.push({
|
|
529
654
|
name: msg.media[0].mediaUri,
|
|
@@ -575,6 +700,126 @@ class BotiumConnectorVoip {
|
|
|
575
700
|
this.firstSttInfoReceived = false;
|
|
576
701
|
}
|
|
577
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
|
+
}
|
|
578
823
|
_getParams(capParams) {
|
|
579
824
|
if (this.caps[capParams]) {
|
|
580
825
|
if (lodash__default["default"].isString(this.caps[capParams])) return JSON.parse(this.caps[capParams]);else return this.caps[capParams];
|