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
- 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
  },
@@ -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 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
+ };
354
444
  let matched = false;
355
- if ((!lodash__default["default"].isNil(joinLogicHook) && isJoinMethod && silenceDuration > parseInt(joinLogicHook.args[0])) / 1000) {
356
- matched = true;
357
- } else if (lodash__default["default"].isNil(joinLogicHook) && isJoinMethod && silenceDuration > parseInt(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]) / 1000) {
358
- 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
+ }
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):', 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');
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
- }, this._getJoinLogicHook(this.convoStep) && parseInt(this._getJoinLogicHook(this.convoStep).args[0]) || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
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
- if (!this.axiosTtsParams) reject(new Error('TTS not configured, only audio input supported'));
472
- if (this.axiosTtsParams) {
473
- const ttsRequest = {
474
- ...this.axiosTtsParams,
475
- params: {
476
- ...(this.axiosTtsParams.params || {}),
477
- text: msg.messageText
478
- },
479
- data: this._getBody(Capabilities.VOIP_TTS_BODY),
480
- responseType: 'arraybuffer'
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 ttsResponse = null;
601
+ let ttsResult = null;
484
602
  try {
485
- ttsResponse = await axios__default["default"](ttsRequest);
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(ttsResponse.data)) {
499
- 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');
500
619
  const request = JSON.stringify({
501
620
  METHOD: 'sendAudio',
502
621
  PESQ: false,
503
622
  sessionId: this.sessionId,
504
- b64_buffer: ttsResponse.data.toString('base64')
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: ttsResponse.data.toString('base64')
633
+ base64: b64Buffer
510
634
  });
511
635
  this.ws.send(request);
512
636
  } else {
513
- reject(new Error(`TTS failed, response is: ${this._getAxiosShortenedOutput(ttsResponse.data)}`));
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];