botium-connector-voip 0.0.27 → 0.0.29

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.
@@ -30,7 +30,7 @@ const _info = (event, data) => {
30
30
  const parts = Object.entries({
31
31
  event,
32
32
  ...data
33
- }).filter(([, v]) => v != null && v !== '').map(([k, v]) => `${k}=${typeof v === 'string' && v.length > 80 ? `"${v.substring(0, 77)}..."` : JSON.stringify(v)}`);
33
+ }).filter(([, v]) => v != null && v !== '').map(([k, v]) => `${k}=${JSON.stringify(v)}`);
34
34
  console.info(`[botium-connector-voip] ${parts.join(' ')}`);
35
35
  };
36
36
 
@@ -51,6 +51,7 @@ const Capabilities = {
51
51
  VOIP_STT_MESSAGE_HANDLING_TIMEOUT: 'VOIP_STT_MESSAGE_HANDLING_TIMEOUT',
52
52
  VOIP_STT_MESSAGE_HANDLING_TIMEOUT_SUBSEQUENT: 'VOIP_STT_MESSAGE_HANDLING_TIMEOUT_SUBSEQUENT',
53
53
  VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING: 'VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING',
54
+ VOIP_STT_DICTIONARY_REPLACEMENTS: 'VOIP_STT_DICTIONARY_REPLACEMENTS',
54
55
  VOIP_STT_MESSAGE_HANDLING_DELIMITER: 'VOIP_STT_MESSAGE_HANDLING_DELIMITER',
55
56
  VOIP_STT_MESSAGE_HANDLING_PUNCTUATION: 'VOIP_STT_MESSAGE_HANDLING_PUNCTUATION',
56
57
  VOIP_TTS_URL: 'VOIP_TTS_URL',
@@ -90,7 +91,8 @@ const Capabilities = {
90
91
  VOIP_STT_CONFIDENCE_THRESHOLD: 'VOIP_STT_CONFIDENCE_THRESHOLD',
91
92
  VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER',
92
93
  VOIP_USER_INPUT_PREFER_VOICE: 'VOIP_USER_INPUT_PREFER_VOICE',
93
- VOIP_EMIT_SPECULATIVE_TEXT: 'VOIP_EMIT_SPECULATIVE_TEXT'
94
+ VOIP_EMIT_SPECULATIVE_TEXT: 'VOIP_EMIT_SPECULATIVE_TEXT',
95
+ VOIP_SDP_MEDIA_TYPE_TEXT_ENABLE: 'VOIP_SDP_MEDIA_TYPE_TEXT_ENABLE'
94
96
  };
95
97
  const Defaults = {
96
98
  VOIP_STT_METHOD: 'POST',
@@ -113,7 +115,8 @@ const Defaults = {
113
115
  VOIP_STT_CONFIDENCE_THRESHOLD: 0.5,
114
116
  VOIP_USE_GLOBAL_VOIP_WORKER: false,
115
117
  VOIP_SIP_PROTOCOL: 'TCP',
116
- VOIP_USER_INPUT_PREFER_VOICE: true
118
+ VOIP_USER_INPUT_PREFER_VOICE: true,
119
+ VOIP_SDP_MEDIA_TYPE_TEXT_ENABLE: false
117
120
  };
118
121
  const TTS_HTTP_AGENT = new http__default["default"].Agent({
119
122
  keepAlive: true
@@ -184,6 +187,7 @@ class BotiumConnectorVoip {
184
187
  debug$3(this.caps[Capabilities.VOIP_TTS_URL]);
185
188
  this.stopCalled = false;
186
189
  this.fullRecord = '';
190
+ this.fullRecordAttachmentEmitted = false;
187
191
  this.end = false;
188
192
  this.connected = false;
189
193
  this.convoStep = null;
@@ -556,6 +560,7 @@ class BotiumConnectorVoip {
556
560
  ICE_TURN_PASSWORD: this.caps[Capabilities.VOIP_ICE_TURN_PASSWORD],
557
561
  ICE_TURN_PROTOCOL: this.caps[Capabilities.VOIP_ICE_TURN_PROTOCOL] || 'TCP',
558
562
  MIN_SILENCE_DURATION: this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT_ENABLE] ? this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT] : null,
563
+ SDP_MEDIA_TYPE_TEXT_ENABLE: !!this.caps[Capabilities.VOIP_SDP_MEDIA_TYPE_TEXT_ENABLE],
559
564
  STT_LEGACY: sttLegacy,
560
565
  STT_CONFIG: {
561
566
  stt_url: sttUrl,
@@ -703,6 +708,18 @@ class BotiumConnectorVoip {
703
708
  testSessionJobId: this.caps.VOIP_TEST_SESSION_JOB_ID || null
704
709
  }
705
710
  });
711
+ this.fullRecordAttachmentEmitted = true;
712
+ };
713
+ this._emitBufferedFullRecordIfAny = reason => {
714
+ if (this.fullRecordAttachmentEmitted) return false;
715
+ if (!this.fullRecord || typeof this.fullRecord !== 'string' || this.fullRecord.length === 0) return false;
716
+ emitFullRecordAttachment(this.fullRecord);
717
+ _info('recording_attached', {
718
+ sessionId: this.sessionId,
719
+ source: reason,
720
+ base64Len: this.fullRecord.length
721
+ });
722
+ return true;
706
723
  };
707
724
  if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
708
725
  this.sessionId = parsedData.voipConfig.sessionId;
@@ -749,6 +766,9 @@ class BotiumConnectorVoip {
749
766
  sttPartialCount: this.sttPartialCount
750
767
  });
751
768
  flushPendingBotMsgs('callinfo_disconnected');
769
+ // Some workers may disconnect without sending fullRecordEnd.
770
+ // If we already buffered full-record chunks, emit them now.
771
+ this._emitBufferedFullRecordIfAny('callinfo_disconnected_buffered');
752
772
  const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_STT_BODY));
753
773
  if (parsedData.connectDuration && parsedData.connectDuration > 0) {
754
774
  this.eventEmitter.emit('CONSUMPTION_METADATA', this, {
@@ -769,6 +789,8 @@ class BotiumConnectorVoip {
769
789
  }
770
790
  if (parsedData && parsedData.type === 'error') {
771
791
  flushPendingBotMsgs('error');
792
+ // Ensure buffered recording is not lost on terminal worker errors.
793
+ this._emitBufferedFullRecordIfAny('error_buffered');
772
794
  _info('ws_error_msg', {
773
795
  sessionId: this.sessionId,
774
796
  message: parsedData.message || null,
@@ -828,14 +850,15 @@ class BotiumConnectorVoip {
828
850
  this.sttPartialCount++;
829
851
  const partialText = parsedData.data.message;
830
852
  if (typeof partialText === 'string' && partialText.trim().length > 0) {
853
+ const replacementResult = this._applySttDictionaryReplacements(partialText);
831
854
  this.lastPartialBotMsg = {
832
- messageText: partialText,
833
- sourceData: Object.assign({}, parsedData, {
855
+ messageText: replacementResult.text,
856
+ sourceData: Object.assign({}, this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult), {
834
857
  partialRecovery: true
835
858
  })
836
859
  };
837
860
  }
838
- const partialPreview = typeof partialText === 'string' ? partialText.trim().substring(0, 60) : '';
861
+ const partialPreview = typeof partialText === 'string' ? partialText.trim() : '';
839
862
  _info('stt_partial_received', {
840
863
  sessionId: this.sessionId,
841
864
  partialIndex: this.sttPartialCount,
@@ -917,11 +940,13 @@ class BotiumConnectorVoip {
917
940
  const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
918
941
  const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
919
942
  const msgText = parsedData.data.message || '';
943
+ const replacementResult = this._applySttDictionaryReplacements(msgText);
944
+ const normalizedMsgText = replacementResult.text;
920
945
  const msgLen = typeof msgText === 'string' ? msgText.length : 0;
921
- const msgPreview = typeof msgText === 'string' ? msgText.trim().substring(0, 80) : '';
946
+ const msgPreview = typeof normalizedMsgText === 'string' ? normalizedMsgText.trim() : '';
922
947
  // A final supersedes the cached interim; clear to avoid duplicate tail emission.
923
948
  this.lastPartialBotMsg = null;
924
- debug$3(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
949
+ debug$3(`Message: ${normalizedMsgText} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
925
950
  _info('stt_final', {
926
951
  sessionId: this.sessionId,
927
952
  message: msgPreview,
@@ -933,10 +958,10 @@ class BotiumConnectorVoip {
933
958
  // ORIGINAL: always emit final message immediately (ignore JOIN hooks/rules).
934
959
  if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL') {
935
960
  let botMsg = {
936
- messageText: parsedData.data.message
961
+ messageText: normalizedMsgText
937
962
  };
938
963
  if (this.firstMsg) {
939
- const sourceData = parsedData;
964
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
940
965
  sourceData.silenceDuration = parsedData.data.start;
941
966
  sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
942
967
  botMsg = Object.assign({}, botMsg, {
@@ -944,7 +969,7 @@ class BotiumConnectorVoip {
944
969
  });
945
970
  this.firstMsg = false;
946
971
  } else {
947
- const sourceData = parsedData;
972
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
948
973
  const start = lodash__default["default"].get(parsedData, 'data.start', null);
949
974
  const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
950
975
  sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
@@ -962,10 +987,10 @@ class BotiumConnectorVoip {
962
987
  }
963
988
  if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'SPLIT' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'EXPAND') {
964
989
  let botMsg = {
965
- messageText: parsedData.data.message
990
+ messageText: normalizedMsgText
966
991
  };
967
992
  if (this.firstMsg) {
968
- const sourceData = parsedData;
993
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
969
994
  sourceData.silenceDuration = parsedData.data.start;
970
995
  sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
971
996
  botMsg = Object.assign({}, botMsg, {
@@ -973,7 +998,7 @@ class BotiumConnectorVoip {
973
998
  });
974
999
  this.firstMsg = false;
975
1000
  } else {
976
- const sourceData = parsedData;
1001
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
977
1002
  const start = lodash__default["default"].get(parsedData, 'data.start', null);
978
1003
  const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
979
1004
  sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
@@ -992,8 +1017,8 @@ class BotiumConnectorVoip {
992
1017
  }
993
1018
  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') {
994
1019
  const botMsg = {
995
- messageText: parsedData.data.message,
996
- sourceData: parsedData
1020
+ messageText: normalizedMsgText,
1021
+ sourceData: this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult)
997
1022
  };
998
1023
  this.prevData = parsedData;
999
1024
  if (successfulConfidenceScore) {
@@ -1279,13 +1304,22 @@ class BotiumConnectorVoip {
1279
1304
  }
1280
1305
  }, 100);
1281
1306
  });
1307
+ // Final guard: if worker never emitted fullRecordEnd/fullRecord but
1308
+ // chunks were buffered, publish the attachment before teardown.
1309
+ if (typeof this._emitBufferedFullRecordIfAny === 'function') {
1310
+ this._emitBufferedFullRecordIfAny('stop_final_guard');
1311
+ }
1282
1312
  } else {
1283
1313
  this.wsOpened = false;
1284
1314
  this.ws = null;
1285
1315
  this.end = false;
1286
1316
  this.convoStep = null;
1287
1317
  this.firstSttInfoReceived = false;
1318
+ if (typeof this._emitBufferedFullRecordIfAny === 'function') {
1319
+ this._emitBufferedFullRecordIfAny('stop_final_guard');
1320
+ }
1288
1321
  }
1322
+ this._emitBufferedFullRecordIfAny = null;
1289
1323
  }
1290
1324
  _isTtsCacheEnabled() {
1291
1325
  return this.ttsCacheEnabled && this.ttsCacheMaxEntries > 0;
@@ -1494,27 +1528,99 @@ class BotiumConnectorVoip {
1494
1528
  };
1495
1529
  }).filter(Boolean);
1496
1530
  }
1497
- _getJoinRuleBySubstring(convoStep, botMsgs) {
1498
- const texts = [];
1499
- const stepMessageText = lodash__default["default"].get(convoStep, 'messageText', '');
1500
- if (typeof stepMessageText === 'string' && stepMessageText.length > 0) {
1501
- texts.push(stepMessageText);
1502
- }
1503
- if (lodash__default["default"].isArray(botMsgs) && botMsgs.length > 0) {
1504
- const bufferedText = botMsgs.map(m => m && typeof m.messageText === 'string' ? m.messageText : '').filter(Boolean).join(' ');
1505
- if (bufferedText.length > 0) texts.push(bufferedText);
1506
- }
1507
- if (texts.length === 0) return null;
1531
+
1532
+ // Matches a substring rule against ONLY the latest buffered STT final chunk.
1533
+ // This keeps the rule's custom timeout in effect for just the one following
1534
+ // final: once a final no longer matches, callers fall back to the default
1535
+ // timeout (the convoStep expected text and the cumulative buffer are
1536
+ // deliberately not considered, so a stale match cannot stick).
1537
+ _getJoinRuleBySubstring(botMsgs) {
1538
+ if (!lodash__default["default"].isArray(botMsgs) || botMsgs.length === 0) return null;
1539
+ const last = botMsgs[botMsgs.length - 1];
1540
+ const text = last && typeof last.messageText === 'string' ? last.messageText : '';
1541
+ if (!text) return null;
1542
+ const loweredText = text.toLowerCase();
1508
1543
  const rules = this._normalizeJoinRulesBySubstring();
1509
- for (const text of texts) {
1510
- const loweredText = text.toLowerCase();
1511
- const match = rules.find(rule => loweredText.includes(rule.substring.toLowerCase()));
1512
- if (match) return match;
1544
+ return rules.find(rule => loweredText.includes(rule.substring.toLowerCase())) || null;
1545
+ }
1546
+ _normalizeSttDictionaryReplacements() {
1547
+ const rawRules = this.caps[Capabilities.VOIP_STT_DICTIONARY_REPLACEMENTS];
1548
+ if (lodash__default["default"].isNil(rawRules) || rawRules === '') return [];
1549
+ let parsedRules = rawRules;
1550
+ if (lodash__default["default"].isString(rawRules)) {
1551
+ try {
1552
+ parsedRules = JSON.parse(rawRules);
1553
+ } catch (err) {
1554
+ debug$3(`Invalid ${Capabilities.VOIP_STT_DICTIONARY_REPLACEMENTS} JSON: ${err.message || err}`);
1555
+ return [];
1556
+ }
1513
1557
  }
1514
- return null;
1558
+ if (!lodash__default["default"].isArray(parsedRules)) {
1559
+ debug$3(`Invalid ${Capabilities.VOIP_STT_DICTIONARY_REPLACEMENTS}: expected array`);
1560
+ return [];
1561
+ }
1562
+ return parsedRules.map(rule => {
1563
+ if (!rule || typeof rule !== 'object') return null;
1564
+ const fromValues = lodash__default["default"].isArray(rule.from) ? rule.from : [rule.from];
1565
+ const from = lodash__default["default"].uniq(fromValues.map(value => value != null ? String(value).trim() : '').filter(Boolean));
1566
+ const to = rule.to != null ? String(rule.to).trim() : '';
1567
+ if (from.length === 0 || !to) return null;
1568
+ return {
1569
+ from,
1570
+ to
1571
+ };
1572
+ }).filter(Boolean);
1573
+ }
1574
+ _applySttDictionaryReplacements(text) {
1575
+ if (!lodash__default["default"].isString(text) || text.length === 0) return {
1576
+ text,
1577
+ applied: []
1578
+ };
1579
+ const replacements = this._normalizeSttDictionaryReplacements();
1580
+ if (replacements.length === 0) return {
1581
+ text,
1582
+ applied: []
1583
+ };
1584
+ const replacementsByFrom = new Map();
1585
+ const fromAlternatives = [];
1586
+ replacements.forEach(rule => {
1587
+ rule.from.forEach(from => {
1588
+ const fromKey = from.toLowerCase();
1589
+ if (!replacementsByFrom.has(fromKey)) {
1590
+ replacementsByFrom.set(fromKey, rule.to);
1591
+ fromAlternatives.push(from);
1592
+ }
1593
+ });
1594
+ });
1595
+ if (fromAlternatives.length === 0) return {
1596
+ text,
1597
+ applied: []
1598
+ };
1599
+ fromAlternatives.sort((a, b) => b.length - a.length);
1600
+ const matcher = new RegExp(fromAlternatives.map(value => lodash__default["default"].escapeRegExp(value)).join('|'), 'gi');
1601
+ const applied = [];
1602
+ const replacedText = text.replace(matcher, match => {
1603
+ const to = replacementsByFrom.get(match.toLowerCase());
1604
+ applied.push({
1605
+ from: match,
1606
+ to
1607
+ });
1608
+ return to;
1609
+ });
1610
+ return {
1611
+ text: replacedText,
1612
+ applied
1613
+ };
1614
+ }
1615
+ _decorateSourceDataWithSttDictionaryReplacements(sourceData, replacementResult) {
1616
+ if (!replacementResult || !lodash__default["default"].isArray(replacementResult.applied) || replacementResult.applied.length === 0) return sourceData;
1617
+ return Object.assign({}, sourceData, {
1618
+ sttDictionaryOriginalMessage: replacementResult.text === sourceData?.data?.message ? undefined : sourceData?.data?.message,
1619
+ sttDictionaryReplacements: replacementResult.applied
1620
+ });
1515
1621
  }
1516
1622
  _hasJoinLogicHookOrRule(convoStep) {
1517
- return !lodash__default["default"].isNil(this._getJoinLogicHook(convoStep)) || !lodash__default["default"].isNil(this._getJoinRuleBySubstring(convoStep, this.botMsgs));
1623
+ return !lodash__default["default"].isNil(this._getJoinLogicHook(convoStep)) || !lodash__default["default"].isNil(this._getJoinRuleBySubstring(this.botMsgs));
1518
1624
  }
1519
1625
  _toJoinTimeoutMs(ms, isPsst) {
1520
1626
  const parsed = parseInt(ms, 10);
@@ -1529,7 +1635,7 @@ class BotiumConnectorVoip {
1529
1635
  const joinHookTimeoutMs = this._toJoinTimeoutMs(joinLogicHook.args[0], isPsst);
1530
1636
  if (lodash__default["default"].isFinite(joinHookTimeoutMs) && joinHookTimeoutMs > 0) return joinHookTimeoutMs;
1531
1637
  }
1532
- const joinRule = this._getJoinRuleBySubstring(convoStep, botMsgs);
1638
+ const joinRule = this._getJoinRuleBySubstring(botMsgs);
1533
1639
  if (joinRule) {
1534
1640
  const joinRuleTimeoutMs = this._toJoinTimeoutMs(joinRule.timeoutMs, isPsst);
1535
1641
  if (lodash__default["default"].isFinite(joinRuleTimeoutMs) && joinRuleTimeoutMs > 0) return joinRuleTimeoutMs;
@@ -1627,6 +1733,18 @@ var botiumConnectorVoip = {
1627
1733
  type: 'json',
1628
1734
  required: false,
1629
1735
  advanced: true
1736
+ }, {
1737
+ name: 'VOIP_STT_DICTIONARY_REPLACEMENTS',
1738
+ label: 'STT dictionary replacements',
1739
+ type: 'json',
1740
+ required: false,
1741
+ advanced: true
1742
+ }, {
1743
+ name: 'VOIP_SDP_MEDIA_TYPE_TEXT_ENABLE',
1744
+ label: 'Enable SDP media type text',
1745
+ type: 'boolean',
1746
+ required: false,
1747
+ advanced: true
1630
1748
  }]
1631
1749
  },
1632
1750
  PluginLogicHooks: {