botium-connector-voip 0.0.21 → 0.0.23
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.
|
@@ -21,6 +21,24 @@ var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug$4);
|
|
|
21
21
|
var musicMetadata__default = /*#__PURE__*/_interopDefaultLegacy(musicMetadata);
|
|
22
22
|
|
|
23
23
|
const debug$3 = debug__default["default"]('botium-connector-voip');
|
|
24
|
+
|
|
25
|
+
// Logging policy: info = rare, business-relevant lifecycle events (always visible).
|
|
26
|
+
// debug = high-frequency diagnostics (DEBUG=botium-connector-voip). warn = degraded but continuing.
|
|
27
|
+
// error = abort/failure. No secrets in info; STT text only as length or truncated in info.
|
|
28
|
+
|
|
29
|
+
const _info = (event, data) => {
|
|
30
|
+
const parts = Object.entries({
|
|
31
|
+
event,
|
|
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)}`);
|
|
34
|
+
console.info(`[botium-connector-voip] ${parts.join(' ')}`);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** DTMF for RTP/PJSIP: 0–9, *, #, A–D. Strips spaces and other separators so the worker gets one compact string. */
|
|
38
|
+
const sanitizeDtmfDigits = raw => {
|
|
39
|
+
if (raw == null) return '';
|
|
40
|
+
return String(raw).replace(/[^0-9*#ABCDabcd]/g, '').toUpperCase();
|
|
41
|
+
};
|
|
24
42
|
const Capabilities = {
|
|
25
43
|
VOIP_STT_URL_STREAM: 'VOIP_STT_URL_STREAM',
|
|
26
44
|
VOIP_STT_PARAMS_STREAM: 'VOIP_STT_PARAMS_STREAM',
|
|
@@ -68,7 +86,8 @@ const Capabilities = {
|
|
|
68
86
|
VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: 'VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE',
|
|
69
87
|
VOIP_SILENCE_DURATION_TIMEOUT_START: 'VOIP_SILENCE_DURATION_TIMEOUT_START',
|
|
70
88
|
VOIP_STT_CONFIDENCE_THRESHOLD: 'VOIP_STT_CONFIDENCE_THRESHOLD',
|
|
71
|
-
VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER'
|
|
89
|
+
VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER',
|
|
90
|
+
VOIP_USER_INPUT_PREFER_VOICE: 'VOIP_USER_INPUT_PREFER_VOICE'
|
|
72
91
|
};
|
|
73
92
|
const Defaults = {
|
|
74
93
|
VOIP_STT_METHOD: 'POST',
|
|
@@ -90,7 +109,8 @@ const Defaults = {
|
|
|
90
109
|
VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: false,
|
|
91
110
|
VOIP_STT_CONFIDENCE_THRESHOLD: 0.5,
|
|
92
111
|
VOIP_USE_GLOBAL_VOIP_WORKER: false,
|
|
93
|
-
VOIP_SIP_PROTOCOL: 'TCP'
|
|
112
|
+
VOIP_SIP_PROTOCOL: 'TCP',
|
|
113
|
+
VOIP_USER_INPUT_PREFER_VOICE: true
|
|
94
114
|
};
|
|
95
115
|
const TTS_HTTP_AGENT = new http__default["default"].Agent({
|
|
96
116
|
keepAlive: true
|
|
@@ -213,7 +233,8 @@ class BotiumConnectorVoip {
|
|
|
213
233
|
};
|
|
214
234
|
let data = null;
|
|
215
235
|
let headers = null;
|
|
216
|
-
|
|
236
|
+
let httpInitRetries = 0;
|
|
237
|
+
const connectHttp = async retryIndex => {
|
|
217
238
|
retryIndex = retryIndex || 0;
|
|
218
239
|
try {
|
|
219
240
|
const workerUrl = this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_URL : this.caps[Capabilities.VOIP_WORKER_URL].replace('wss', 'https').replace('ws', 'http');
|
|
@@ -234,17 +255,24 @@ class BotiumConnectorVoip {
|
|
|
234
255
|
if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
|
|
235
256
|
throw new Error(`Connecting to VOIP Worker failed: ${err.message || 'Not reachable'}`);
|
|
236
257
|
}
|
|
258
|
+
httpInitRetries = retryIndex + 1;
|
|
237
259
|
// Retry after the defined timeout
|
|
238
260
|
await new Promise(resolve => setTimeout(resolve, this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]));
|
|
239
261
|
|
|
240
262
|
// Retry the connection
|
|
241
|
-
await
|
|
263
|
+
await connectHttp(retryIndex + 1);
|
|
242
264
|
}
|
|
243
265
|
};
|
|
244
|
-
await
|
|
266
|
+
await connectHttp();
|
|
267
|
+
if (httpInitRetries > 0) {
|
|
268
|
+
_info('connected_after_retries', {
|
|
269
|
+
phase: 'initCall',
|
|
270
|
+
retries: httpInitRetries
|
|
271
|
+
});
|
|
272
|
+
}
|
|
245
273
|
return new Promise((resolve, reject) => {
|
|
246
274
|
const wsEndpoint = `${this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_URL : this.caps[Capabilities.VOIP_WORKER_URL]}/ws/${data.port}`;
|
|
247
|
-
const
|
|
275
|
+
const connectWs = retryIndex => {
|
|
248
276
|
retryIndex = retryIndex || 0;
|
|
249
277
|
return new Promise((resolve, reject) => {
|
|
250
278
|
if ('set-cookie' in headers) {
|
|
@@ -257,17 +285,23 @@ class BotiumConnectorVoip {
|
|
|
257
285
|
this.ws = new ws__default["default"](wsEndpoint);
|
|
258
286
|
}
|
|
259
287
|
this.ws.on('open', () => {
|
|
260
|
-
resolve();
|
|
288
|
+
resolve(retryIndex);
|
|
261
289
|
});
|
|
262
290
|
this.ws.on('error', () => {
|
|
263
291
|
if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
|
|
264
292
|
reject(new Error(`Websocket connection failed to ${wsEndpoint}`));
|
|
265
293
|
}
|
|
266
|
-
setTimeout(() =>
|
|
294
|
+
setTimeout(() => connectWs(retryIndex + 1).then(resolve).catch(reject), this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]);
|
|
267
295
|
});
|
|
268
296
|
});
|
|
269
297
|
};
|
|
270
|
-
|
|
298
|
+
connectWs().then(wsRetries => {
|
|
299
|
+
if (wsRetries > 0) {
|
|
300
|
+
_info('connected_after_retries', {
|
|
301
|
+
phase: 'websocket',
|
|
302
|
+
retries: wsRetries
|
|
303
|
+
});
|
|
304
|
+
}
|
|
271
305
|
if (!lodash__default["default"].isArray(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
|
|
272
306
|
if (lodash__default["default"].isEmpty(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
|
|
273
307
|
this.caps[Capabilities.VOIP_ICE_STUN_SERVERS] = [];
|
|
@@ -309,6 +343,7 @@ class BotiumConnectorVoip {
|
|
|
309
343
|
});
|
|
310
344
|
this.silence = null;
|
|
311
345
|
this.msgCount = 0;
|
|
346
|
+
this.sttPartialCount = 0;
|
|
312
347
|
this.firstMsg = true;
|
|
313
348
|
this.firstSttInfoReceived = false;
|
|
314
349
|
this.silenceTimeout = null;
|
|
@@ -453,6 +488,10 @@ class BotiumConnectorVoip {
|
|
|
453
488
|
};
|
|
454
489
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
455
490
|
this.sessionId = parsedData.voipConfig.sessionId;
|
|
491
|
+
_info('callinfo_initialized', {
|
|
492
|
+
sessionId: this.sessionId,
|
|
493
|
+
sttHandling: this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING]
|
|
494
|
+
});
|
|
456
495
|
}
|
|
457
496
|
|
|
458
497
|
// if sessionId is not the same as the one in the callinfo, return
|
|
@@ -467,9 +506,17 @@ class BotiumConnectorVoip {
|
|
|
467
506
|
reject(new Error('Error: Sip Registration failed'));
|
|
468
507
|
}
|
|
469
508
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'connected') {
|
|
509
|
+
_info('callinfo_connected', {
|
|
510
|
+
sessionId: this.sessionId
|
|
511
|
+
});
|
|
470
512
|
resolve();
|
|
471
513
|
}
|
|
472
514
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'disconnected') {
|
|
515
|
+
_info('callinfo_disconnected', {
|
|
516
|
+
sessionId: this.sessionId,
|
|
517
|
+
connectDurationSec: parsedData.connectDuration || null,
|
|
518
|
+
sttPartialCount: this.sttPartialCount
|
|
519
|
+
});
|
|
473
520
|
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_STT_BODY));
|
|
474
521
|
if (parsedData.connectDuration && parsedData.connectDuration > 0) {
|
|
475
522
|
this.eventEmitter.emit('CONSUMPTION_METADATA', this, {
|
|
@@ -499,7 +546,13 @@ class BotiumConnectorVoip {
|
|
|
499
546
|
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
500
547
|
const tail = _extractFullRecordBase64(parsedData);
|
|
501
548
|
if (tail) this.fullRecord = (this.fullRecord || '') + tail;
|
|
549
|
+
const base64Len = (this.fullRecord || tail || '').length;
|
|
502
550
|
emitFullRecordAttachment(this.fullRecord || tail);
|
|
551
|
+
_info('recording_attached', {
|
|
552
|
+
sessionId: this.sessionId,
|
|
553
|
+
source: 'fullRecordEnd',
|
|
554
|
+
base64Len
|
|
555
|
+
});
|
|
503
556
|
this.end = true;
|
|
504
557
|
}
|
|
505
558
|
if (parsedData && parsedData.type === 'silence') {
|
|
@@ -512,10 +565,17 @@ class BotiumConnectorVoip {
|
|
|
512
565
|
}
|
|
513
566
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
514
567
|
// Non-chunked full record
|
|
515
|
-
|
|
568
|
+
const base64 = _extractFullRecordBase64(parsedData);
|
|
569
|
+
emitFullRecordAttachment(base64);
|
|
570
|
+
_info('recording_attached', {
|
|
571
|
+
sessionId: this.sessionId,
|
|
572
|
+
source: 'fullRecord',
|
|
573
|
+
base64Len: (base64 || '').length
|
|
574
|
+
});
|
|
516
575
|
this.end = true;
|
|
517
576
|
}
|
|
518
577
|
if (parsedData && parsedData.data && parsedData.data.final === false) {
|
|
578
|
+
this.sttPartialCount++;
|
|
519
579
|
if (this.silenceTimeout) {
|
|
520
580
|
clearTimeout(this.silenceTimeout);
|
|
521
581
|
}
|
|
@@ -562,7 +622,18 @@ class BotiumConnectorVoip {
|
|
|
562
622
|
if (parsedData && parsedData.data && parsedData.data.type === 'stt' && parsedData.data.final) {
|
|
563
623
|
const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
|
|
564
624
|
const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
|
|
625
|
+
const msgText = parsedData.data.message || '';
|
|
626
|
+
const msgLen = typeof msgText === 'string' ? msgText.length : 0;
|
|
627
|
+
const msgPreview = typeof msgText === 'string' ? msgText.trim().substring(0, 80) : '';
|
|
565
628
|
debug$3(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
|
|
629
|
+
_info('stt_final', {
|
|
630
|
+
sessionId: this.sessionId,
|
|
631
|
+
message: msgPreview,
|
|
632
|
+
messageLength: msgLen,
|
|
633
|
+
confidence: this._getConfidenceScore(parsedData),
|
|
634
|
+
threshold: confidenceThreshold,
|
|
635
|
+
accepted: successfulConfidenceScore
|
|
636
|
+
});
|
|
566
637
|
// ORIGINAL: emit final message immediately, unless join hooks are active.
|
|
567
638
|
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
568
639
|
let botMsg = {
|
|
@@ -663,6 +734,19 @@ class BotiumConnectorVoip {
|
|
|
663
734
|
}
|
|
664
735
|
async UserSays(msg) {
|
|
665
736
|
debug$3('UserSays called');
|
|
737
|
+
const hasText = !!(msg && msg.messageText);
|
|
738
|
+
const hasVoiceMedia = !!(msg && msg.media && msg.media.length > 0 && msg.media[0].buffer);
|
|
739
|
+
const hasDtmf = !!(msg && msg.buttons && msg.buttons.length > 0);
|
|
740
|
+
const dtmfMatch = msg && msg.messageText && msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
741
|
+
const inputType = hasDtmf || dtmfMatch ? 'dtmf' : hasText && hasVoiceMedia ? 'mixed' : hasText ? 'text' : hasVoiceMedia ? 'media' : 'unknown';
|
|
742
|
+
const msgPreview = hasText && msg.messageText ? String(msg.messageText).trim().substring(0, 80) : '';
|
|
743
|
+
_info('user_says', {
|
|
744
|
+
sessionId: this.sessionId,
|
|
745
|
+
inputType,
|
|
746
|
+
message: msgPreview || undefined,
|
|
747
|
+
messageLength: hasText && msg.messageText ? msg.messageText.length : undefined,
|
|
748
|
+
mediaSize: hasVoiceMedia && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined
|
|
749
|
+
});
|
|
666
750
|
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
667
751
|
try {
|
|
668
752
|
const safeLog = {
|
|
@@ -687,10 +771,20 @@ class BotiumConnectorVoip {
|
|
|
687
771
|
return new Promise((resolve, reject) => {
|
|
688
772
|
setTimeout(async () => {
|
|
689
773
|
let duration = 0;
|
|
774
|
+
const preferVoiceCapRaw = this.caps[Capabilities.VOIP_USER_INPUT_PREFER_VOICE];
|
|
775
|
+
const preferVoice = !!preferVoiceCapRaw;
|
|
776
|
+
const skipTtsForMixedInput = preferVoice && hasText && hasVoiceMedia;
|
|
777
|
+
debug$3(`UserSays routing: hasText=${hasText} hasVoiceMedia=${hasVoiceMedia} preferVoice=${preferVoice} preferVoiceRaw=${JSON.stringify(preferVoiceCapRaw)} skipTtsForMixedInput=${skipTtsForMixedInput}`);
|
|
690
778
|
if (msg && msg.buttons && msg.buttons.length > 0) {
|
|
779
|
+
const digits = sanitizeDtmfDigits(msg.buttons[0].payload);
|
|
780
|
+
if (!digits) {
|
|
781
|
+
debug$3('sendDtmf skipped: no valid DTMF digits after sanitizing button payload');
|
|
782
|
+
return resolve();
|
|
783
|
+
}
|
|
784
|
+
debug$3(`Sending DTMF digits: ${digits}`);
|
|
691
785
|
const request = JSON.stringify({
|
|
692
786
|
METHOD: 'sendDtmf',
|
|
693
|
-
digits
|
|
787
|
+
digits,
|
|
694
788
|
sessionId: this.sessionId
|
|
695
789
|
});
|
|
696
790
|
this.ws.send(request);
|
|
@@ -698,8 +792,17 @@ class BotiumConnectorVoip {
|
|
|
698
792
|
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
699
793
|
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
700
794
|
if (dtmfMatch && dtmfMatch[1]) {
|
|
701
|
-
const
|
|
702
|
-
|
|
795
|
+
const rawDigits = dtmfMatch[1];
|
|
796
|
+
const digits = sanitizeDtmfDigits(rawDigits);
|
|
797
|
+
if (!digits) {
|
|
798
|
+
debug$3(`sendDtmf skipped: no valid DTMF digits after sanitizing <DTMF> content (raw length=${String(rawDigits).length})`);
|
|
799
|
+
return resolve();
|
|
800
|
+
}
|
|
801
|
+
if (digits !== String(rawDigits).replace(/\s/g, '')) {
|
|
802
|
+
debug$3(`Sending DTMF from messageText (sanitized): "${rawDigits}" -> "${digits}"`);
|
|
803
|
+
} else {
|
|
804
|
+
debug$3(`Sending DTMF from messageText: ${digits}`);
|
|
805
|
+
}
|
|
703
806
|
const request = JSON.stringify({
|
|
704
807
|
METHOD: 'sendDtmf',
|
|
705
808
|
digits,
|
|
@@ -708,55 +811,61 @@ class BotiumConnectorVoip {
|
|
|
708
811
|
this.ws.send(request);
|
|
709
812
|
return resolve();
|
|
710
813
|
}
|
|
711
|
-
if (!
|
|
712
|
-
if (!
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
} else {
|
|
716
|
-
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
717
|
-
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
718
|
-
msg.sourceData = ttsRequest;
|
|
719
|
-
let ttsResult = null;
|
|
720
|
-
try {
|
|
721
|
-
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
722
|
-
} catch (err) {
|
|
723
|
-
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
724
|
-
}
|
|
725
|
-
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
726
|
-
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
727
|
-
this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
|
|
728
|
-
type: lodash__default["default"].isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
|
|
729
|
-
metricName: 'consumption.e2e.voip.tts.characters',
|
|
730
|
-
transactions: msg.messageText.length,
|
|
731
|
-
apiKey
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
735
|
-
duration = parseFloat(ttsResult.duration) || 0;
|
|
736
|
-
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
737
|
-
const request = JSON.stringify({
|
|
738
|
-
METHOD: 'sendAudio',
|
|
739
|
-
PESQ: false,
|
|
740
|
-
sessionId: this.sessionId,
|
|
741
|
-
b64_buffer: b64Buffer
|
|
742
|
-
});
|
|
743
|
-
if (this._lastBotSaysQueuedAt) {
|
|
744
|
-
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
745
|
-
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
746
|
-
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
814
|
+
if (!skipTtsForMixedInput) {
|
|
815
|
+
if (!this.axiosTtsParams) {
|
|
816
|
+
if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
|
|
817
|
+
return reject(new Error('TTS not configured, only audio input supported'));
|
|
747
818
|
}
|
|
748
|
-
msg.attachments.push({
|
|
749
|
-
name: 'tts.wav',
|
|
750
|
-
mimeType: 'audio/wav',
|
|
751
|
-
base64: b64Buffer
|
|
752
|
-
});
|
|
753
|
-
this.ws.send(request);
|
|
754
819
|
} else {
|
|
755
|
-
|
|
820
|
+
debug$3('UserSays routing: executing TTS branch');
|
|
821
|
+
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
822
|
+
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
823
|
+
msg.sourceData = ttsRequest;
|
|
824
|
+
let ttsResult = null;
|
|
825
|
+
try {
|
|
826
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
827
|
+
} catch (err) {
|
|
828
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
829
|
+
}
|
|
830
|
+
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
831
|
+
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
832
|
+
this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
|
|
833
|
+
type: lodash__default["default"].isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
|
|
834
|
+
metricName: 'consumption.e2e.voip.tts.characters',
|
|
835
|
+
transactions: msg.messageText.length,
|
|
836
|
+
apiKey
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
840
|
+
duration = parseFloat(ttsResult.duration) || 0;
|
|
841
|
+
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
842
|
+
const request = JSON.stringify({
|
|
843
|
+
METHOD: 'sendAudio',
|
|
844
|
+
PESQ: false,
|
|
845
|
+
sessionId: this.sessionId,
|
|
846
|
+
b64_buffer: b64Buffer
|
|
847
|
+
});
|
|
848
|
+
if (this._lastBotSaysQueuedAt) {
|
|
849
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
850
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
851
|
+
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
852
|
+
}
|
|
853
|
+
msg.attachments.push({
|
|
854
|
+
name: 'tts.wav',
|
|
855
|
+
mimeType: 'audio/wav',
|
|
856
|
+
base64: b64Buffer
|
|
857
|
+
});
|
|
858
|
+
this.ws.send(request);
|
|
859
|
+
} else {
|
|
860
|
+
return reject(new Error('TTS failed, response is empty'));
|
|
861
|
+
}
|
|
756
862
|
}
|
|
863
|
+
} else {
|
|
864
|
+
debug$3('UserSays routing: skipping TTS for mixed input because VOIP_USER_INPUT_PREFER_VOICE is enabled');
|
|
757
865
|
}
|
|
758
866
|
}
|
|
759
867
|
if (msg && msg.media && msg.media.length > 0 && msg.media[0].buffer) {
|
|
868
|
+
debug$3('UserSays routing: executing MEDIA branch');
|
|
760
869
|
const request = JSON.stringify({
|
|
761
870
|
METHOD: 'sendAudio',
|
|
762
871
|
sessionId: this.sessionId,
|
|
@@ -1075,6 +1184,12 @@ var botiumConnectorVoip = {
|
|
|
1075
1184
|
label: 'Speech Synthesis Profile',
|
|
1076
1185
|
type: 'speechsynthesisprofile',
|
|
1077
1186
|
required: true
|
|
1187
|
+
}, {
|
|
1188
|
+
name: 'VOIP_USER_INPUT_PREFER_VOICE',
|
|
1189
|
+
label: 'Prefer voice media when both text and voice present',
|
|
1190
|
+
type: 'boolean',
|
|
1191
|
+
required: false,
|
|
1192
|
+
advanced: true
|
|
1078
1193
|
}]
|
|
1079
1194
|
},
|
|
1080
1195
|
PluginLogicHooks: {
|