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.
|
@@ -7,6 +7,24 @@ import debug$4 from 'debug';
|
|
|
7
7
|
import musicMetadata from 'music-metadata';
|
|
8
8
|
|
|
9
9
|
const debug$3 = debug$4('botium-connector-voip');
|
|
10
|
+
|
|
11
|
+
// Logging policy: info = rare, business-relevant lifecycle events (always visible).
|
|
12
|
+
// debug = high-frequency diagnostics (DEBUG=botium-connector-voip). warn = degraded but continuing.
|
|
13
|
+
// error = abort/failure. No secrets in info; STT text only as length or truncated in info.
|
|
14
|
+
|
|
15
|
+
const _info = (event, data) => {
|
|
16
|
+
const parts = Object.entries({
|
|
17
|
+
event,
|
|
18
|
+
...data
|
|
19
|
+
}).filter(([, v]) => v != null && v !== '').map(([k, v]) => `${k}=${typeof v === 'string' && v.length > 80 ? `"${v.substring(0, 77)}..."` : JSON.stringify(v)}`);
|
|
20
|
+
console.info(`[botium-connector-voip] ${parts.join(' ')}`);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** DTMF for RTP/PJSIP: 0–9, *, #, A–D. Strips spaces and other separators so the worker gets one compact string. */
|
|
24
|
+
const sanitizeDtmfDigits = raw => {
|
|
25
|
+
if (raw == null) return '';
|
|
26
|
+
return String(raw).replace(/[^0-9*#ABCDabcd]/g, '').toUpperCase();
|
|
27
|
+
};
|
|
10
28
|
const Capabilities = {
|
|
11
29
|
VOIP_STT_URL_STREAM: 'VOIP_STT_URL_STREAM',
|
|
12
30
|
VOIP_STT_PARAMS_STREAM: 'VOIP_STT_PARAMS_STREAM',
|
|
@@ -54,7 +72,8 @@ const Capabilities = {
|
|
|
54
72
|
VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: 'VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE',
|
|
55
73
|
VOIP_SILENCE_DURATION_TIMEOUT_START: 'VOIP_SILENCE_DURATION_TIMEOUT_START',
|
|
56
74
|
VOIP_STT_CONFIDENCE_THRESHOLD: 'VOIP_STT_CONFIDENCE_THRESHOLD',
|
|
57
|
-
VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER'
|
|
75
|
+
VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER',
|
|
76
|
+
VOIP_USER_INPUT_PREFER_VOICE: 'VOIP_USER_INPUT_PREFER_VOICE'
|
|
58
77
|
};
|
|
59
78
|
const Defaults = {
|
|
60
79
|
VOIP_STT_METHOD: 'POST',
|
|
@@ -76,7 +95,8 @@ const Defaults = {
|
|
|
76
95
|
VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: false,
|
|
77
96
|
VOIP_STT_CONFIDENCE_THRESHOLD: 0.5,
|
|
78
97
|
VOIP_USE_GLOBAL_VOIP_WORKER: false,
|
|
79
|
-
VOIP_SIP_PROTOCOL: 'TCP'
|
|
98
|
+
VOIP_SIP_PROTOCOL: 'TCP',
|
|
99
|
+
VOIP_USER_INPUT_PREFER_VOICE: true
|
|
80
100
|
};
|
|
81
101
|
const TTS_HTTP_AGENT = new http.Agent({
|
|
82
102
|
keepAlive: true
|
|
@@ -199,7 +219,8 @@ class BotiumConnectorVoip {
|
|
|
199
219
|
};
|
|
200
220
|
let data = null;
|
|
201
221
|
let headers = null;
|
|
202
|
-
|
|
222
|
+
let httpInitRetries = 0;
|
|
223
|
+
const connectHttp = async retryIndex => {
|
|
203
224
|
retryIndex = retryIndex || 0;
|
|
204
225
|
try {
|
|
205
226
|
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');
|
|
@@ -220,17 +241,24 @@ class BotiumConnectorVoip {
|
|
|
220
241
|
if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
|
|
221
242
|
throw new Error(`Connecting to VOIP Worker failed: ${err.message || 'Not reachable'}`);
|
|
222
243
|
}
|
|
244
|
+
httpInitRetries = retryIndex + 1;
|
|
223
245
|
// Retry after the defined timeout
|
|
224
246
|
await new Promise(resolve => setTimeout(resolve, this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]));
|
|
225
247
|
|
|
226
248
|
// Retry the connection
|
|
227
|
-
await
|
|
249
|
+
await connectHttp(retryIndex + 1);
|
|
228
250
|
}
|
|
229
251
|
};
|
|
230
|
-
await
|
|
252
|
+
await connectHttp();
|
|
253
|
+
if (httpInitRetries > 0) {
|
|
254
|
+
_info('connected_after_retries', {
|
|
255
|
+
phase: 'initCall',
|
|
256
|
+
retries: httpInitRetries
|
|
257
|
+
});
|
|
258
|
+
}
|
|
231
259
|
return new Promise((resolve, reject) => {
|
|
232
260
|
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}`;
|
|
233
|
-
const
|
|
261
|
+
const connectWs = retryIndex => {
|
|
234
262
|
retryIndex = retryIndex || 0;
|
|
235
263
|
return new Promise((resolve, reject) => {
|
|
236
264
|
if ('set-cookie' in headers) {
|
|
@@ -243,17 +271,23 @@ class BotiumConnectorVoip {
|
|
|
243
271
|
this.ws = new ws(wsEndpoint);
|
|
244
272
|
}
|
|
245
273
|
this.ws.on('open', () => {
|
|
246
|
-
resolve();
|
|
274
|
+
resolve(retryIndex);
|
|
247
275
|
});
|
|
248
276
|
this.ws.on('error', () => {
|
|
249
277
|
if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
|
|
250
278
|
reject(new Error(`Websocket connection failed to ${wsEndpoint}`));
|
|
251
279
|
}
|
|
252
|
-
setTimeout(() =>
|
|
280
|
+
setTimeout(() => connectWs(retryIndex + 1).then(resolve).catch(reject), this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]);
|
|
253
281
|
});
|
|
254
282
|
});
|
|
255
283
|
};
|
|
256
|
-
|
|
284
|
+
connectWs().then(wsRetries => {
|
|
285
|
+
if (wsRetries > 0) {
|
|
286
|
+
_info('connected_after_retries', {
|
|
287
|
+
phase: 'websocket',
|
|
288
|
+
retries: wsRetries
|
|
289
|
+
});
|
|
290
|
+
}
|
|
257
291
|
if (!lodash.isArray(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
|
|
258
292
|
if (lodash.isEmpty(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
|
|
259
293
|
this.caps[Capabilities.VOIP_ICE_STUN_SERVERS] = [];
|
|
@@ -295,6 +329,7 @@ class BotiumConnectorVoip {
|
|
|
295
329
|
});
|
|
296
330
|
this.silence = null;
|
|
297
331
|
this.msgCount = 0;
|
|
332
|
+
this.sttPartialCount = 0;
|
|
298
333
|
this.firstMsg = true;
|
|
299
334
|
this.firstSttInfoReceived = false;
|
|
300
335
|
this.silenceTimeout = null;
|
|
@@ -439,6 +474,10 @@ class BotiumConnectorVoip {
|
|
|
439
474
|
};
|
|
440
475
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
441
476
|
this.sessionId = parsedData.voipConfig.sessionId;
|
|
477
|
+
_info('callinfo_initialized', {
|
|
478
|
+
sessionId: this.sessionId,
|
|
479
|
+
sttHandling: this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING]
|
|
480
|
+
});
|
|
442
481
|
}
|
|
443
482
|
|
|
444
483
|
// if sessionId is not the same as the one in the callinfo, return
|
|
@@ -453,9 +492,17 @@ class BotiumConnectorVoip {
|
|
|
453
492
|
reject(new Error('Error: Sip Registration failed'));
|
|
454
493
|
}
|
|
455
494
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'connected') {
|
|
495
|
+
_info('callinfo_connected', {
|
|
496
|
+
sessionId: this.sessionId
|
|
497
|
+
});
|
|
456
498
|
resolve();
|
|
457
499
|
}
|
|
458
500
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'disconnected') {
|
|
501
|
+
_info('callinfo_disconnected', {
|
|
502
|
+
sessionId: this.sessionId,
|
|
503
|
+
connectDurationSec: parsedData.connectDuration || null,
|
|
504
|
+
sttPartialCount: this.sttPartialCount
|
|
505
|
+
});
|
|
459
506
|
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_STT_BODY));
|
|
460
507
|
if (parsedData.connectDuration && parsedData.connectDuration > 0) {
|
|
461
508
|
this.eventEmitter.emit('CONSUMPTION_METADATA', this, {
|
|
@@ -485,7 +532,13 @@ class BotiumConnectorVoip {
|
|
|
485
532
|
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
486
533
|
const tail = _extractFullRecordBase64(parsedData);
|
|
487
534
|
if (tail) this.fullRecord = (this.fullRecord || '') + tail;
|
|
535
|
+
const base64Len = (this.fullRecord || tail || '').length;
|
|
488
536
|
emitFullRecordAttachment(this.fullRecord || tail);
|
|
537
|
+
_info('recording_attached', {
|
|
538
|
+
sessionId: this.sessionId,
|
|
539
|
+
source: 'fullRecordEnd',
|
|
540
|
+
base64Len
|
|
541
|
+
});
|
|
489
542
|
this.end = true;
|
|
490
543
|
}
|
|
491
544
|
if (parsedData && parsedData.type === 'silence') {
|
|
@@ -498,10 +551,17 @@ class BotiumConnectorVoip {
|
|
|
498
551
|
}
|
|
499
552
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
500
553
|
// Non-chunked full record
|
|
501
|
-
|
|
554
|
+
const base64 = _extractFullRecordBase64(parsedData);
|
|
555
|
+
emitFullRecordAttachment(base64);
|
|
556
|
+
_info('recording_attached', {
|
|
557
|
+
sessionId: this.sessionId,
|
|
558
|
+
source: 'fullRecord',
|
|
559
|
+
base64Len: (base64 || '').length
|
|
560
|
+
});
|
|
502
561
|
this.end = true;
|
|
503
562
|
}
|
|
504
563
|
if (parsedData && parsedData.data && parsedData.data.final === false) {
|
|
564
|
+
this.sttPartialCount++;
|
|
505
565
|
if (this.silenceTimeout) {
|
|
506
566
|
clearTimeout(this.silenceTimeout);
|
|
507
567
|
}
|
|
@@ -548,7 +608,18 @@ class BotiumConnectorVoip {
|
|
|
548
608
|
if (parsedData && parsedData.data && parsedData.data.type === 'stt' && parsedData.data.final) {
|
|
549
609
|
const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
|
|
550
610
|
const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
|
|
611
|
+
const msgText = parsedData.data.message || '';
|
|
612
|
+
const msgLen = typeof msgText === 'string' ? msgText.length : 0;
|
|
613
|
+
const msgPreview = typeof msgText === 'string' ? msgText.trim().substring(0, 80) : '';
|
|
551
614
|
debug$3(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
|
|
615
|
+
_info('stt_final', {
|
|
616
|
+
sessionId: this.sessionId,
|
|
617
|
+
message: msgPreview,
|
|
618
|
+
messageLength: msgLen,
|
|
619
|
+
confidence: this._getConfidenceScore(parsedData),
|
|
620
|
+
threshold: confidenceThreshold,
|
|
621
|
+
accepted: successfulConfidenceScore
|
|
622
|
+
});
|
|
552
623
|
// ORIGINAL: emit final message immediately, unless join hooks are active.
|
|
553
624
|
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && lodash.isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
554
625
|
let botMsg = {
|
|
@@ -649,6 +720,19 @@ class BotiumConnectorVoip {
|
|
|
649
720
|
}
|
|
650
721
|
async UserSays(msg) {
|
|
651
722
|
debug$3('UserSays called');
|
|
723
|
+
const hasText = !!(msg && msg.messageText);
|
|
724
|
+
const hasVoiceMedia = !!(msg && msg.media && msg.media.length > 0 && msg.media[0].buffer);
|
|
725
|
+
const hasDtmf = !!(msg && msg.buttons && msg.buttons.length > 0);
|
|
726
|
+
const dtmfMatch = msg && msg.messageText && msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
727
|
+
const inputType = hasDtmf || dtmfMatch ? 'dtmf' : hasText && hasVoiceMedia ? 'mixed' : hasText ? 'text' : hasVoiceMedia ? 'media' : 'unknown';
|
|
728
|
+
const msgPreview = hasText && msg.messageText ? String(msg.messageText).trim().substring(0, 80) : '';
|
|
729
|
+
_info('user_says', {
|
|
730
|
+
sessionId: this.sessionId,
|
|
731
|
+
inputType,
|
|
732
|
+
message: msgPreview || undefined,
|
|
733
|
+
messageLength: hasText && msg.messageText ? msg.messageText.length : undefined,
|
|
734
|
+
mediaSize: hasVoiceMedia && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined
|
|
735
|
+
});
|
|
652
736
|
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
653
737
|
try {
|
|
654
738
|
const safeLog = {
|
|
@@ -673,10 +757,20 @@ class BotiumConnectorVoip {
|
|
|
673
757
|
return new Promise((resolve, reject) => {
|
|
674
758
|
setTimeout(async () => {
|
|
675
759
|
let duration = 0;
|
|
760
|
+
const preferVoiceCapRaw = this.caps[Capabilities.VOIP_USER_INPUT_PREFER_VOICE];
|
|
761
|
+
const preferVoice = !!preferVoiceCapRaw;
|
|
762
|
+
const skipTtsForMixedInput = preferVoice && hasText && hasVoiceMedia;
|
|
763
|
+
debug$3(`UserSays routing: hasText=${hasText} hasVoiceMedia=${hasVoiceMedia} preferVoice=${preferVoice} preferVoiceRaw=${JSON.stringify(preferVoiceCapRaw)} skipTtsForMixedInput=${skipTtsForMixedInput}`);
|
|
676
764
|
if (msg && msg.buttons && msg.buttons.length > 0) {
|
|
765
|
+
const digits = sanitizeDtmfDigits(msg.buttons[0].payload);
|
|
766
|
+
if (!digits) {
|
|
767
|
+
debug$3('sendDtmf skipped: no valid DTMF digits after sanitizing button payload');
|
|
768
|
+
return resolve();
|
|
769
|
+
}
|
|
770
|
+
debug$3(`Sending DTMF digits: ${digits}`);
|
|
677
771
|
const request = JSON.stringify({
|
|
678
772
|
METHOD: 'sendDtmf',
|
|
679
|
-
digits
|
|
773
|
+
digits,
|
|
680
774
|
sessionId: this.sessionId
|
|
681
775
|
});
|
|
682
776
|
this.ws.send(request);
|
|
@@ -684,8 +778,17 @@ class BotiumConnectorVoip {
|
|
|
684
778
|
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
685
779
|
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
686
780
|
if (dtmfMatch && dtmfMatch[1]) {
|
|
687
|
-
const
|
|
688
|
-
|
|
781
|
+
const rawDigits = dtmfMatch[1];
|
|
782
|
+
const digits = sanitizeDtmfDigits(rawDigits);
|
|
783
|
+
if (!digits) {
|
|
784
|
+
debug$3(`sendDtmf skipped: no valid DTMF digits after sanitizing <DTMF> content (raw length=${String(rawDigits).length})`);
|
|
785
|
+
return resolve();
|
|
786
|
+
}
|
|
787
|
+
if (digits !== String(rawDigits).replace(/\s/g, '')) {
|
|
788
|
+
debug$3(`Sending DTMF from messageText (sanitized): "${rawDigits}" -> "${digits}"`);
|
|
789
|
+
} else {
|
|
790
|
+
debug$3(`Sending DTMF from messageText: ${digits}`);
|
|
791
|
+
}
|
|
689
792
|
const request = JSON.stringify({
|
|
690
793
|
METHOD: 'sendDtmf',
|
|
691
794
|
digits,
|
|
@@ -694,55 +797,61 @@ class BotiumConnectorVoip {
|
|
|
694
797
|
this.ws.send(request);
|
|
695
798
|
return resolve();
|
|
696
799
|
}
|
|
697
|
-
if (!
|
|
698
|
-
if (!
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
} else {
|
|
702
|
-
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
703
|
-
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
704
|
-
msg.sourceData = ttsRequest;
|
|
705
|
-
let ttsResult = null;
|
|
706
|
-
try {
|
|
707
|
-
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
708
|
-
} catch (err) {
|
|
709
|
-
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
710
|
-
}
|
|
711
|
-
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
712
|
-
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
713
|
-
this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
|
|
714
|
-
type: lodash.isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
|
|
715
|
-
metricName: 'consumption.e2e.voip.tts.characters',
|
|
716
|
-
transactions: msg.messageText.length,
|
|
717
|
-
apiKey
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
721
|
-
duration = parseFloat(ttsResult.duration) || 0;
|
|
722
|
-
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
723
|
-
const request = JSON.stringify({
|
|
724
|
-
METHOD: 'sendAudio',
|
|
725
|
-
PESQ: false,
|
|
726
|
-
sessionId: this.sessionId,
|
|
727
|
-
b64_buffer: b64Buffer
|
|
728
|
-
});
|
|
729
|
-
if (this._lastBotSaysQueuedAt) {
|
|
730
|
-
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
731
|
-
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
732
|
-
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
800
|
+
if (!skipTtsForMixedInput) {
|
|
801
|
+
if (!this.axiosTtsParams) {
|
|
802
|
+
if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
|
|
803
|
+
return reject(new Error('TTS not configured, only audio input supported'));
|
|
733
804
|
}
|
|
734
|
-
msg.attachments.push({
|
|
735
|
-
name: 'tts.wav',
|
|
736
|
-
mimeType: 'audio/wav',
|
|
737
|
-
base64: b64Buffer
|
|
738
|
-
});
|
|
739
|
-
this.ws.send(request);
|
|
740
805
|
} else {
|
|
741
|
-
|
|
806
|
+
debug$3('UserSays routing: executing TTS branch');
|
|
807
|
+
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
808
|
+
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
809
|
+
msg.sourceData = ttsRequest;
|
|
810
|
+
let ttsResult = null;
|
|
811
|
+
try {
|
|
812
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
815
|
+
}
|
|
816
|
+
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
817
|
+
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
818
|
+
this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
|
|
819
|
+
type: lodash.isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
|
|
820
|
+
metricName: 'consumption.e2e.voip.tts.characters',
|
|
821
|
+
transactions: msg.messageText.length,
|
|
822
|
+
apiKey
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
826
|
+
duration = parseFloat(ttsResult.duration) || 0;
|
|
827
|
+
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
828
|
+
const request = JSON.stringify({
|
|
829
|
+
METHOD: 'sendAudio',
|
|
830
|
+
PESQ: false,
|
|
831
|
+
sessionId: this.sessionId,
|
|
832
|
+
b64_buffer: b64Buffer
|
|
833
|
+
});
|
|
834
|
+
if (this._lastBotSaysQueuedAt) {
|
|
835
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
836
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
837
|
+
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
838
|
+
}
|
|
839
|
+
msg.attachments.push({
|
|
840
|
+
name: 'tts.wav',
|
|
841
|
+
mimeType: 'audio/wav',
|
|
842
|
+
base64: b64Buffer
|
|
843
|
+
});
|
|
844
|
+
this.ws.send(request);
|
|
845
|
+
} else {
|
|
846
|
+
return reject(new Error('TTS failed, response is empty'));
|
|
847
|
+
}
|
|
742
848
|
}
|
|
849
|
+
} else {
|
|
850
|
+
debug$3('UserSays routing: skipping TTS for mixed input because VOIP_USER_INPUT_PREFER_VOICE is enabled');
|
|
743
851
|
}
|
|
744
852
|
}
|
|
745
853
|
if (msg && msg.media && msg.media.length > 0 && msg.media[0].buffer) {
|
|
854
|
+
debug$3('UserSays routing: executing MEDIA branch');
|
|
746
855
|
const request = JSON.stringify({
|
|
747
856
|
METHOD: 'sendAudio',
|
|
748
857
|
sessionId: this.sessionId,
|
|
@@ -1061,6 +1170,12 @@ var botiumConnectorVoip = {
|
|
|
1061
1170
|
label: 'Speech Synthesis Profile',
|
|
1062
1171
|
type: 'speechsynthesisprofile',
|
|
1063
1172
|
required: true
|
|
1173
|
+
}, {
|
|
1174
|
+
name: 'VOIP_USER_INPUT_PREFER_VOICE',
|
|
1175
|
+
label: 'Prefer voice media when both text and voice present',
|
|
1176
|
+
type: 'boolean',
|
|
1177
|
+
required: false,
|
|
1178
|
+
advanced: true
|
|
1064
1179
|
}]
|
|
1065
1180
|
},
|
|
1066
1181
|
PluginLogicHooks: {
|