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
- this.caps = Object.assign({}, Defaults, this.caps);
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: this.caps[Capabilities.VOIP_STT_URL_STREAM],
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 isJoinMethod = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'JOIN';
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 ((!lodash__default["default"].isNil(joinLogicHook) && isJoinMethod && silenceDuration > parseInt(joinLogicHook.args[0])) / 1000) {
360
- matched = true;
361
- } else if (lodash__default["default"].isNil(joinLogicHook) && isJoinMethod && silenceDuration > parseInt(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]) / 1000) {
362
- matched = true;
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):', this._getJoinLogicHook(this.convoStep) && parseInt(this._getJoinLogicHook(this.convoStep).args[0]) || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT], 'ms');
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
- }, this._getJoinLogicHook(this.convoStep) && parseInt(this._getJoinLogicHook(this.convoStep).args[0]) || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
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
- if (!this.axiosTtsParams) reject(new Error('TTS not configured, only audio input supported'));
476
- if (this.axiosTtsParams) {
477
- const ttsRequest = {
478
- ...this.axiosTtsParams,
479
- params: {
480
- ...(this.axiosTtsParams.params || {}),
481
- text: msg.messageText
482
- },
483
- data: this._getBody(Capabilities.VOIP_TTS_BODY),
484
- responseType: 'arraybuffer'
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 ttsResponse = null;
601
+ let ttsResult = null;
488
602
  try {
489
- ttsResponse = await axios__default["default"](ttsRequest);
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(ttsResponse.data)) {
503
- duration = ttsResponse.headers['content-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: ttsResponse.data.toString('base64')
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: ttsResponse.data.toString('base64')
633
+ base64: b64Buffer
514
634
  });
515
635
  this.ws.send(request);
516
636
  } else {
517
- reject(new Error(`TTS failed, response is: ${this._getAxiosShortenedOutput(ttsResponse.data)}`));
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];