botium-connector-voip 0.0.28 → 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
@@ -557,6 +560,7 @@ class BotiumConnectorVoip {
557
560
  ICE_TURN_PASSWORD: this.caps[Capabilities.VOIP_ICE_TURN_PASSWORD],
558
561
  ICE_TURN_PROTOCOL: this.caps[Capabilities.VOIP_ICE_TURN_PROTOCOL] || 'TCP',
559
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],
560
564
  STT_LEGACY: sttLegacy,
561
565
  STT_CONFIG: {
562
566
  stt_url: sttUrl,
@@ -846,14 +850,15 @@ class BotiumConnectorVoip {
846
850
  this.sttPartialCount++;
847
851
  const partialText = parsedData.data.message;
848
852
  if (typeof partialText === 'string' && partialText.trim().length > 0) {
853
+ const replacementResult = this._applySttDictionaryReplacements(partialText);
849
854
  this.lastPartialBotMsg = {
850
- messageText: partialText,
851
- sourceData: Object.assign({}, parsedData, {
855
+ messageText: replacementResult.text,
856
+ sourceData: Object.assign({}, this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult), {
852
857
  partialRecovery: true
853
858
  })
854
859
  };
855
860
  }
856
- const partialPreview = typeof partialText === 'string' ? partialText.trim().substring(0, 60) : '';
861
+ const partialPreview = typeof partialText === 'string' ? partialText.trim() : '';
857
862
  _info('stt_partial_received', {
858
863
  sessionId: this.sessionId,
859
864
  partialIndex: this.sttPartialCount,
@@ -935,11 +940,13 @@ class BotiumConnectorVoip {
935
940
  const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
936
941
  const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
937
942
  const msgText = parsedData.data.message || '';
943
+ const replacementResult = this._applySttDictionaryReplacements(msgText);
944
+ const normalizedMsgText = replacementResult.text;
938
945
  const msgLen = typeof msgText === 'string' ? msgText.length : 0;
939
- const msgPreview = typeof msgText === 'string' ? msgText.trim().substring(0, 80) : '';
946
+ const msgPreview = typeof normalizedMsgText === 'string' ? normalizedMsgText.trim() : '';
940
947
  // A final supersedes the cached interim; clear to avoid duplicate tail emission.
941
948
  this.lastPartialBotMsg = null;
942
- 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})`);
943
950
  _info('stt_final', {
944
951
  sessionId: this.sessionId,
945
952
  message: msgPreview,
@@ -951,10 +958,10 @@ class BotiumConnectorVoip {
951
958
  // ORIGINAL: always emit final message immediately (ignore JOIN hooks/rules).
952
959
  if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL') {
953
960
  let botMsg = {
954
- messageText: parsedData.data.message
961
+ messageText: normalizedMsgText
955
962
  };
956
963
  if (this.firstMsg) {
957
- const sourceData = parsedData;
964
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
958
965
  sourceData.silenceDuration = parsedData.data.start;
959
966
  sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
960
967
  botMsg = Object.assign({}, botMsg, {
@@ -962,7 +969,7 @@ class BotiumConnectorVoip {
962
969
  });
963
970
  this.firstMsg = false;
964
971
  } else {
965
- const sourceData = parsedData;
972
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
966
973
  const start = lodash__default["default"].get(parsedData, 'data.start', null);
967
974
  const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
968
975
  sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
@@ -980,10 +987,10 @@ class BotiumConnectorVoip {
980
987
  }
981
988
  if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'SPLIT' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'EXPAND') {
982
989
  let botMsg = {
983
- messageText: parsedData.data.message
990
+ messageText: normalizedMsgText
984
991
  };
985
992
  if (this.firstMsg) {
986
- const sourceData = parsedData;
993
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
987
994
  sourceData.silenceDuration = parsedData.data.start;
988
995
  sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
989
996
  botMsg = Object.assign({}, botMsg, {
@@ -991,7 +998,7 @@ class BotiumConnectorVoip {
991
998
  });
992
999
  this.firstMsg = false;
993
1000
  } else {
994
- const sourceData = parsedData;
1001
+ const sourceData = this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult);
995
1002
  const start = lodash__default["default"].get(parsedData, 'data.start', null);
996
1003
  const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
997
1004
  sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
@@ -1010,8 +1017,8 @@ class BotiumConnectorVoip {
1010
1017
  }
1011
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') {
1012
1019
  const botMsg = {
1013
- messageText: parsedData.data.message,
1014
- sourceData: parsedData
1020
+ messageText: normalizedMsgText,
1021
+ sourceData: this._decorateSourceDataWithSttDictionaryReplacements(parsedData, replacementResult)
1015
1022
  };
1016
1023
  this.prevData = parsedData;
1017
1024
  if (successfulConfidenceScore) {
@@ -1521,27 +1528,99 @@ class BotiumConnectorVoip {
1521
1528
  };
1522
1529
  }).filter(Boolean);
1523
1530
  }
1524
- _getJoinRuleBySubstring(convoStep, botMsgs) {
1525
- const texts = [];
1526
- const stepMessageText = lodash__default["default"].get(convoStep, 'messageText', '');
1527
- if (typeof stepMessageText === 'string' && stepMessageText.length > 0) {
1528
- texts.push(stepMessageText);
1529
- }
1530
- if (lodash__default["default"].isArray(botMsgs) && botMsgs.length > 0) {
1531
- const bufferedText = botMsgs.map(m => m && typeof m.messageText === 'string' ? m.messageText : '').filter(Boolean).join(' ');
1532
- if (bufferedText.length > 0) texts.push(bufferedText);
1533
- }
1534
- 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();
1535
1543
  const rules = this._normalizeJoinRulesBySubstring();
1536
- for (const text of texts) {
1537
- const loweredText = text.toLowerCase();
1538
- const match = rules.find(rule => loweredText.includes(rule.substring.toLowerCase()));
1539
- 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
+ }
1540
1557
  }
1541
- 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
+ });
1542
1621
  }
1543
1622
  _hasJoinLogicHookOrRule(convoStep) {
1544
- 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));
1545
1624
  }
1546
1625
  _toJoinTimeoutMs(ms, isPsst) {
1547
1626
  const parsed = parseInt(ms, 10);
@@ -1556,7 +1635,7 @@ class BotiumConnectorVoip {
1556
1635
  const joinHookTimeoutMs = this._toJoinTimeoutMs(joinLogicHook.args[0], isPsst);
1557
1636
  if (lodash__default["default"].isFinite(joinHookTimeoutMs) && joinHookTimeoutMs > 0) return joinHookTimeoutMs;
1558
1637
  }
1559
- const joinRule = this._getJoinRuleBySubstring(convoStep, botMsgs);
1638
+ const joinRule = this._getJoinRuleBySubstring(botMsgs);
1560
1639
  if (joinRule) {
1561
1640
  const joinRuleTimeoutMs = this._toJoinTimeoutMs(joinRule.timeoutMs, isPsst);
1562
1641
  if (lodash__default["default"].isFinite(joinRuleTimeoutMs) && joinRuleTimeoutMs > 0) return joinRuleTimeoutMs;
@@ -1654,6 +1733,18 @@ var botiumConnectorVoip = {
1654
1733
  type: 'json',
1655
1734
  required: false,
1656
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
1657
1748
  }]
1658
1749
  },
1659
1750
  PluginLogicHooks: {