botium-connector-voip 0.0.26 → 0.0.27
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.
- package/dist/botium-connector-voip-cjs.js +241 -186
- package/dist/botium-connector-voip-cjs.js.map +1 -1
- package/dist/botium-connector-voip-es.js +241 -186
- package/dist/botium-connector-voip-es.js.map +1 -1
- package/index.js +7 -0
- package/package.json +1 -1
- package/src/connector.js +245 -186
|
@@ -50,6 +50,7 @@ const Capabilities = {
|
|
|
50
50
|
VOIP_STT_MESSAGE_HANDLING: 'VOIP_STT_MESSAGE_HANDLING',
|
|
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
|
+
VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING: 'VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING',
|
|
53
54
|
VOIP_STT_MESSAGE_HANDLING_DELIMITER: 'VOIP_STT_MESSAGE_HANDLING_DELIMITER',
|
|
54
55
|
VOIP_STT_MESSAGE_HANDLING_PUNCTUATION: 'VOIP_STT_MESSAGE_HANDLING_PUNCTUATION',
|
|
55
56
|
VOIP_TTS_URL: 'VOIP_TTS_URL',
|
|
@@ -250,19 +251,9 @@ class BotiumConnectorVoip {
|
|
|
250
251
|
const armJoinSilenceTimer = () => {
|
|
251
252
|
if (!this.botMsgs || this.botMsgs.length === 0) return;
|
|
252
253
|
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
253
|
-
const
|
|
254
|
-
const isJoinMethod = sttHandling === 'JOIN' || sttHandling === 'PSST' || sttHandling === 'CONCAT' || !lodash__default["default"].isNil(joinLogicHook);
|
|
254
|
+
const isJoinMethod = sttHandling === 'JOIN' || sttHandling === 'PSST' || sttHandling === 'CONCAT' || this._hasJoinLogicHookOrRule(this.convoStep);
|
|
255
255
|
if (!isJoinMethod) return;
|
|
256
|
-
const
|
|
257
|
-
const toJoinTimeoutMs = ms => {
|
|
258
|
-
const parsed = parseInt(ms, 10);
|
|
259
|
-
if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
|
|
260
|
-
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
261
|
-
};
|
|
262
|
-
let joinTimeoutMs = toJoinTimeoutMs(lodash__default["default"].get(joinLogicHook, 'args[0]'));
|
|
263
|
-
if (!lodash__default["default"].isFinite(joinTimeoutMs) || joinTimeoutMs <= 0) {
|
|
264
|
-
joinTimeoutMs = toJoinTimeoutMs(this._getEffectiveMessageHandlingTimeout());
|
|
265
|
-
}
|
|
256
|
+
const joinTimeoutMs = this._getEffectiveJoinTimeoutMs(this.convoStep, this.botMsgs);
|
|
266
257
|
if (this.silenceTimeout) {
|
|
267
258
|
clearTimeout(this.silenceTimeout);
|
|
268
259
|
this.silenceTimeout = null;
|
|
@@ -505,19 +496,7 @@ class BotiumConnectorVoip {
|
|
|
505
496
|
// For PSST: send join silence duration per step to VOIP worker (controls PSST silence trigger)
|
|
506
497
|
try {
|
|
507
498
|
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'PSST' && this.ws && this.ws.readyState === ws__default["default"].OPEN) {
|
|
508
|
-
const
|
|
509
|
-
let silenceMs = null;
|
|
510
|
-
if (joinLogicHook && joinLogicHook.args && joinLogicHook.args.length > 0) {
|
|
511
|
-
silenceMs = parseInt(joinLogicHook.args[0], 10);
|
|
512
|
-
}
|
|
513
|
-
// Fallback to effective timeout (subsequent if past first flush, else global)
|
|
514
|
-
if (!lodash__default["default"].isFinite(silenceMs) || silenceMs <= 0) {
|
|
515
|
-
silenceMs = parseInt(this._getEffectiveMessageHandlingTimeout(), 10);
|
|
516
|
-
}
|
|
517
|
-
// PSST: treat like JOIN, but subtract 500ms from timeout
|
|
518
|
-
if (lodash__default["default"].isFinite(silenceMs) && silenceMs > 0) {
|
|
519
|
-
silenceMs = Math.max(0, silenceMs - 500);
|
|
520
|
-
}
|
|
499
|
+
const silenceMs = this._getEffectiveJoinTimeoutMs(convoStep, this.botMsgs);
|
|
521
500
|
if (lodash__default["default"].isFinite(silenceMs) && silenceMs > 0 && this.sessionId) {
|
|
522
501
|
debug$3(`PSST: sending silenceDurationMs=${silenceMs} for sessionId=${this.sessionId}`);
|
|
523
502
|
this.ws.send(JSON.stringify({
|
|
@@ -826,7 +805,7 @@ class BotiumConnectorVoip {
|
|
|
826
805
|
}
|
|
827
806
|
if (parsedData && parsedData.type === 'silence') {
|
|
828
807
|
if (lodash__default["default"].isNil(this._getIgnoreSilenceDurationAsserterLogicHook(this.convoStep))) {
|
|
829
|
-
if (
|
|
808
|
+
if (!this._hasJoinLogicHookOrRule(this.convoStep) && parsedData.data.silence.length > 0) {
|
|
830
809
|
flushPendingBotMsgs('silence_exceeded');
|
|
831
810
|
this.end = true;
|
|
832
811
|
sendBotMsg(new Error(`Silence Duration of ${parsedData.data.silence[0][2].toFixed(2)}s exceeded General Silence Duration Timeout of ${this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT] / 1000}s`));
|
|
@@ -907,7 +886,7 @@ class BotiumConnectorVoip {
|
|
|
907
886
|
}
|
|
908
887
|
}
|
|
909
888
|
if (lodash__default["default"].isNil(this._getIgnoreSilenceDurationAsserterLogicHook(this.convoStep))) {
|
|
910
|
-
if (!this.firstSttInfoReceived &&
|
|
889
|
+
if (!this.firstSttInfoReceived && !this._hasJoinLogicHookOrRule(this.convoStep) && this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE] && parsedData.data.start && parsedData.data.start * 1000 > this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT_START]) {
|
|
911
890
|
this.end = true;
|
|
912
891
|
sendBotMsg(new Error(`Silence Duration of ${parsedData.data.start}s exceeded Initial Silence Duration Timeout of ${this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT_START] / 1000}s`));
|
|
913
892
|
}
|
|
@@ -916,23 +895,11 @@ class BotiumConnectorVoip {
|
|
|
916
895
|
if (this.prevData && this.prevData.data && !lodash__default["default"].isNil(this.prevData.data.end)) {
|
|
917
896
|
if (!lodash__default["default"].isNil(parsedData.data.start)) {
|
|
918
897
|
const silenceDuration = parsedData.data.start - this.prevData.data.end;
|
|
919
|
-
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
920
898
|
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
921
|
-
const
|
|
922
|
-
const isJoinMethod = sttHandling === 'JOIN' || isPsst;
|
|
923
|
-
const toJoinTimeoutMs = ms => {
|
|
924
|
-
const parsed = parseInt(ms, 10);
|
|
925
|
-
if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
|
|
926
|
-
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
927
|
-
};
|
|
899
|
+
const isJoinMethod = sttHandling === 'JOIN' || sttHandling === 'PSST';
|
|
928
900
|
let matched = false;
|
|
929
|
-
if (
|
|
930
|
-
const joinTimeoutMs =
|
|
931
|
-
if (lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
932
|
-
matched = true;
|
|
933
|
-
}
|
|
934
|
-
} else if (lodash__default["default"].isNil(joinLogicHook) && isJoinMethod) {
|
|
935
|
-
const joinTimeoutMs = toJoinTimeoutMs(this._getEffectiveMessageHandlingTimeout());
|
|
901
|
+
if (isJoinMethod) {
|
|
902
|
+
const joinTimeoutMs = this._getEffectiveJoinTimeoutMs(this.convoStep, this.botMsgs);
|
|
936
903
|
if (lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
937
904
|
matched = true;
|
|
938
905
|
}
|
|
@@ -963,8 +930,8 @@ class BotiumConnectorVoip {
|
|
|
963
930
|
threshold: confidenceThreshold,
|
|
964
931
|
accepted: successfulConfidenceScore
|
|
965
932
|
});
|
|
966
|
-
// ORIGINAL: emit final message immediately
|
|
967
|
-
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL'
|
|
933
|
+
// ORIGINAL: always emit final message immediately (ignore JOIN hooks/rules).
|
|
934
|
+
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL') {
|
|
968
935
|
let botMsg = {
|
|
969
936
|
messageText: parsedData.data.message
|
|
970
937
|
};
|
|
@@ -1023,7 +990,7 @@ class BotiumConnectorVoip {
|
|
|
1023
990
|
}
|
|
1024
991
|
this.botMsgs = [];
|
|
1025
992
|
}
|
|
1026
|
-
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'
|
|
993
|
+
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') {
|
|
1027
994
|
const botMsg = {
|
|
1028
995
|
messageText: parsedData.data.message,
|
|
1029
996
|
sourceData: parsedData
|
|
@@ -1102,58 +1069,35 @@ class BotiumConnectorVoip {
|
|
|
1102
1069
|
}
|
|
1103
1070
|
return new Promise((resolve, reject) => {
|
|
1104
1071
|
setTimeout(async () => {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
let duration = 0;
|
|
1074
|
+
const preferVoiceCapRaw = this.caps[Capabilities.VOIP_USER_INPUT_PREFER_VOICE];
|
|
1075
|
+
const preferVoice = !!preferVoiceCapRaw;
|
|
1076
|
+
const skipTtsForMixedInput = preferVoice && hasText && hasVoiceMedia;
|
|
1077
|
+
debug$3(`UserSays routing: hasText=${hasText} hasVoiceMedia=${hasVoiceMedia} preferVoice=${preferVoice} preferVoiceRaw=${JSON.stringify(preferVoiceCapRaw)} skipTtsForMixedInput=${skipTtsForMixedInput}`);
|
|
1110
1078
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1079
|
+
// Stamp `msg.voipAgent` at the moment bytes leave the WebSocket so
|
|
1080
|
+
// the coach can place the agent turn on the recording timeline.
|
|
1081
|
+
// `requestedDurationMs` is the best estimate of on-wire playback
|
|
1082
|
+
// length (DTMF tones × digits, TTS synth output, parsed media duration).
|
|
1083
|
+
const stampAgentWire = (wireKind, requestedDurationMs, extras = {}) => {
|
|
1084
|
+
msg.voipAgent = {
|
|
1085
|
+
wireSentAtMs: Date.now(),
|
|
1086
|
+
inputType,
|
|
1087
|
+
wireKind,
|
|
1088
|
+
requestedDurationMs: Math.max(0, Math.round(requestedDurationMs || 0)),
|
|
1089
|
+
...extras
|
|
1090
|
+
};
|
|
1122
1091
|
};
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const digits = sanitizeDtmfDigits(msg.buttons[0].payload);
|
|
1128
|
-
if (!digits) {
|
|
1129
|
-
debug$3('sendDtmf skipped: no valid DTMF digits after sanitizing button payload');
|
|
1130
|
-
return resolve();
|
|
1131
|
-
}
|
|
1132
|
-
debug$3(`Sending DTMF digits: ${digits}`);
|
|
1133
|
-
const request = JSON.stringify({
|
|
1134
|
-
METHOD: 'sendDtmf',
|
|
1135
|
-
digits,
|
|
1136
|
-
sessionId: this.sessionId
|
|
1137
|
-
});
|
|
1138
|
-
stampAgentWire('dtmf', digits.length * DTMF_MS_PER_DIGIT, {
|
|
1139
|
-
digitCount: digits.length
|
|
1140
|
-
});
|
|
1141
|
-
this.ws.send(request);
|
|
1142
|
-
} else if (msg && msg.messageText) {
|
|
1143
|
-
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
1144
|
-
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
1145
|
-
if (dtmfMatch && dtmfMatch[1]) {
|
|
1146
|
-
const rawDigits = dtmfMatch[1];
|
|
1147
|
-
const digits = sanitizeDtmfDigits(rawDigits);
|
|
1092
|
+
// Twilio default: 100 ms tone + 100 ms gap per digit. Drives agent-bar width only.
|
|
1093
|
+
const DTMF_MS_PER_DIGIT = 200;
|
|
1094
|
+
if (msg && msg.buttons && msg.buttons.length > 0) {
|
|
1095
|
+
const digits = sanitizeDtmfDigits(msg.buttons[0].payload);
|
|
1148
1096
|
if (!digits) {
|
|
1149
|
-
debug$3(
|
|
1097
|
+
debug$3('sendDtmf skipped: no valid DTMF digits after sanitizing button payload');
|
|
1150
1098
|
return resolve();
|
|
1151
1099
|
}
|
|
1152
|
-
|
|
1153
|
-
debug$3(`Sending DTMF from messageText (sanitized): "${rawDigits}" -> "${digits}"`);
|
|
1154
|
-
} else {
|
|
1155
|
-
debug$3(`Sending DTMF from messageText: ${digits}`);
|
|
1156
|
-
}
|
|
1100
|
+
debug$3(`Sending DTMF digits: ${digits}`);
|
|
1157
1101
|
const request = JSON.stringify({
|
|
1158
1102
|
METHOD: 'sendDtmf',
|
|
1159
1103
|
digits,
|
|
@@ -1162,114 +1106,150 @@ class BotiumConnectorVoip {
|
|
|
1162
1106
|
stampAgentWire('dtmf', digits.length * DTMF_MS_PER_DIGIT, {
|
|
1163
1107
|
digitCount: digits.length
|
|
1164
1108
|
});
|
|
1165
|
-
this.
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
if (
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
1176
|
-
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
1177
|
-
msg.sourceData = ttsRequest;
|
|
1178
|
-
let ttsResult = null;
|
|
1179
|
-
const ttsStartedAt = Date.now();
|
|
1180
|
-
let ttsSynthMs = 0;
|
|
1181
|
-
try {
|
|
1182
|
-
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
1183
|
-
ttsSynthMs = Date.now() - ttsStartedAt;
|
|
1184
|
-
} catch (err) {
|
|
1185
|
-
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
1109
|
+
this._sendUserSaysWs(request);
|
|
1110
|
+
} else if (msg && msg.messageText) {
|
|
1111
|
+
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
1112
|
+
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
1113
|
+
if (dtmfMatch && dtmfMatch[1]) {
|
|
1114
|
+
const rawDigits = dtmfMatch[1];
|
|
1115
|
+
const digits = sanitizeDtmfDigits(rawDigits);
|
|
1116
|
+
if (!digits) {
|
|
1117
|
+
debug$3(`sendDtmf skipped: no valid DTMF digits after sanitizing <DTMF> content (raw length=${String(rawDigits).length})`);
|
|
1118
|
+
return resolve();
|
|
1186
1119
|
}
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
metricName: 'consumption.e2e.voip.tts.characters',
|
|
1192
|
-
transactions: msg.messageText.length,
|
|
1193
|
-
apiKey
|
|
1194
|
-
});
|
|
1120
|
+
if (digits !== String(rawDigits).replace(/\s/g, '')) {
|
|
1121
|
+
debug$3(`Sending DTMF from messageText (sanitized): "${rawDigits}" -> "${digits}"`);
|
|
1122
|
+
} else {
|
|
1123
|
+
debug$3(`Sending DTMF from messageText: ${digits}`);
|
|
1195
1124
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1125
|
+
const request = JSON.stringify({
|
|
1126
|
+
METHOD: 'sendDtmf',
|
|
1127
|
+
digits,
|
|
1128
|
+
sessionId: this.sessionId
|
|
1129
|
+
});
|
|
1130
|
+
stampAgentWire('dtmf', digits.length * DTMF_MS_PER_DIGIT, {
|
|
1131
|
+
digitCount: digits.length
|
|
1132
|
+
});
|
|
1133
|
+
this._sendUserSaysWs(request);
|
|
1134
|
+
return resolve();
|
|
1135
|
+
}
|
|
1136
|
+
if (!skipTtsForMixedInput) {
|
|
1137
|
+
if (!this.axiosTtsParams) {
|
|
1138
|
+
if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
|
|
1139
|
+
return reject(new Error('TTS not configured, only audio input supported'));
|
|
1209
1140
|
}
|
|
1210
|
-
msg.attachments.push({
|
|
1211
|
-
name: 'tts.wav',
|
|
1212
|
-
mimeType: 'audio/wav',
|
|
1213
|
-
base64: b64Buffer
|
|
1214
|
-
});
|
|
1215
|
-
stampAgentWire('tts', (duration || 0) * 1000, {
|
|
1216
|
-
ttsSynthMs,
|
|
1217
|
-
textLength: msg.messageText ? msg.messageText.length : 0
|
|
1218
|
-
});
|
|
1219
|
-
this.ws.send(request);
|
|
1220
1141
|
} else {
|
|
1221
|
-
|
|
1142
|
+
debug$3('UserSays routing: executing TTS branch');
|
|
1143
|
+
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
1144
|
+
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
1145
|
+
msg.sourceData = ttsRequest;
|
|
1146
|
+
let ttsResult = null;
|
|
1147
|
+
const ttsStartedAt = Date.now();
|
|
1148
|
+
let ttsSynthMs = 0;
|
|
1149
|
+
try {
|
|
1150
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
1151
|
+
ttsSynthMs = Date.now() - ttsStartedAt;
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
1154
|
+
}
|
|
1155
|
+
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
1156
|
+
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
1157
|
+
this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
|
|
1158
|
+
type: lodash__default["default"].isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
|
|
1159
|
+
metricName: 'consumption.e2e.voip.tts.characters',
|
|
1160
|
+
transactions: msg.messageText.length,
|
|
1161
|
+
apiKey
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
1165
|
+
duration = parseFloat(ttsResult.duration) || 0;
|
|
1166
|
+
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
1167
|
+
const request = JSON.stringify({
|
|
1168
|
+
METHOD: 'sendAudio',
|
|
1169
|
+
PESQ: false,
|
|
1170
|
+
sessionId: this.sessionId,
|
|
1171
|
+
b64_buffer: b64Buffer
|
|
1172
|
+
});
|
|
1173
|
+
if (this._lastBotSaysQueuedAt) {
|
|
1174
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
1175
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
1176
|
+
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
1177
|
+
}
|
|
1178
|
+
msg.attachments.push({
|
|
1179
|
+
name: 'tts.wav',
|
|
1180
|
+
mimeType: 'audio/wav',
|
|
1181
|
+
base64: b64Buffer
|
|
1182
|
+
});
|
|
1183
|
+
stampAgentWire('tts', (duration || 0) * 1000, {
|
|
1184
|
+
ttsSynthMs,
|
|
1185
|
+
textLength: msg.messageText ? msg.messageText.length : 0
|
|
1186
|
+
});
|
|
1187
|
+
this._sendUserSaysWs(request);
|
|
1188
|
+
} else {
|
|
1189
|
+
return reject(new Error('TTS failed, response is empty'));
|
|
1190
|
+
}
|
|
1222
1191
|
}
|
|
1192
|
+
} else {
|
|
1193
|
+
debug$3('UserSays routing: skipping TTS for mixed input because VOIP_USER_INPUT_PREFER_VOICE is enabled');
|
|
1223
1194
|
}
|
|
1224
|
-
} else {
|
|
1225
|
-
debug$3('UserSays routing: skipping TTS for mixed input because VOIP_USER_INPUT_PREFER_VOICE is enabled');
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
if (msg && msg.media && msg.media.length > 0 && msg.media[0].buffer) {
|
|
1229
|
-
debug$3('UserSays routing: executing MEDIA branch');
|
|
1230
|
-
const request = JSON.stringify({
|
|
1231
|
-
METHOD: 'sendAudio',
|
|
1232
|
-
sessionId: this.sessionId,
|
|
1233
|
-
b64_buffer: msg.media[0].buffer.toString('base64')
|
|
1234
|
-
});
|
|
1235
|
-
if (this._lastBotSaysQueuedAt) {
|
|
1236
|
-
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
1237
|
-
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
1238
|
-
debug$3(`Latency (bot says -> sendAudio/MEDIA): ${latencyMs} ms (last bot: ${botText})`);
|
|
1239
1195
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1196
|
+
if (msg && msg.media && msg.media.length > 0 && msg.media[0].buffer) {
|
|
1197
|
+
debug$3('UserSays routing: executing MEDIA branch');
|
|
1198
|
+
const request = JSON.stringify({
|
|
1199
|
+
METHOD: 'sendAudio',
|
|
1200
|
+
sessionId: this.sessionId,
|
|
1201
|
+
b64_buffer: msg.media[0].buffer.toString('base64')
|
|
1202
|
+
});
|
|
1203
|
+
if (this._lastBotSaysQueuedAt) {
|
|
1204
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
1205
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
1206
|
+
debug$3(`Latency (bot says -> sendAudio/MEDIA): ${latencyMs} ms (last bot: ${botText})`);
|
|
1207
|
+
}
|
|
1208
|
+
// Stamp now; `requestedDurationMs` is backfilled once media metadata is parsed.
|
|
1209
|
+
stampAgentWire('media', 0, {
|
|
1210
|
+
mediaUri: msg.media[0].mediaUri || null
|
|
1211
|
+
});
|
|
1212
|
+
this._sendUserSaysWs(request);
|
|
1213
|
+
msg.attachments.push({
|
|
1214
|
+
name: msg.media[0].mediaUri,
|
|
1215
|
+
mimeType: msg.media[0].mimeType,
|
|
1216
|
+
base64: msg.media[0].buffer.toString('base64')
|
|
1217
|
+
});
|
|
1218
|
+
try {
|
|
1219
|
+
const metadata = await musicMetadata__default["default"].parseBuffer(msg.media[0].buffer);
|
|
1220
|
+
if (metadata && metadata.format && metadata.format.duration) {
|
|
1221
|
+
debug$3('Audio duration of user audio:', metadata.format.duration, 'seconds');
|
|
1222
|
+
duration = Math.round(metadata.format.duration);
|
|
1223
|
+
if (msg.voipAgent) {
|
|
1224
|
+
msg.voipAgent.requestedDurationMs = Math.max(0, Math.round((duration || 0) * 1000));
|
|
1225
|
+
}
|
|
1226
|
+
} else {
|
|
1227
|
+
reject(new Error('Could not determine audio duration from metadata'));
|
|
1257
1228
|
}
|
|
1258
|
-
}
|
|
1259
|
-
reject(new Error(
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
reject(new Error(`Getting audio duration failed: ${err.message}`));
|
|
1260
1231
|
}
|
|
1261
|
-
} catch (err) {
|
|
1262
|
-
reject(new Error(`Getting audio duration failed: ${err.message}`));
|
|
1263
1232
|
}
|
|
1233
|
+
const requestedDurationMs = Math.max(0, Math.round((duration || 0) * 1000));
|
|
1234
|
+
if (requestedDurationMs <= 0) {
|
|
1235
|
+
return resolve();
|
|
1236
|
+
}
|
|
1237
|
+
setTimeout(resolve, requestedDurationMs);
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
reject(err);
|
|
1264
1240
|
}
|
|
1265
|
-
const requestedDurationMs = Math.max(0, Math.round((duration || 0) * 1000));
|
|
1266
|
-
if (requestedDurationMs <= 0) {
|
|
1267
|
-
return resolve();
|
|
1268
|
-
}
|
|
1269
|
-
setTimeout(resolve, requestedDurationMs);
|
|
1270
1241
|
}, 0);
|
|
1271
1242
|
});
|
|
1272
1243
|
}
|
|
1244
|
+
_voipWsCanSend() {
|
|
1245
|
+
return !this.stopCalled && this.ws && this.ws.readyState === ws__default["default"].OPEN;
|
|
1246
|
+
}
|
|
1247
|
+
_sendUserSaysWs(request) {
|
|
1248
|
+
if (!this._voipWsCanSend()) {
|
|
1249
|
+
throw new Error('VoIP session stopped');
|
|
1250
|
+
}
|
|
1251
|
+
this.ws.send(request);
|
|
1252
|
+
}
|
|
1273
1253
|
async Stop() {
|
|
1274
1254
|
debug$3(`${this.sessionId} - Stop called`);
|
|
1275
1255
|
this.stopCalled = true;
|
|
@@ -1487,6 +1467,75 @@ class BotiumConnectorVoip {
|
|
|
1487
1467
|
if (lodash__default["default"].isNil(convoStep.logicHooks)) return null;
|
|
1488
1468
|
return convoStep && convoStep.logicHooks && convoStep.logicHooks.find(lh => lh.name === 'VOIP_JOIN_SILENCE_DURATION');
|
|
1489
1469
|
}
|
|
1470
|
+
_normalizeJoinRulesBySubstring() {
|
|
1471
|
+
const rawRules = this.caps[Capabilities.VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING];
|
|
1472
|
+
if (lodash__default["default"].isNil(rawRules) || rawRules === '') return [];
|
|
1473
|
+
let parsedRules = rawRules;
|
|
1474
|
+
if (lodash__default["default"].isString(rawRules)) {
|
|
1475
|
+
try {
|
|
1476
|
+
parsedRules = JSON.parse(rawRules);
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
debug$3(`Invalid ${Capabilities.VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING} JSON: ${err.message || err}`);
|
|
1479
|
+
return [];
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (!lodash__default["default"].isArray(parsedRules)) {
|
|
1483
|
+
debug$3(`Invalid ${Capabilities.VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING}: expected array`);
|
|
1484
|
+
return [];
|
|
1485
|
+
}
|
|
1486
|
+
return parsedRules.map(rule => {
|
|
1487
|
+
if (!rule || typeof rule !== 'object') return null;
|
|
1488
|
+
const substring = typeof rule.substring === 'string' ? rule.substring.trim() : '';
|
|
1489
|
+
const timeoutMs = parseInt(rule.timeoutMs, 10);
|
|
1490
|
+
if (!substring || !lodash__default["default"].isFinite(timeoutMs) || timeoutMs <= 0) return null;
|
|
1491
|
+
return {
|
|
1492
|
+
substring,
|
|
1493
|
+
timeoutMs
|
|
1494
|
+
};
|
|
1495
|
+
}).filter(Boolean);
|
|
1496
|
+
}
|
|
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;
|
|
1508
|
+
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;
|
|
1513
|
+
}
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
_hasJoinLogicHookOrRule(convoStep) {
|
|
1517
|
+
return !lodash__default["default"].isNil(this._getJoinLogicHook(convoStep)) || !lodash__default["default"].isNil(this._getJoinRuleBySubstring(convoStep, this.botMsgs));
|
|
1518
|
+
}
|
|
1519
|
+
_toJoinTimeoutMs(ms, isPsst) {
|
|
1520
|
+
const parsed = parseInt(ms, 10);
|
|
1521
|
+
if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
|
|
1522
|
+
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
1523
|
+
}
|
|
1524
|
+
_getEffectiveJoinTimeoutMs(convoStep, botMsgs) {
|
|
1525
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
1526
|
+
const isPsst = sttHandling === 'PSST';
|
|
1527
|
+
const joinLogicHook = this._getJoinLogicHook(convoStep);
|
|
1528
|
+
if (joinLogicHook && joinLogicHook.args && joinLogicHook.args.length > 0) {
|
|
1529
|
+
const joinHookTimeoutMs = this._toJoinTimeoutMs(joinLogicHook.args[0], isPsst);
|
|
1530
|
+
if (lodash__default["default"].isFinite(joinHookTimeoutMs) && joinHookTimeoutMs > 0) return joinHookTimeoutMs;
|
|
1531
|
+
}
|
|
1532
|
+
const joinRule = this._getJoinRuleBySubstring(convoStep, botMsgs);
|
|
1533
|
+
if (joinRule) {
|
|
1534
|
+
const joinRuleTimeoutMs = this._toJoinTimeoutMs(joinRule.timeoutMs, isPsst);
|
|
1535
|
+
if (lodash__default["default"].isFinite(joinRuleTimeoutMs) && joinRuleTimeoutMs > 0) return joinRuleTimeoutMs;
|
|
1536
|
+
}
|
|
1537
|
+
return this._toJoinTimeoutMs(this._getEffectiveMessageHandlingTimeout(), isPsst);
|
|
1538
|
+
}
|
|
1490
1539
|
_getIgnoreSilenceDurationAsserterLogicHook(convoStep) {
|
|
1491
1540
|
if (lodash__default["default"].isNil(convoStep)) return null;
|
|
1492
1541
|
if (lodash__default["default"].isNil(convoStep.logicHooks)) return null;
|
|
@@ -1572,6 +1621,12 @@ var botiumConnectorVoip = {
|
|
|
1572
1621
|
type: 'boolean',
|
|
1573
1622
|
required: false,
|
|
1574
1623
|
advanced: true
|
|
1624
|
+
}, {
|
|
1625
|
+
name: 'VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING',
|
|
1626
|
+
label: 'Join silence timeout rules by bot message substring',
|
|
1627
|
+
type: 'json',
|
|
1628
|
+
required: false,
|
|
1629
|
+
advanced: true
|
|
1575
1630
|
}]
|
|
1576
1631
|
},
|
|
1577
1632
|
PluginLogicHooks: {
|