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.
package/src/connector.js
CHANGED
|
@@ -6,6 +6,23 @@ const https = require('https')
|
|
|
6
6
|
const debug = require('debug')('botium-connector-voip')
|
|
7
7
|
const mm = require('music-metadata')
|
|
8
8
|
|
|
9
|
+
// Logging policy: info = rare, business-relevant lifecycle events (always visible).
|
|
10
|
+
// debug = high-frequency diagnostics (DEBUG=botium-connector-voip). warn = degraded but continuing.
|
|
11
|
+
// error = abort/failure. No secrets in info; STT text only as length or truncated in info.
|
|
12
|
+
|
|
13
|
+
const _info = (event, data) => {
|
|
14
|
+
const parts = Object.entries({ event, ...data })
|
|
15
|
+
.filter(([, v]) => v != null && v !== '')
|
|
16
|
+
.map(([k, v]) => `${k}=${typeof v === 'string' && v.length > 80 ? `"${v.substring(0, 77)}..."` : JSON.stringify(v)}`)
|
|
17
|
+
console.info(`[botium-connector-voip] ${parts.join(' ')}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** DTMF for RTP/PJSIP: 0–9, *, #, A–D. Strips spaces and other separators so the worker gets one compact string. */
|
|
21
|
+
const sanitizeDtmfDigits = (raw) => {
|
|
22
|
+
if (raw == null) return ''
|
|
23
|
+
return String(raw).replace(/[^0-9*#ABCDabcd]/g, '').toUpperCase()
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
const Capabilities = {
|
|
10
27
|
VOIP_STT_URL_STREAM: 'VOIP_STT_URL_STREAM',
|
|
11
28
|
VOIP_STT_PARAMS_STREAM: 'VOIP_STT_PARAMS_STREAM',
|
|
@@ -53,7 +70,8 @@ const Capabilities = {
|
|
|
53
70
|
VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: 'VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE',
|
|
54
71
|
VOIP_SILENCE_DURATION_TIMEOUT_START: 'VOIP_SILENCE_DURATION_TIMEOUT_START',
|
|
55
72
|
VOIP_STT_CONFIDENCE_THRESHOLD: 'VOIP_STT_CONFIDENCE_THRESHOLD',
|
|
56
|
-
VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER'
|
|
73
|
+
VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER',
|
|
74
|
+
VOIP_USER_INPUT_PREFER_VOICE: 'VOIP_USER_INPUT_PREFER_VOICE'
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
const Defaults = {
|
|
@@ -76,7 +94,8 @@ const Defaults = {
|
|
|
76
94
|
VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: false,
|
|
77
95
|
VOIP_STT_CONFIDENCE_THRESHOLD: 0.5,
|
|
78
96
|
VOIP_USE_GLOBAL_VOIP_WORKER: false,
|
|
79
|
-
VOIP_SIP_PROTOCOL: 'TCP'
|
|
97
|
+
VOIP_SIP_PROTOCOL: 'TCP',
|
|
98
|
+
VOIP_USER_INPUT_PREFER_VOICE: true
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
const TTS_HTTP_AGENT = new http.Agent({ keepAlive: true })
|
|
@@ -200,7 +219,8 @@ class BotiumConnectorVoip {
|
|
|
200
219
|
|
|
201
220
|
let data = null
|
|
202
221
|
let headers = null
|
|
203
|
-
|
|
222
|
+
let httpInitRetries = 0
|
|
223
|
+
const connectHttp = async (retryIndex) => {
|
|
204
224
|
retryIndex = retryIndex || 0
|
|
205
225
|
try {
|
|
206
226
|
const workerUrl = this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER]
|
|
@@ -223,18 +243,22 @@ class BotiumConnectorVoip {
|
|
|
223
243
|
if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
|
|
224
244
|
throw new Error(`Connecting to VOIP Worker failed: ${err.message || 'Not reachable'}`)
|
|
225
245
|
}
|
|
246
|
+
httpInitRetries = retryIndex + 1
|
|
226
247
|
// Retry after the defined timeout
|
|
227
248
|
await new Promise(resolve => setTimeout(resolve, this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]))
|
|
228
249
|
|
|
229
250
|
// Retry the connection
|
|
230
|
-
await
|
|
251
|
+
await connectHttp(retryIndex + 1)
|
|
231
252
|
}
|
|
232
253
|
}
|
|
233
|
-
await
|
|
254
|
+
await connectHttp()
|
|
255
|
+
if (httpInitRetries > 0) {
|
|
256
|
+
_info('connected_after_retries', { phase: 'initCall', retries: httpInitRetries })
|
|
257
|
+
}
|
|
234
258
|
|
|
235
259
|
return new Promise((resolve, reject) => {
|
|
236
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}`
|
|
237
|
-
const
|
|
261
|
+
const connectWs = (retryIndex) => {
|
|
238
262
|
retryIndex = retryIndex || 0
|
|
239
263
|
return new Promise((resolve, reject) => {
|
|
240
264
|
if ('set-cookie' in headers) {
|
|
@@ -247,17 +271,20 @@ class BotiumConnectorVoip {
|
|
|
247
271
|
this.ws = new WebSocket(wsEndpoint)
|
|
248
272
|
}
|
|
249
273
|
this.ws.on('open', () => {
|
|
250
|
-
resolve()
|
|
274
|
+
resolve(retryIndex)
|
|
251
275
|
})
|
|
252
276
|
this.ws.on('error', () => {
|
|
253
277
|
if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
|
|
254
278
|
reject(new Error(`Websocket connection failed to ${wsEndpoint}`))
|
|
255
279
|
}
|
|
256
|
-
setTimeout(() =>
|
|
280
|
+
setTimeout(() => connectWs(retryIndex + 1).then(resolve).catch(reject), this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT])
|
|
257
281
|
})
|
|
258
282
|
})
|
|
259
283
|
}
|
|
260
|
-
|
|
284
|
+
connectWs().then((wsRetries) => {
|
|
285
|
+
if (wsRetries > 0) {
|
|
286
|
+
_info('connected_after_retries', { phase: 'websocket', retries: wsRetries })
|
|
287
|
+
}
|
|
261
288
|
if (!_.isArray(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
|
|
262
289
|
if (_.isEmpty(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
|
|
263
290
|
this.caps[Capabilities.VOIP_ICE_STUN_SERVERS] = []
|
|
@@ -301,6 +328,7 @@ class BotiumConnectorVoip {
|
|
|
301
328
|
|
|
302
329
|
this.silence = null
|
|
303
330
|
this.msgCount = 0
|
|
331
|
+
this.sttPartialCount = 0
|
|
304
332
|
this.firstMsg = true
|
|
305
333
|
this.firstSttInfoReceived = false
|
|
306
334
|
this.silenceTimeout = null
|
|
@@ -468,6 +496,10 @@ class BotiumConnectorVoip {
|
|
|
468
496
|
|
|
469
497
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
470
498
|
this.sessionId = parsedData.voipConfig.sessionId
|
|
499
|
+
_info('callinfo_initialized', {
|
|
500
|
+
sessionId: this.sessionId,
|
|
501
|
+
sttHandling: this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING]
|
|
502
|
+
})
|
|
471
503
|
}
|
|
472
504
|
|
|
473
505
|
// if sessionId is not the same as the one in the callinfo, return
|
|
@@ -485,10 +517,16 @@ class BotiumConnectorVoip {
|
|
|
485
517
|
}
|
|
486
518
|
|
|
487
519
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'connected') {
|
|
520
|
+
_info('callinfo_connected', { sessionId: this.sessionId })
|
|
488
521
|
resolve()
|
|
489
522
|
}
|
|
490
523
|
|
|
491
524
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'disconnected') {
|
|
525
|
+
_info('callinfo_disconnected', {
|
|
526
|
+
sessionId: this.sessionId,
|
|
527
|
+
connectDurationSec: parsedData.connectDuration || null,
|
|
528
|
+
sttPartialCount: this.sttPartialCount
|
|
529
|
+
})
|
|
492
530
|
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_STT_BODY))
|
|
493
531
|
if (parsedData.connectDuration && parsedData.connectDuration > 0) {
|
|
494
532
|
this.eventEmitter.emit('CONSUMPTION_METADATA', this, {
|
|
@@ -519,7 +557,9 @@ class BotiumConnectorVoip {
|
|
|
519
557
|
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
520
558
|
const tail = _extractFullRecordBase64(parsedData)
|
|
521
559
|
if (tail) this.fullRecord = (this.fullRecord || '') + tail
|
|
560
|
+
const base64Len = (this.fullRecord || tail || '').length
|
|
522
561
|
emitFullRecordAttachment(this.fullRecord || tail)
|
|
562
|
+
_info('recording_attached', { sessionId: this.sessionId, source: 'fullRecordEnd', base64Len })
|
|
523
563
|
this.end = true
|
|
524
564
|
}
|
|
525
565
|
|
|
@@ -534,11 +574,14 @@ class BotiumConnectorVoip {
|
|
|
534
574
|
|
|
535
575
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
536
576
|
// Non-chunked full record
|
|
537
|
-
|
|
577
|
+
const base64 = _extractFullRecordBase64(parsedData)
|
|
578
|
+
emitFullRecordAttachment(base64)
|
|
579
|
+
_info('recording_attached', { sessionId: this.sessionId, source: 'fullRecord', base64Len: (base64 || '').length })
|
|
538
580
|
this.end = true
|
|
539
581
|
}
|
|
540
582
|
|
|
541
583
|
if (parsedData && parsedData.data && parsedData.data.final === false) {
|
|
584
|
+
this.sttPartialCount++
|
|
542
585
|
if (this.silenceTimeout) {
|
|
543
586
|
clearTimeout(this.silenceTimeout)
|
|
544
587
|
}
|
|
@@ -586,7 +629,18 @@ class BotiumConnectorVoip {
|
|
|
586
629
|
if (parsedData && parsedData.data && parsedData.data.type === 'stt' && parsedData.data.final) {
|
|
587
630
|
const confidenceThreshold = ((this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0]) || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD])
|
|
588
631
|
const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold
|
|
632
|
+
const msgText = parsedData.data.message || ''
|
|
633
|
+
const msgLen = typeof msgText === 'string' ? msgText.length : 0
|
|
634
|
+
const msgPreview = typeof msgText === 'string' ? msgText.trim().substring(0, 80) : ''
|
|
589
635
|
debug(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`)
|
|
636
|
+
_info('stt_final', {
|
|
637
|
+
sessionId: this.sessionId,
|
|
638
|
+
message: msgPreview,
|
|
639
|
+
messageLength: msgLen,
|
|
640
|
+
confidence: this._getConfidenceScore(parsedData),
|
|
641
|
+
threshold: confidenceThreshold,
|
|
642
|
+
accepted: successfulConfidenceScore
|
|
643
|
+
})
|
|
590
644
|
// ORIGINAL: emit final message immediately, unless join hooks are active.
|
|
591
645
|
if (
|
|
592
646
|
(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && (_.isNil(this._getJoinLogicHook(this.convoStep))))
|
|
@@ -675,6 +729,19 @@ class BotiumConnectorVoip {
|
|
|
675
729
|
|
|
676
730
|
async UserSays (msg) {
|
|
677
731
|
debug('UserSays called')
|
|
732
|
+
const hasText = !!(msg && msg.messageText)
|
|
733
|
+
const hasVoiceMedia = !!(msg && msg.media && msg.media.length > 0 && msg.media[0].buffer)
|
|
734
|
+
const hasDtmf = !!(msg && msg.buttons && msg.buttons.length > 0)
|
|
735
|
+
const dtmfMatch = msg && msg.messageText && msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i)
|
|
736
|
+
const inputType = hasDtmf || dtmfMatch ? 'dtmf' : (hasText && hasVoiceMedia ? 'mixed' : hasText ? 'text' : hasVoiceMedia ? 'media' : 'unknown')
|
|
737
|
+
const msgPreview = hasText && msg.messageText ? String(msg.messageText).trim().substring(0, 80) : ''
|
|
738
|
+
_info('user_says', {
|
|
739
|
+
sessionId: this.sessionId,
|
|
740
|
+
inputType,
|
|
741
|
+
message: msgPreview || undefined,
|
|
742
|
+
messageLength: hasText && msg.messageText ? msg.messageText.length : undefined,
|
|
743
|
+
mediaSize: hasVoiceMedia && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined
|
|
744
|
+
})
|
|
678
745
|
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
679
746
|
try {
|
|
680
747
|
const safeLog = {
|
|
@@ -700,10 +767,21 @@ class BotiumConnectorVoip {
|
|
|
700
767
|
return new Promise((resolve, reject) => {
|
|
701
768
|
setTimeout(async () => {
|
|
702
769
|
let duration = 0
|
|
770
|
+
const preferVoiceCapRaw = this.caps[Capabilities.VOIP_USER_INPUT_PREFER_VOICE]
|
|
771
|
+
const preferVoice = !!preferVoiceCapRaw
|
|
772
|
+
const skipTtsForMixedInput = preferVoice && hasText && hasVoiceMedia
|
|
773
|
+
debug(`UserSays routing: hasText=${hasText} hasVoiceMedia=${hasVoiceMedia} preferVoice=${preferVoice} preferVoiceRaw=${JSON.stringify(preferVoiceCapRaw)} skipTtsForMixedInput=${skipTtsForMixedInput}`)
|
|
774
|
+
|
|
703
775
|
if (msg && msg.buttons && msg.buttons.length > 0) {
|
|
776
|
+
const digits = sanitizeDtmfDigits(msg.buttons[0].payload)
|
|
777
|
+
if (!digits) {
|
|
778
|
+
debug('sendDtmf skipped: no valid DTMF digits after sanitizing button payload')
|
|
779
|
+
return resolve()
|
|
780
|
+
}
|
|
781
|
+
debug(`Sending DTMF digits: ${digits}`)
|
|
704
782
|
const request = JSON.stringify({
|
|
705
783
|
METHOD: 'sendDtmf',
|
|
706
|
-
digits
|
|
784
|
+
digits,
|
|
707
785
|
sessionId: this.sessionId
|
|
708
786
|
})
|
|
709
787
|
this.ws.send(request)
|
|
@@ -711,8 +789,17 @@ class BotiumConnectorVoip {
|
|
|
711
789
|
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
712
790
|
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i)
|
|
713
791
|
if (dtmfMatch && dtmfMatch[1]) {
|
|
714
|
-
const
|
|
715
|
-
|
|
792
|
+
const rawDigits = dtmfMatch[1]
|
|
793
|
+
const digits = sanitizeDtmfDigits(rawDigits)
|
|
794
|
+
if (!digits) {
|
|
795
|
+
debug(`sendDtmf skipped: no valid DTMF digits after sanitizing <DTMF> content (raw length=${String(rawDigits).length})`)
|
|
796
|
+
return resolve()
|
|
797
|
+
}
|
|
798
|
+
if (digits !== String(rawDigits).replace(/\s/g, '')) {
|
|
799
|
+
debug(`Sending DTMF from messageText (sanitized): "${rawDigits}" -> "${digits}"`)
|
|
800
|
+
} else {
|
|
801
|
+
debug(`Sending DTMF from messageText: ${digits}`)
|
|
802
|
+
}
|
|
716
803
|
const request = JSON.stringify({
|
|
717
804
|
METHOD: 'sendDtmf',
|
|
718
805
|
digits,
|
|
@@ -721,56 +808,62 @@ class BotiumConnectorVoip {
|
|
|
721
808
|
this.ws.send(request)
|
|
722
809
|
return resolve()
|
|
723
810
|
}
|
|
724
|
-
if (!
|
|
725
|
-
if (!
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
} else {
|
|
729
|
-
const ttsRequest = this._buildTtsRequest(msg.messageText)
|
|
730
|
-
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'))
|
|
731
|
-
msg.sourceData = ttsRequest
|
|
732
|
-
|
|
733
|
-
let ttsResult = null
|
|
734
|
-
try {
|
|
735
|
-
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText)
|
|
736
|
-
} catch (err) {
|
|
737
|
-
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`))
|
|
738
|
-
}
|
|
739
|
-
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
740
|
-
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY))
|
|
741
|
-
this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
|
|
742
|
-
type: _.isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
|
|
743
|
-
metricName: 'consumption.e2e.voip.tts.characters',
|
|
744
|
-
transactions: msg.messageText.length,
|
|
745
|
-
apiKey
|
|
746
|
-
})
|
|
747
|
-
}
|
|
748
|
-
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
749
|
-
duration = parseFloat(ttsResult.duration) || 0
|
|
750
|
-
const b64Buffer = ttsResult.buffer.toString('base64')
|
|
751
|
-
const request = JSON.stringify({
|
|
752
|
-
METHOD: 'sendAudio',
|
|
753
|
-
PESQ: false,
|
|
754
|
-
sessionId: this.sessionId,
|
|
755
|
-
b64_buffer: b64Buffer
|
|
756
|
-
})
|
|
757
|
-
if (this._lastBotSaysQueuedAt) {
|
|
758
|
-
const latencyMs = Date.now() - this._lastBotSaysQueuedAt
|
|
759
|
-
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>'
|
|
760
|
-
debug(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`)
|
|
811
|
+
if (!skipTtsForMixedInput) {
|
|
812
|
+
if (!this.axiosTtsParams) {
|
|
813
|
+
if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
|
|
814
|
+
return reject(new Error('TTS not configured, only audio input supported'))
|
|
761
815
|
}
|
|
762
|
-
msg.attachments.push({
|
|
763
|
-
name: 'tts.wav',
|
|
764
|
-
mimeType: 'audio/wav',
|
|
765
|
-
base64: b64Buffer
|
|
766
|
-
})
|
|
767
|
-
this.ws.send(request)
|
|
768
816
|
} else {
|
|
769
|
-
|
|
817
|
+
debug('UserSays routing: executing TTS branch')
|
|
818
|
+
const ttsRequest = this._buildTtsRequest(msg.messageText)
|
|
819
|
+
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'))
|
|
820
|
+
msg.sourceData = ttsRequest
|
|
821
|
+
|
|
822
|
+
let ttsResult = null
|
|
823
|
+
try {
|
|
824
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText)
|
|
825
|
+
} catch (err) {
|
|
826
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`))
|
|
827
|
+
}
|
|
828
|
+
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
829
|
+
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY))
|
|
830
|
+
this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
|
|
831
|
+
type: _.isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
|
|
832
|
+
metricName: 'consumption.e2e.voip.tts.characters',
|
|
833
|
+
transactions: msg.messageText.length,
|
|
834
|
+
apiKey
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
838
|
+
duration = parseFloat(ttsResult.duration) || 0
|
|
839
|
+
const b64Buffer = ttsResult.buffer.toString('base64')
|
|
840
|
+
const request = JSON.stringify({
|
|
841
|
+
METHOD: 'sendAudio',
|
|
842
|
+
PESQ: false,
|
|
843
|
+
sessionId: this.sessionId,
|
|
844
|
+
b64_buffer: b64Buffer
|
|
845
|
+
})
|
|
846
|
+
if (this._lastBotSaysQueuedAt) {
|
|
847
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt
|
|
848
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>'
|
|
849
|
+
debug(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`)
|
|
850
|
+
}
|
|
851
|
+
msg.attachments.push({
|
|
852
|
+
name: 'tts.wav',
|
|
853
|
+
mimeType: 'audio/wav',
|
|
854
|
+
base64: b64Buffer
|
|
855
|
+
})
|
|
856
|
+
this.ws.send(request)
|
|
857
|
+
} else {
|
|
858
|
+
return reject(new Error('TTS failed, response is empty'))
|
|
859
|
+
}
|
|
770
860
|
}
|
|
861
|
+
} else {
|
|
862
|
+
debug('UserSays routing: skipping TTS for mixed input because VOIP_USER_INPUT_PREFER_VOICE is enabled')
|
|
771
863
|
}
|
|
772
864
|
}
|
|
773
865
|
if (msg && msg.media && msg.media.length > 0 && msg.media[0].buffer) {
|
|
866
|
+
debug('UserSays routing: executing MEDIA branch')
|
|
774
867
|
const request = JSON.stringify({
|
|
775
868
|
METHOD: 'sendAudio',
|
|
776
869
|
sessionId: this.sessionId,
|