@tiledesk/tiledesk-voice-twilio-connector 0.1.26-rc12 → 0.1.26-rc13
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/index.js +230 -187
- package/package.json +1 -1
- package/tiledesk/TiledeskChannel.js +1 -1
- package/tiledesk/TiledeskTwilioTranslator.js +59 -32
- package/tiledesk/VoiceChannel.js +42 -37
- package/tiledesk/constants.js +13 -2
- package/tiledesk/errors.js +28 -0
- package/tiledesk/services/AiService.js +86 -64
- package/tiledesk/services/UploadService.js +4 -1
- package/tiledesk/services/voiceEventEmitter.js +6 -0
- package/tiledesk/utils.js +20 -1
package/index.js
CHANGED
|
@@ -98,12 +98,6 @@ router.post("/tiledesk", async (req, res) => {
|
|
|
98
98
|
});
|
|
99
99
|
tdChannel.setProjectId(project_id)
|
|
100
100
|
|
|
101
|
-
/*SKIP INFO MESSAGES*/
|
|
102
|
-
/*SKIP CURRENT USER MESSAGES*/
|
|
103
|
-
if(!utilsMess.messageType(TYPE_MESSAGE.INFO, tiledeskMessage) && !(tiledeskMessage.sender.indexOf("vxml") > -1) ){
|
|
104
|
-
winston.debug("> whook SAVE MESSAGE TO QUEUE " + JSON.stringify(tiledeskMessage) );
|
|
105
|
-
}
|
|
106
|
-
|
|
107
101
|
await tdChannel.addMessageToQueue(tiledeskMessage)
|
|
108
102
|
|
|
109
103
|
res.send("(voice) Message received from Voice Twilio Proxy");
|
|
@@ -156,32 +150,33 @@ router.post('/webhook/:id_project', async (req, res) => {
|
|
|
156
150
|
return;
|
|
157
151
|
}
|
|
158
152
|
let end2 = new Date().getTime();
|
|
159
|
-
// console.log('Time after signIn: ', end2-start2, '[ms]')
|
|
160
153
|
|
|
161
154
|
//let conversation_id = await tdChannel.getConversation(ani, callId, user.token);
|
|
162
155
|
let conversation_id = await tdChannel.generateConversation(from, callSid, user.token);
|
|
163
156
|
winston.debug("(voice) conversation returned:"+ conversation_id);
|
|
164
157
|
|
|
165
|
-
|
|
166
|
-
//GET AND SAVE GPT-KET IF
|
|
167
158
|
let integrations = [], publicKey = false;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
159
|
+
try {
|
|
160
|
+
//GET AND SAVE GPT-KET IF
|
|
161
|
+
let key = await integrationService.getKeyFromIntegrations(project_id, 'openai', settings.token)
|
|
162
|
+
if (!key) {
|
|
163
|
+
winston.debug("(voice) - Key not found in Integrations. Searching in kb settings...");
|
|
164
|
+
key = await integrationService.getKeyFromKbSettings(project_id, settings.token);
|
|
165
|
+
}
|
|
166
|
+
if (!key) {
|
|
167
|
+
winston.debug("(voice) - Retrieve public gptkey")
|
|
168
|
+
key = GPT_KEY;
|
|
169
|
+
publicKey = true;
|
|
170
|
+
}
|
|
171
|
+
integrations.push({type: 'openai', key: key, publicKey: publicKey})
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
let eleven_labs = await integrationService.getKeyFromIntegrations(project_id, 'elevenlabs', settings.token)
|
|
174
|
+
if (eleven_labs) {
|
|
175
|
+
winston.debug("(voice) - Key found in Integrations: "+ eleven_labs);
|
|
176
|
+
integrations.push({type: 'elevenlabs', key: eleven_labs, publicKey: false})
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
winston.error('(voice) - Error retrieving integrations keys:', error);
|
|
185
180
|
}
|
|
186
181
|
|
|
187
182
|
//save data to redis
|
|
@@ -240,7 +235,7 @@ router.post('/webhook/:id_project', async (req, res) => {
|
|
|
240
235
|
winston.debug('(voice) /webhook/:id_project messageVXML-->'+ messageToVXML)
|
|
241
236
|
|
|
242
237
|
let end_call = new Date().getTime();
|
|
243
|
-
winston.info(`Time to respond to /webhook/${project_id}
|
|
238
|
+
winston.info(`Time to respond to /webhook/${project_id}: ${(end_call-start_call)}[ms]`)
|
|
244
239
|
|
|
245
240
|
// Render the response as XML in reply to the webhook request
|
|
246
241
|
res.set('Content-Type', 'text/xml');
|
|
@@ -252,7 +247,6 @@ router.post('/nextblock_old/:callSid/', async(req, res) => {
|
|
|
252
247
|
let start_call = new Date()
|
|
253
248
|
winston.debug("(voice) called POST /nextblock ", req.body);
|
|
254
249
|
winston.debug("(voice) called POST /nextblock query ", req.query);
|
|
255
|
-
console.log('/nextblock at: ', new Date(), 'with text:', req.body.SpeechResult)
|
|
256
250
|
|
|
257
251
|
let usertext = req.body.SpeechResult;
|
|
258
252
|
let confidence = req.body.Confidence
|
|
@@ -326,7 +320,7 @@ router.post('/nextblock_old/:callSid/', async(req, res) => {
|
|
|
326
320
|
|
|
327
321
|
// convert response to vxml
|
|
328
322
|
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
329
|
-
winston.
|
|
323
|
+
winston.debug("(voice) VXML to SEND: "+ messageToVXML);
|
|
330
324
|
|
|
331
325
|
let end_call = new Date()
|
|
332
326
|
console.log('Time to responde to /nextblock/:callSid : ', end_call-start_call, '[ms]')
|
|
@@ -339,8 +333,6 @@ router.post('/nextblock_old/:callSid/', async(req, res) => {
|
|
|
339
333
|
|
|
340
334
|
router.post('/nextblock/:callSid/', async(req, res) => {
|
|
341
335
|
let start_call = new Date()
|
|
342
|
-
winston.debug("(voice) called POST /nextblock ", req.body);
|
|
343
|
-
winston.debug("(voice) called POST /nextblock query ", req.query);
|
|
344
336
|
winston.verbose("(voice) called POST /nextblock at" + new Date() + "with text: "+ req.body.SpeechResult);
|
|
345
337
|
|
|
346
338
|
let usertext = req.body.SpeechResult;
|
|
@@ -405,7 +397,6 @@ router.post('/nextblock/:callSid/', async(req, res) => {
|
|
|
405
397
|
let start_time_send_message = new Date()
|
|
406
398
|
let tdMessage = await tdChannel.send(tiledeskMessage, user.token, conversation_id);
|
|
407
399
|
let end_time_send_message = new Date()
|
|
408
|
-
winston.debug("message sent : ", tdMessage);
|
|
409
400
|
winston.verbose(`(else) Time to send message to tiledesk in /nextblock/${callSid} : ${(end_time_send_message-start_time_send_message)}[ms] with text ` + tdMessage.text + ' --- at time:' + new Date())
|
|
410
401
|
|
|
411
402
|
let start_time_get_message = new Date()
|
|
@@ -423,7 +414,7 @@ router.post('/nextblock/:callSid/', async(req, res) => {
|
|
|
423
414
|
}
|
|
424
415
|
})
|
|
425
416
|
let end_promise_message = new Date()
|
|
426
|
-
winston.verbose(`Time to manage message in Promise /nextblock/${callSid}: ${(end_promise_message-start_promise_message)}[ms]` + ' with text' + message.text + ' --- at time --' + new Date())
|
|
417
|
+
winston.verbose(`Time to manage message in Promise /nextblock/${callSid}: ${(end_promise_message-start_promise_message)}[ms]` + ' with text:' + message.text + ' --- at time --' + new Date())
|
|
427
418
|
|
|
428
419
|
// convert response to vxml
|
|
429
420
|
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
@@ -525,8 +516,6 @@ async function getMessage(callSid, ani, project_id, conversation_id){
|
|
|
525
516
|
|
|
526
517
|
router.post('/speechresult/:callSid', async (req, res) => {
|
|
527
518
|
let start_call = new Date();
|
|
528
|
-
winston.debug("(voice) called POST /speechresult ", req.body);
|
|
529
|
-
winston.debug("(voice) called POST /speechresult query ", req.query);
|
|
530
519
|
winston.verbose("(voice) called POST /speechresult at" + new Date() + "with text: "+ req.body.SpeechResult);
|
|
531
520
|
|
|
532
521
|
let usertext = req.body.SpeechResult;
|
|
@@ -612,7 +601,7 @@ router.post('/speechresult/:callSid', async (req, res) => {
|
|
|
612
601
|
|
|
613
602
|
// convert response to vxml
|
|
614
603
|
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
615
|
-
winston.
|
|
604
|
+
winston.debug("(voice) VXML to SEND: "+ messageToVXML);
|
|
616
605
|
|
|
617
606
|
let end_call = new Date()
|
|
618
607
|
winston.info(`Time to respond to /speechresult/${callSid} : ${(end_call-start_call)} [ms]`)
|
|
@@ -622,12 +611,142 @@ router.post('/speechresult/:callSid', async (req, res) => {
|
|
|
622
611
|
res.status(200).send(messageToVXML);
|
|
623
612
|
})
|
|
624
613
|
|
|
614
|
+
/* ----> called with Record tag in action property <----- */
|
|
615
|
+
router.post('/record/action/:callSid/',async (req, res) => {
|
|
616
|
+
winston.verbose('+++++++++++(voice) called POST record/action/:callSid at time ' + new Date());
|
|
617
|
+
|
|
618
|
+
let callSid = req.body.CallSid;
|
|
619
|
+
let sessionInfo;
|
|
620
|
+
let project_id, conversation_id, user;
|
|
621
|
+
let from, to;
|
|
622
|
+
|
|
623
|
+
let redis_data = await redis_client.get('tiledesk:voice:'+callSid+':session');
|
|
624
|
+
if (!redis_data) {
|
|
625
|
+
return res.status(500).send({ success: "false", message: "Can't retrive data for callSid ->" + callSid });
|
|
626
|
+
}
|
|
627
|
+
sessionInfo = JSON.parse(redis_data)
|
|
628
|
+
project_id = sessionInfo.project_id;
|
|
629
|
+
from = sessionInfo.from;
|
|
630
|
+
to = sessionInfo.to;
|
|
631
|
+
conversation_id = sessionInfo.conversation_id;
|
|
632
|
+
user = sessionInfo.user;
|
|
633
|
+
|
|
634
|
+
let vxmlAttributes = {
|
|
635
|
+
TTS_VOICE_LANGUAGE: VOICE_LANGUAGE,
|
|
636
|
+
TTS_VOICE_NAME: VOICE_NAME,
|
|
637
|
+
callSid: callSid,
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const tdChannel = new TiledeskChannel({
|
|
641
|
+
API_URL: API_URL,
|
|
642
|
+
redis_client: redis_client
|
|
643
|
+
})
|
|
644
|
+
tdChannel.setProjectId(project_id)
|
|
645
|
+
|
|
646
|
+
const tdTranslator = new TiledeskTwilioTranslator({
|
|
647
|
+
BASE_URL: BASE_URL,
|
|
648
|
+
aiService: aiService,
|
|
649
|
+
uploadService: uploadService
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
let start_time_get_message = new Date()
|
|
654
|
+
let message = await getMessage(callSid, from, project_id, conversation_id)
|
|
655
|
+
winston.debug('message from getMessage in /record/action/: ', message)
|
|
656
|
+
let end_time_get_message = new Date()
|
|
657
|
+
winston.verbose(`Time to getMessage from queue in /record/action/${callSid} : ${(end_time_get_message-start_time_get_message)}[ms]` + ' --- at time:' + new Date())
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
//generate Tiledesk wait message
|
|
661
|
+
// let delayTime = await voiceChannel.getNextDelayTimeForCallId(callSid)
|
|
662
|
+
// let message = await tdChannel.generateWaitTdMessage(from, delayTime)
|
|
663
|
+
// //update delayIndex for wait command message time
|
|
664
|
+
// await voiceChannel.saveDelayIndexForCallId(callSid)
|
|
665
|
+
|
|
666
|
+
// convert response to vxml
|
|
667
|
+
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
668
|
+
winston.debug("(voice) /record/action VXML to SEND: "+ messageToVXML);
|
|
669
|
+
|
|
670
|
+
res.set('Content-Type', 'application/xml');
|
|
671
|
+
res.status(200).send(messageToVXML);
|
|
672
|
+
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
/* ----> called with Record tag in recordingStatusCallback property <----- */
|
|
676
|
+
router.post('/record/callback/:callSid/',async (req, res) => {
|
|
677
|
+
winston.verbose('+++++++++++(voice) called POST record/callback/:callSid at time', new Date());
|
|
678
|
+
let start_call = new Date();
|
|
679
|
+
|
|
680
|
+
let callSid = req.params.callSid || req.body.CallSid;
|
|
681
|
+
let audioFileUrl = req.body.RecordingUrl;
|
|
682
|
+
let audioFileDuration = req.body.RecordingDuration;
|
|
683
|
+
let button_action = req.query.button_action ? '#' + req.query.button_action : '';
|
|
684
|
+
let previousIntentName = req.query.intentName || '';
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
let sessionInfo;
|
|
688
|
+
let project_id, conversation_id, user;
|
|
689
|
+
let from, to;
|
|
690
|
+
|
|
691
|
+
let redis_data = await redis_client.get('tiledesk:voice:'+callSid+':session');
|
|
692
|
+
if (!redis_data) {
|
|
693
|
+
return res.status(500).send({ success: "false", message: "Can't retrive data for callSid ->" + callSid });
|
|
694
|
+
}
|
|
695
|
+
sessionInfo = JSON.parse(redis_data)
|
|
696
|
+
project_id = sessionInfo.project_id;
|
|
697
|
+
from = sessionInfo.from;
|
|
698
|
+
to = sessionInfo.to;
|
|
699
|
+
conversation_id = sessionInfo.conversation_id;
|
|
700
|
+
user = sessionInfo.user;
|
|
701
|
+
|
|
702
|
+
const tdChannel = new TiledeskChannel({
|
|
703
|
+
API_URL: API_URL,
|
|
704
|
+
redis_client: redis_client
|
|
705
|
+
})
|
|
706
|
+
tdChannel.setProjectId(project_id)
|
|
707
|
+
|
|
708
|
+
const CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
709
|
+
let settings = await db.get(CONTENT_KEY);
|
|
710
|
+
if(!settings){
|
|
711
|
+
return res.status(404).send({error: "VOICE Channel not already connected"})
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
let tiledeskMessage = null;
|
|
715
|
+
// tiledeskMessage = buildNoInputMessage('no_input', { from, button_action, payload: { event: 'no_input', lastBlock: previousIntentName, lastTimestamp: Date.now()} });
|
|
716
|
+
|
|
717
|
+
//SPEECH TO TEXT
|
|
718
|
+
const attributes = await voiceChannel.getSettingsForCallId(callSid);
|
|
719
|
+
winston.debug(`[VOICE] getting text message from STT: ${audioFileUrl}, model: ${attributes.STT_MODEL}`);
|
|
720
|
+
// generateSTT ritorna sempre un oggetto coerente (anche vuoto o /close)
|
|
721
|
+
tiledeskMessage = await generateSTT(audioFileUrl, attributes, sessionInfo, settings)
|
|
722
|
+
winston.debug('[VOICE] tiledeskMessage from STT: ', tiledeskMessage)
|
|
723
|
+
if (!tiledeskMessage || Object.keys(tiledeskMessage).length === 0) {
|
|
724
|
+
winston.debug(`[VOICE] STT result empty, fallback to no_input branch for callSid ${callSid}`);
|
|
725
|
+
tiledeskMessage = buildNoInputMessage('no_input', { from, button_action, payload: { event: 'no_input', lastBlock: previousIntentName, lastTimestamp: Date.now()} });
|
|
726
|
+
}else {
|
|
727
|
+
const normalizedText = utils.normalizeSTT(tiledeskMessage.text);
|
|
728
|
+
winston.verbose(`[VOICE] normalized STT text: ${normalizedText} for callSid ${callSid}`);
|
|
729
|
+
if(!normalizedText){
|
|
730
|
+
tiledeskMessage = buildNoInputMessage('no_input', { from, button_action, payload: { event: 'no_input', lastBlock: previousIntentName, lastTimestamp: Date.now()} });
|
|
731
|
+
}else{
|
|
732
|
+
tiledeskMessage.text = normalizedText;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
//send message to tiledesk
|
|
737
|
+
let tdMessage = await tdChannel.send(tiledeskMessage, user.token, conversation_id);
|
|
738
|
+
let end_call = new Date();
|
|
739
|
+
winston.info(`Time to respond to /record/callback/${callSid} : ${(end_call-start_call)} [ms]`)
|
|
740
|
+
|
|
741
|
+
res.status(200).send({ success: true , message: "Message sent to Tiledesk for callSid " + callSid});
|
|
742
|
+
})
|
|
743
|
+
|
|
625
744
|
|
|
626
745
|
router.post('/menublock/:callSid', async (req, res) => {
|
|
627
746
|
let start_call = new Date().getTime();
|
|
628
747
|
winston.debug("(voice) called POST /menu", req.body);
|
|
629
748
|
winston.debug("(voice) called POST /menu query" , req.query);
|
|
630
|
-
winston.verbose('/menublock at: '
|
|
749
|
+
winston.verbose('/menublock at: ' + new Date() + 'with text:'+ req.body.Digits)
|
|
631
750
|
|
|
632
751
|
let message_text = '';
|
|
633
752
|
let attributes = {};
|
|
@@ -731,7 +850,7 @@ router.post('/menublock/:callSid', async (req, res) => {
|
|
|
731
850
|
|
|
732
851
|
// convert response to vxml
|
|
733
852
|
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
734
|
-
winston.
|
|
853
|
+
winston.debug("(voice) VXML to SEND: "+ messageToVXML);
|
|
735
854
|
|
|
736
855
|
let end_call = new Date().getTime();
|
|
737
856
|
winston.info(`Time to respond to /menublock/${callSid} : ${(end_call-start_call)} [ms]`)
|
|
@@ -810,7 +929,7 @@ router.post('/handle/:callSid/:event', async (req, res) => {
|
|
|
810
929
|
|
|
811
930
|
// convert response to vxml
|
|
812
931
|
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes,sessionInfo)
|
|
813
|
-
winston.
|
|
932
|
+
winston.debug("(voice) VXML to SEND: "+ messageToVXML);
|
|
814
933
|
|
|
815
934
|
res.set('Content-Type', 'application/xml');
|
|
816
935
|
res.status(200).send(messageToVXML);
|
|
@@ -915,7 +1034,7 @@ router.post('/event/:callSid/:event', async(req, res)=> {
|
|
|
915
1034
|
|
|
916
1035
|
// convert response to vxml
|
|
917
1036
|
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
918
|
-
winston.
|
|
1037
|
+
winston.debug("(voice) VXML to SEND: "+ messageToVXML);
|
|
919
1038
|
|
|
920
1039
|
res.set('Content-Type', 'application/xml');
|
|
921
1040
|
res.status(200).send(messageToVXML);
|
|
@@ -990,8 +1109,7 @@ router.post('/twilio/status',async (req, res) => {
|
|
|
990
1109
|
let tdMessage = await tdChannel.send(tiledeskMessage, user.token, conversation_id);
|
|
991
1110
|
|
|
992
1111
|
//remove session data for current callId and relative queue data
|
|
993
|
-
await
|
|
994
|
-
await redis_client.del('tiledesk:voice:'+callSid+':delayIndex');
|
|
1112
|
+
await voiceChannel.deleteCallKeys(callSid);
|
|
995
1113
|
await tdChannel.clearQueue(conversation_id);
|
|
996
1114
|
break;
|
|
997
1115
|
}
|
|
@@ -1012,172 +1130,97 @@ router.post('/twilio/fail',async (req, res) => {
|
|
|
1012
1130
|
})
|
|
1013
1131
|
|
|
1014
1132
|
|
|
1015
|
-
/* ----> catch Twilio Events <----- */
|
|
1016
|
-
router.post('/record/:callSid/',async (req, res) => {
|
|
1017
|
-
winston.debug('+++++++++++(voice) called POST record/:callSid ', req.body);
|
|
1018
|
-
|
|
1019
|
-
let callSid = req.body.CallSid;
|
|
1020
|
-
let audioFileUrl = req.body.RecordingUrl;
|
|
1021
|
-
|
|
1022
|
-
let sessionInfo;
|
|
1023
|
-
let project_id, conversation_id, user;
|
|
1024
|
-
let from, to;
|
|
1025
|
-
|
|
1026
|
-
let redis_data = await redis_client.get('tiledesk:voice:'+callSid+':session');
|
|
1027
|
-
if (!redis_data) {
|
|
1028
|
-
return res.status(500).send({ success: "false", message: "Can't retrive data for callSid ->" + callSid });
|
|
1029
|
-
}
|
|
1030
|
-
sessionInfo = JSON.parse(redis_data)
|
|
1031
|
-
project_id = sessionInfo.project_id;
|
|
1032
|
-
from = sessionInfo.from;
|
|
1033
|
-
to = sessionInfo.to;
|
|
1034
|
-
conversation_id = sessionInfo.conversation_id;
|
|
1035
|
-
user = sessionInfo.user;
|
|
1036
|
-
|
|
1037
|
-
let vxmlAttributes = {
|
|
1038
|
-
TTS_VOICE_LANGUAGE: VOICE_LANGUAGE,
|
|
1039
|
-
TTS_VOICE_NAME: VOICE_NAME,
|
|
1040
|
-
callSid: callSid,
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const tdChannel = new TiledeskChannel({
|
|
1045
|
-
API_URL: API_URL,
|
|
1046
|
-
redis_client: redis_client
|
|
1047
|
-
})
|
|
1048
|
-
tdChannel.setProjectId(project_id)
|
|
1049
|
-
|
|
1050
|
-
const tdTranslator = new TiledeskTwilioTranslator({
|
|
1051
|
-
BASE_URL: BASE_URL,
|
|
1052
|
-
aiService: aiService,
|
|
1053
|
-
uploadService: uploadService
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
const CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
1057
|
-
let settings = await db.get(CONTENT_KEY);
|
|
1058
|
-
if(!settings){
|
|
1059
|
-
return res.status(404).send({error: "VOICE Channel not already connected"})
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
let attributes = await voiceChannel.getSettingsForCallId(callSid);
|
|
1064
|
-
console.log('attributessss', attributes)
|
|
1065
|
-
|
|
1066
|
-
//SPEECH TO TEXT
|
|
1067
|
-
console.log('getting text message . . . ', audioFileUrl, attributes.STT_MODEL)
|
|
1068
|
-
let tiledeskMessage = await generateSTT(audioFileUrl, attributes, sessionInfo, settings)
|
|
1069
|
-
console.log('(voice) Message captured after STT -->', tiledeskMessage)
|
|
1070
|
-
|
|
1071
|
-
if(!tiledeskMessage){
|
|
1072
|
-
//case NO_INPUT
|
|
1073
|
-
const queryString = utils.buildQueryString(req.query);
|
|
1074
|
-
winston.debug('case no input.. redirect '+ queryString)
|
|
1075
|
-
|
|
1076
|
-
return await axios({
|
|
1077
|
-
url: "http://localhost:3000/handle/" + callSid + '/no_input'+ queryString,
|
|
1078
|
-
headers: req.headers,
|
|
1079
|
-
data: req.body,
|
|
1080
|
-
method: 'POST'
|
|
1081
|
-
}).then((response) => {
|
|
1082
|
-
winston.debug("[TiledeskChannel] speechToText response : ", response.data);
|
|
1083
|
-
return res.status(response.status).send(response.data);
|
|
1084
|
-
}).catch((err) => {
|
|
1085
|
-
winston.error("[TiledeskChannel] speechToText error: ", err);
|
|
1086
|
-
return res.status(500).send({ success: false, message: "Errore while redirect to /handle for callSid " + callSid});;
|
|
1087
|
-
})
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
let tdMessage = await tdChannel.send(tiledeskMessage, user.token, conversation_id);
|
|
1092
|
-
winston.debug("message sent : ", tdMessage);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
//generate Tiledesk wait message
|
|
1096
|
-
let delayTime = await voiceChannel.getNextDelayTimeForCallId(callSid)
|
|
1097
|
-
let message = await tdChannel.generateWaitTdMessage(from, delayTime)
|
|
1098
|
-
//update delayIndex for wait command message time
|
|
1099
|
-
await voiceChannel.saveDelayIndexForCallId(callSid)
|
|
1100
|
-
|
|
1101
|
-
// convert response to vxml
|
|
1102
|
-
let messageToVXML = await tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
1103
|
-
winston.verbose("(voice) VXML to SEND: "+ messageToVXML);
|
|
1104
|
-
|
|
1105
|
-
res.set('Content-Type', 'application/xml');
|
|
1106
|
-
res.status(200).send(messageToVXML);
|
|
1107
|
-
|
|
1108
|
-
})
|
|
1109
1133
|
|
|
1110
1134
|
async function generateSTT(audioFileUrl, attributes, sessionInfo, settings){
|
|
1111
1135
|
|
|
1112
1136
|
winston.debug("(voice) generateSTT: "+ attributes.VOICE_PROVIDER);
|
|
1113
1137
|
|
|
1114
|
-
let tiledeskMessage = {}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
let
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1138
|
+
let tiledeskMessage = {};
|
|
1139
|
+
let text = null;
|
|
1140
|
+
|
|
1141
|
+
try {
|
|
1142
|
+
switch(attributes.VOICE_PROVIDER){
|
|
1143
|
+
case VOICE_PROVIDER.OPENAI: {
|
|
1144
|
+
let GPT_KEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
|
|
1145
|
+
let publicKey = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.publicKey
|
|
1146
|
+
if(publicKey){
|
|
1147
|
+
let keep_going = await aiService.checkQuoteAvailability(sessionInfo.project_id, settings.token)
|
|
1148
|
+
winston.debug('(voice) checkQuoteAvailability return: '+ keep_going);
|
|
1149
|
+
if(!keep_going){
|
|
1150
|
+
//no token is available --> close conversation
|
|
1151
|
+
return tiledeskMessage= {
|
|
1152
|
+
//text:'\\close',
|
|
1153
|
+
text:'/close',
|
|
1154
|
+
senderFullname: sessionInfo.from,
|
|
1155
|
+
type: 'text',
|
|
1156
|
+
channel: { name: CHANNEL_NAME },
|
|
1157
|
+
attributes: {
|
|
1158
|
+
subtype: "info",
|
|
1159
|
+
action: 'close'+JSON.stringify({event: 'quota_exceeded'}),
|
|
1160
|
+
payload: {
|
|
1161
|
+
catchEvent: 'quota_exceeded'
|
|
1162
|
+
},
|
|
1163
|
+
timestamp: 'xxxxxx'
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1141
1166
|
|
|
1167
|
+
}
|
|
1142
1168
|
}
|
|
1169
|
+
|
|
1170
|
+
text = await aiService.speechToText(audioFileUrl, attributes.STT_MODEL, GPT_KEY)
|
|
1171
|
+
break;
|
|
1143
1172
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
// La condizione negli input del metodo è corretta, ma può essere scritta in modo più leggibile:
|
|
1158
|
-
const ttsLanguage = attributes.TTS_LANGUAGE || 'en';
|
|
1159
|
-
text = await this.aiService.speechToTextElevenLabs(
|
|
1160
|
-
audioFileUrl,
|
|
1161
|
-
attributes.STT_MODEL,
|
|
1162
|
-
ttsLanguage,
|
|
1163
|
-
ELEVENLABS_APIKEY
|
|
1164
|
-
).catch((err) => {
|
|
1165
|
-
winston.error('errr while creating elevenlabs audio message', err?.response?.data);
|
|
1166
|
-
});
|
|
1167
|
-
tiledeskMessage= {
|
|
1173
|
+
case VOICE_PROVIDER.ELEVENLABS: {
|
|
1174
|
+
let ELEVENLABS_APIKEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
|
|
1175
|
+
const ttsLanguage = attributes.TTS_LANGUAGE || 'en';
|
|
1176
|
+
text = await aiService.speechToTextElevenLabs( audioFileUrl, attributes.STT_MODEL, ttsLanguage, ELEVENLABS_APIKEY )
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
default:
|
|
1180
|
+
throw new Error('Unsupported VOICE_PROVIDER: ' + attributes.VOICE_PROVIDER);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if(text){
|
|
1184
|
+
winston.debug('[STT] text empty → fallback no_input');
|
|
1185
|
+
tiledeskMessage = {
|
|
1168
1186
|
text: text,
|
|
1169
1187
|
senderFullname: sessionInfo.from,
|
|
1170
1188
|
type: 'text',
|
|
1171
1189
|
channel: { name: CHANNEL_NAME }
|
|
1172
1190
|
};
|
|
1173
|
-
|
|
1191
|
+
}
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
winston.error('[STT] generateSTT error:', error);
|
|
1194
|
+
switch (error.code) {
|
|
1195
|
+
case 'AISERVICE_FAILED':
|
|
1196
|
+
winston.error('[STT] AISERVICE_FAILED → ', error.message);
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1174
1199
|
|
|
1200
|
+
// fallback: tiledeskMessage vuoto
|
|
1201
|
+
tiledeskMessage = {};
|
|
1202
|
+
|
|
1175
1203
|
}
|
|
1176
1204
|
|
|
1177
1205
|
return tiledeskMessage
|
|
1178
1206
|
}
|
|
1179
1207
|
|
|
1180
1208
|
|
|
1209
|
+
async function buildNoInputMessage(event, { from, button_action, payload }) {
|
|
1210
|
+
return {
|
|
1211
|
+
text: `/${event}`,
|
|
1212
|
+
senderFullname: from,
|
|
1213
|
+
type: 'text',
|
|
1214
|
+
channel: { name: CHANNEL_NAME },
|
|
1215
|
+
attributes: {
|
|
1216
|
+
type: 'info',
|
|
1217
|
+
action: button_action,
|
|
1218
|
+
payload: payload
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
|
|
1181
1224
|
|
|
1182
1225
|
router.get('/addon/transcript', async (req, res) => {
|
|
1183
1226
|
winston.debug("(voice) called GET /transcript query-->" , req.query);
|
package/package.json
CHANGED
|
@@ -266,7 +266,7 @@ class TiledeskChannel {
|
|
|
266
266
|
/** SUBSCRIBE TO REDIS TOPIC */
|
|
267
267
|
async subscribeToTopic(conversation_id){
|
|
268
268
|
const topic = `tiledesk:conversation:${conversation_id}`;
|
|
269
|
-
console.log("subscribeToTopic: " + topic);
|
|
269
|
+
// console.log("subscribeToTopic: " + topic);
|
|
270
270
|
|
|
271
271
|
// duplichi il client principale
|
|
272
272
|
const subscriber = this.redis_client.duplicate();
|
|
@@ -11,11 +11,16 @@ const SETTING_MESSAGE = require('./constants').SETTING_MESSAGE
|
|
|
11
11
|
const CHANNEL_NAME = require('./constants').CHANNEL_NAME
|
|
12
12
|
const VOICE_PROVIDER = require('./constants').VOICE_PROVIDER;
|
|
13
13
|
const OPENAI_SETTINGS = require('./constants').OPENAI_SETTINGS;
|
|
14
|
+
const ELEVENLABS_SETTINGS = require('./constants').ELEVENLABS_SETTINGS;
|
|
14
15
|
|
|
15
16
|
const TYPE_ACTION_VXML = require('./constants').TYPE_ACTION_VXML
|
|
16
17
|
const TYPE_MESSAGE = require('./constants').TYPE_MESSAGE
|
|
17
18
|
const INFO_MESSAGE_TYPE = require('./constants').INFO_MESSAGE_TYPE
|
|
18
19
|
|
|
20
|
+
const voiceEventEmitter = require('./services/voiceEventEmitter');
|
|
21
|
+
|
|
22
|
+
const { SttError } = require('./errors');
|
|
23
|
+
|
|
19
24
|
class TiledeskTwilioTranslator {
|
|
20
25
|
/**
|
|
21
26
|
* Constructor for TiledeskVXMLTranslator
|
|
@@ -67,8 +72,8 @@ class TiledeskTwilioTranslator {
|
|
|
67
72
|
this.voiceProvider = VOICE_PROVIDER.TWILIO
|
|
68
73
|
if(flowAttributes.VOICE_PROVIDER){
|
|
69
74
|
this.voiceProvider = flowAttributes.VOICE_PROVIDER
|
|
70
|
-
|
|
71
75
|
}
|
|
76
|
+
vxmlAttributes.VOICE_PROVIDER = this.voiceProvider;
|
|
72
77
|
|
|
73
78
|
// IF VOICE_PROVIDER is TWILIO --> default values is on user account twilio settings
|
|
74
79
|
// IF VOICE_PROVIDER is OPENAI --> set default values from constants
|
|
@@ -84,13 +89,14 @@ class TiledeskTwilioTranslator {
|
|
|
84
89
|
vxmlAttributes.TTS_VOICE_NAME = flowAttributes.TTS_VOICE_NAME? flowAttributes.TTS_VOICE_NAME : ELEVENLABS_SETTINGS.TTS_VOICE_NAME;
|
|
85
90
|
vxmlAttributes.TTS_MODEL = flowAttributes.TTS_MODEL? flowAttributes.TTS_MODEL : ELEVENLABS_SETTINGS.TTS_MODEL;
|
|
86
91
|
vxmlAttributes.TTS_VOICE_LANGUAGE = flowAttributes.TTS_VOICE_LANGUAGE? flowAttributes.TTS_VOICE_LANGUAGE : ELEVENLABS_SETTINGS.TTS_VOICE_LANGUAGE;
|
|
92
|
+
vxmlAttributes.STT_MODEL = flowAttributes.STT_MODEL? flowAttributes.STT_MODEL : ELEVENLABS_SETTINGS.STT_MODEL;
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
97
|
winston.debug("[TiledeskVXMLTranslator] manageVoiceAttributes: vxmlAttributes returned:", vxmlAttributes);
|
|
98
|
+
voiceEventEmitter.emit('saveSettings', vxmlAttributes);
|
|
99
|
+
|
|
94
100
|
return vxmlAttributes
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -288,6 +294,9 @@ class TiledeskTwilioTranslator {
|
|
|
288
294
|
|
|
289
295
|
async delayVXMLConverter(rootEle, message, xmlAttributes){
|
|
290
296
|
const command = message.attributes.commands[0]
|
|
297
|
+
|
|
298
|
+
const prompt = this.promptVXML(rootEle, message, xmlAttributes);
|
|
299
|
+
|
|
291
300
|
rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/nextblock/' + xmlAttributes.callSid).up()
|
|
292
301
|
|
|
293
302
|
return rootEle.end({ pretty: true });
|
|
@@ -296,7 +305,7 @@ class TiledeskTwilioTranslator {
|
|
|
296
305
|
|
|
297
306
|
async playPromptVXMLConverter(rootEle, message, xmlAttributes){
|
|
298
307
|
|
|
299
|
-
const prompt = this.promptVXML(rootEle, message, xmlAttributes);
|
|
308
|
+
const prompt = await this.promptVXML(rootEle, message, xmlAttributes);
|
|
300
309
|
|
|
301
310
|
const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + '&previousIntentTimestamp='+Date.now();
|
|
302
311
|
rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/nextblock/' + xmlAttributes.callSid + queryUrl).up()
|
|
@@ -314,7 +323,8 @@ class TiledeskTwilioTranslator {
|
|
|
314
323
|
const gather = rootEle.ele("Gather", { input: "speech"})
|
|
315
324
|
|
|
316
325
|
const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
|
|
317
|
-
gather.att("action", this.BASE_URL + '/
|
|
326
|
+
gather.att("action", this.BASE_URL + '/nextBlock/' + xmlAttributes.callSid + queryUrl)
|
|
327
|
+
// gather.att("action", this.BASE_URL + '/speechresult/' + xmlAttributes.callSid + queryUrl)
|
|
318
328
|
.att("method", "POST")
|
|
319
329
|
.att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
|
|
320
330
|
.att('speechTimeout', "auto")
|
|
@@ -346,15 +356,16 @@ class TiledeskTwilioTranslator {
|
|
|
346
356
|
}
|
|
347
357
|
|
|
348
358
|
record
|
|
349
|
-
|
|
359
|
+
.att("action", this.BASE_URL + '/record/action/' + xmlAttributes.callSid + queryUrl)
|
|
350
360
|
.att("method", "POST")
|
|
351
361
|
.att("trim", "trim-silence")
|
|
352
|
-
.att("
|
|
362
|
+
.att("timeout", "2")
|
|
363
|
+
.att("recordingStatusCallback", this.BASE_URL + '/record/callback/' + xmlAttributes.callSid + queryUrl)
|
|
353
364
|
.att("recordingStatusCallbackMethod", "POST")
|
|
354
365
|
|
|
355
|
-
if(xmlAttributes && xmlAttributes.noInputTimeout){
|
|
356
|
-
|
|
357
|
-
}
|
|
366
|
+
// if(xmlAttributes && xmlAttributes.noInputTimeout){
|
|
367
|
+
// record.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
|
|
368
|
+
// }
|
|
358
369
|
|
|
359
370
|
}
|
|
360
371
|
|
|
@@ -373,7 +384,7 @@ class TiledeskTwilioTranslator {
|
|
|
373
384
|
let queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + '&previousIntentTimestamp='+Date.now() + '&menu_options=' + menu_options;
|
|
374
385
|
const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
|
|
375
386
|
if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoMatch){
|
|
376
|
-
queryUrl += '&
|
|
387
|
+
queryUrl += '&'+ handleNoInputNoMatchQuery.queryNoMatch
|
|
377
388
|
}
|
|
378
389
|
|
|
379
390
|
const gather = rootEle.ele("Gather", { input: "dtmf"})
|
|
@@ -477,7 +488,7 @@ class TiledeskTwilioTranslator {
|
|
|
477
488
|
}
|
|
478
489
|
|
|
479
490
|
|
|
480
|
-
queryNoInput = '
|
|
491
|
+
queryNoInput = 'button_action='+button_noIput.action.substring(1);
|
|
481
492
|
//rootEle.ele("Redirect", {}, this.BASE_URL + '/handle/' + attributes.callSid + '/no_input'+ queryNoInput)
|
|
482
493
|
|
|
483
494
|
|
|
@@ -504,7 +515,7 @@ class TiledeskTwilioTranslator {
|
|
|
504
515
|
value: 'no_match'
|
|
505
516
|
}
|
|
506
517
|
|
|
507
|
-
queryNoMatch = '
|
|
518
|
+
queryNoMatch = 'button_action='+button_noMatch.action.substring(1); //remove '#' from intentId because is not a valid char for XML lang
|
|
508
519
|
//rootEle.ele("Redirect", {}, this.BASE_URL + '/handle/' + attributes.callSid + '/no_match'+ queryNoMatch)
|
|
509
520
|
|
|
510
521
|
/*element.ele("nomatch")
|
|
@@ -629,29 +640,45 @@ class TiledeskTwilioTranslator {
|
|
|
629
640
|
}
|
|
630
641
|
|
|
631
642
|
async generateTTS(text, attributes){
|
|
632
|
-
|
|
633
643
|
let audioData = null;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
644
|
+
try {
|
|
645
|
+
switch(this.voiceProvider){
|
|
646
|
+
case VOICE_PROVIDER.OPENAI:
|
|
647
|
+
let GPT_KEY = this.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
|
|
648
|
+
audioData = await this.aiService.textToSpeech(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, GPT_KEY)
|
|
649
|
+
break;
|
|
650
|
+
case VOICE_PROVIDER.ELEVENLABS:
|
|
651
|
+
let ELEVENLABS_APIKEY = this.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
|
|
652
|
+
audioData = await this.aiService.textToSpeechElevenLabs(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, attributes.TTS_VOICE_LANGUAGE, ELEVENLABS_APIKEY)
|
|
653
|
+
break;
|
|
654
|
+
default:
|
|
655
|
+
throw new SttError('TTS_FAILED', 'Unsupported voice provider: ' + this.voiceProvider);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!audioData) {
|
|
659
|
+
throw new SttError('TTS_FAILED', 'TTS returned no audio data');
|
|
660
|
+
}
|
|
647
661
|
|
|
662
|
+
let fileUrl = await this.uploadService.upload(attributes.callSid, audioData, this.user)
|
|
663
|
+
winston.debug('(voice) Audio Message url captured after TTS -->', fileUrl)
|
|
664
|
+
return fileUrl
|
|
665
|
+
} catch (error) {
|
|
666
|
+
winston.error('(voice) TTS generation error:', error);
|
|
667
|
+
switch (error.code) {
|
|
668
|
+
case 'TTS_FAILED':
|
|
669
|
+
winston.error('(voice) TTS_FAILED:', error.message);
|
|
670
|
+
break;
|
|
671
|
+
case 'AI_SERVICE_ERROR':
|
|
672
|
+
winston.error('(voice) AI_SERVICE_ERROR:', error.message);
|
|
673
|
+
break;
|
|
674
|
+
case 'UPLOAD_SERVICE_ERROR':
|
|
675
|
+
winston.error('(voice) UPLOAD_SERVICE_ERROR:', error.message);
|
|
676
|
+
break;
|
|
677
|
+
default:
|
|
678
|
+
throw new SttError('TTS_FAILED', 'TTS generation failed: ' + error.message);
|
|
679
|
+
}
|
|
648
680
|
}
|
|
649
681
|
|
|
650
|
-
let fileUrl = await this.uploadService.upload(attributes.callSid, audioData, this.user).catch((err)=>{
|
|
651
|
-
console.log('errr while uploading audioData', err.response)
|
|
652
|
-
})
|
|
653
|
-
console.log('(voice) Audio Message url captured after TTS -->', fileUrl)
|
|
654
|
-
return fileUrl
|
|
655
682
|
}
|
|
656
683
|
|
|
657
684
|
|
package/tiledesk/VoiceChannel.js
CHANGED
|
@@ -4,17 +4,10 @@ const jwt = require("jsonwebtoken");
|
|
|
4
4
|
const { v4: uuidv4 } = require("uuid");
|
|
5
5
|
const { promisify } = require('util');
|
|
6
6
|
|
|
7
|
-
/*UTILS*/
|
|
8
|
-
const utils = require('./utils-message.js')
|
|
9
|
-
const TYPE_MESSAGE = require('./constants').TYPE_MESSAGE
|
|
10
|
-
const MESSAGE_TYPE_MINE = require('./constants').MESSAGE_TYPE_MINE
|
|
11
|
-
const MESSAGE_TYPE_OTHERS = require('./constants').MESSAGE_TYPE_OTHERS
|
|
12
|
-
const CHANNEL_NAME = require('./constants').CHANNEL_NAME
|
|
13
|
-
const VOICE_PROVIDER = require('./constants').VOICE_PROVIDER;
|
|
14
|
-
const OPENAI_SETTINGS = require('./constants').OPENAI_SETTINGS;
|
|
15
|
-
|
|
16
7
|
const winston = require("../winston");
|
|
17
8
|
|
|
9
|
+
const voiceEventEmitter = require('./services/voiceEventEmitter');
|
|
10
|
+
|
|
18
11
|
class VoiceChannel {
|
|
19
12
|
|
|
20
13
|
|
|
@@ -48,8 +41,21 @@ class VoiceChannel {
|
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
this.redis_client = config.redis_client
|
|
44
|
+
|
|
45
|
+
this.listenToVoiceEvents();
|
|
51
46
|
|
|
52
47
|
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
listenToVoiceEvents(){
|
|
51
|
+
|
|
52
|
+
voiceEventEmitter.on('saveSettings', async (data) => {
|
|
53
|
+
winston.debug('[VoiceChannel] listenToVoiceEvents: saveSettings event received -->', data)
|
|
54
|
+
if(data){
|
|
55
|
+
await this.saveSettingsForCallId(data, data.callSid);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
async getNextDelayTimeForCallId(callId){
|
|
@@ -69,11 +75,11 @@ class VoiceChannel {
|
|
|
69
75
|
//increment
|
|
70
76
|
const delayIndex = (+index) +1
|
|
71
77
|
//save new index to redis
|
|
72
|
-
await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', delayIndex,
|
|
78
|
+
await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', delayIndex, 'EX', 86400);
|
|
73
79
|
return;
|
|
74
80
|
}
|
|
75
81
|
//if index is not present: set to default (0)
|
|
76
|
-
await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', 0,
|
|
82
|
+
await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', 0, 'EX', 86400);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/** RESET INDEX INTO REDIS DATA FOR CURRENT CALLID **/
|
|
@@ -94,50 +100,49 @@ class VoiceChannel {
|
|
|
94
100
|
|
|
95
101
|
async saveSettingsForCallId(attributes, callId){
|
|
96
102
|
|
|
97
|
-
winston.debug('saveSettingsForCallId: attributes -->', attributes)
|
|
98
|
-
let flowAttributes = {}
|
|
99
|
-
if(attributes && attributes.flowAttributes){
|
|
100
|
-
|
|
101
|
-
flowAttributes = attributes.flowAttributes;
|
|
102
|
-
|
|
103
|
-
//MANAGE VOICE SETTINGS from globals attributes
|
|
104
|
-
let voiceProvider = VOICE_PROVIDER.TWILIO
|
|
105
|
-
if(flowAttributes.VOICE_PROVIDER){
|
|
106
|
-
voiceProvider = flowAttributes.VOICE_PROVIDER
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// IF VOICE_PROVIDER is TWILIO --> default values is on user account twilio settings
|
|
111
|
-
// IF VOICE_PROVIDER is OPENAI --> set default values from constants
|
|
112
|
-
if(voiceProvider === VOICE_PROVIDER.OPENAI){
|
|
113
|
-
flowAttributes.TTS_VOICE_NAME = flowAttributes.TTS_VOICE_NAME? flowAttributes.TTS_VOICE_NAME : OPENAI_SETTINGS.TTS_VOICE_NAME;
|
|
114
|
-
flowAttributes.TTS_MODEL = flowAttributes.TTS_MODEL? flowAttributes.TTS_MODEL : OPENAI_SETTINGS.TTS_MODEL;
|
|
115
|
-
flowAttributes.STT_MODEL = flowAttributes.STT_MODEL? flowAttributes.STT_MODEL : OPENAI_SETTINGS.STT_MODEL;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
103
|
+
winston.debug('[VoiceChannel] saveSettingsForCallId: attributes -->', attributes)
|
|
120
104
|
|
|
121
105
|
const index = await this.redis_client.get('tiledesk:voice:'+callId + ':attributes');
|
|
122
|
-
winston.debug('saveSettingsForCallId: attributes found -->'+index)
|
|
106
|
+
winston.debug('[VoiceChannel] saveSettingsForCallId: attributes found -->'+index)
|
|
123
107
|
if(index){
|
|
124
108
|
//set index to default (0)
|
|
125
|
-
await this.redis_client.set('tiledesk:voice:'+callId + ':attributes', JSON.stringify(
|
|
109
|
+
await this.redis_client.set('tiledesk:voice:'+callId + ':attributes', JSON.stringify(attributes), 'EX', 86400);
|
|
126
110
|
return;
|
|
127
111
|
}
|
|
128
112
|
//if index is not present: set to default (0)
|
|
129
|
-
await this.redis_client.set('tiledesk:voice:'+callId + ':attributes', JSON.stringify(
|
|
113
|
+
await this.redis_client.set('tiledesk:voice:'+callId + ':attributes', JSON.stringify(attributes), 'EX', 86400);
|
|
130
114
|
|
|
131
115
|
}
|
|
132
116
|
|
|
133
117
|
|
|
134
118
|
async getSettingsForCallId(callId){
|
|
135
119
|
const attributes = await this.redis_client.get('tiledesk:voice:'+callId + ':attributes');
|
|
120
|
+
winston.debug('[VoiceChannel] getSettingsForCallId: attributes found -->', attributes, callId)
|
|
136
121
|
if(attributes){
|
|
137
122
|
return JSON.parse(attributes)
|
|
138
123
|
}
|
|
139
124
|
return {};
|
|
140
125
|
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async deleteCallKeys(callSid) {
|
|
129
|
+
const pattern = `tiledesk:voice:${callSid}:*`;
|
|
130
|
+
let cursor = 0;
|
|
131
|
+
|
|
132
|
+
do {
|
|
133
|
+
const reply = await this.redis_client.scan(cursor, {
|
|
134
|
+
MATCH: pattern,
|
|
135
|
+
COUNT: 100
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
cursor = reply.cursor;
|
|
139
|
+
const keys = reply.keys;
|
|
140
|
+
|
|
141
|
+
if (keys.length > 0) {
|
|
142
|
+
await this.redis_client.del(keys);
|
|
143
|
+
}
|
|
144
|
+
} while (cursor !== 0);
|
|
145
|
+
}
|
|
141
146
|
|
|
142
147
|
|
|
143
148
|
|
package/tiledesk/constants.js
CHANGED
|
@@ -62,6 +62,17 @@ module.exports = {
|
|
|
62
62
|
ELEVENLABS_SETTINGS:{
|
|
63
63
|
TTS_VOICE_NAME: '21m00Tcm4TlvDq8ikWAM',
|
|
64
64
|
TTS_MODEL: 'eleven_multilingual_v2',
|
|
65
|
-
TTS_VOICE_LANGUAGE: 'en'
|
|
66
|
-
|
|
65
|
+
TTS_VOICE_LANGUAGE: 'en',
|
|
66
|
+
STT_MODEL: 'scribe_v1'
|
|
67
|
+
},
|
|
68
|
+
NON_SPEECH_TOKENS: [
|
|
69
|
+
'(music)',
|
|
70
|
+
'(noise)',
|
|
71
|
+
'(silence)',
|
|
72
|
+
'(background noise)',
|
|
73
|
+
'(applause)',
|
|
74
|
+
'(breathing)',
|
|
75
|
+
'(laughs)',
|
|
76
|
+
'(laughter)'
|
|
77
|
+
]
|
|
67
78
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class SttError extends Error {
|
|
2
|
+
constructor(code, message, extra = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.code = code; // es. 'AUDIO_DOWNLOAD_FAILED'
|
|
5
|
+
this.extra = extra; // opzionale, eventuali dati aggiuntivi
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class RedisError extends Error {
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class ServiceError extends Error {
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// esporta tutte insieme
|
|
24
|
+
module.exports = {
|
|
25
|
+
SttError,
|
|
26
|
+
RedisError,
|
|
27
|
+
ServiceError
|
|
28
|
+
};
|
|
@@ -2,6 +2,9 @@ var winston = require('../../winston');
|
|
|
2
2
|
const axios = require("axios").default;
|
|
3
3
|
const FormData = require('form-data');
|
|
4
4
|
|
|
5
|
+
/*ERROR HANDLER*/
|
|
6
|
+
const { ServiceError } = require('../errors');
|
|
7
|
+
|
|
5
8
|
/*UTILS*/
|
|
6
9
|
const fileUtils = require('../fileUtils.js')
|
|
7
10
|
|
|
@@ -30,44 +33,52 @@ class AiService {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
async speechToText(fileUrl, model, GPT_KEY) {
|
|
33
|
-
|
|
36
|
+
let start_time = new Date();
|
|
34
37
|
winston.debug("[AiService] speechToText url: "+ fileUrl);
|
|
35
|
-
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
36
|
-
winston.error("[AiService] err while downloadFromUrl: ", err)
|
|
37
|
-
return null; // fallback per evitare undefined
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
if (!file) {
|
|
41
|
-
winston.error('file non esisteeeeeeee')
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
38
|
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
|
|
39
|
+
return new Promise(async (resolve, reject) => {
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
try {
|
|
42
|
+
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
43
|
+
winston.error("[AiService] err while downloadFromUrl: ", err)
|
|
44
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl));
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!file) {
|
|
48
|
+
winston.debug('[AiService] OPENAI speechToText file NOT EXIST: . . . return')
|
|
49
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null'));
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
const formData = new FormData();
|
|
53
|
+
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
54
|
+
formData.append('model', model);
|
|
55
|
+
|
|
56
|
+
axios({
|
|
57
|
+
url: `${this.OPENAI_ENDPOINT}/audio/transcriptions`,
|
|
58
|
+
headers: {
|
|
59
|
+
...formData.getHeaders(),
|
|
60
|
+
"Authorization": "Bearer " + GPT_KEY
|
|
61
|
+
},
|
|
62
|
+
data: formData,
|
|
63
|
+
method: 'POST'
|
|
64
|
+
}).then((resbody) => {
|
|
65
|
+
resolve(resbody.data.text);
|
|
66
|
+
let end_time = new Date();
|
|
67
|
+
winston.verbose(`-----> [AiService] OpenAI speechToText time elapsed: ${end_time - start_time} ms`);
|
|
68
|
+
}).catch((err) => {
|
|
69
|
+
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI /audio/transcriptions API failed with err:', err));
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
winston.error("[AiService] OpenAI STT error", err.message);
|
|
73
|
+
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI STT service failed with err:', err));
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
|
|
66
77
|
})
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
async textToSpeech(text, name, model, GPT_KEY){
|
|
70
|
-
|
|
81
|
+
let start_time = new Date();
|
|
71
82
|
winston.debug('[AiService] textToSpeech text:'+ text)
|
|
72
83
|
|
|
73
84
|
const data = {
|
|
@@ -76,7 +87,6 @@ class AiService {
|
|
|
76
87
|
voice: name,
|
|
77
88
|
};
|
|
78
89
|
|
|
79
|
-
|
|
80
90
|
winston.debug('[AiService] textToSpeech config:', data)
|
|
81
91
|
|
|
82
92
|
return new Promise((resolve, reject) => {
|
|
@@ -91,11 +101,13 @@ class AiService {
|
|
|
91
101
|
method: "POST",
|
|
92
102
|
}).then( async (response) => {
|
|
93
103
|
//console.log('[AiService] textToSpeech result', response?.data)
|
|
94
|
-
resolve(response?.data)
|
|
104
|
+
resolve(response?.data)
|
|
105
|
+
let end_time = new Date();
|
|
106
|
+
winston.verbose(`-----> [AiService] textToSpeech time elapsed: ${end_time - start_time} ms`);
|
|
95
107
|
})
|
|
96
108
|
.catch((err) => {
|
|
97
109
|
winston.error("[AiService] textToSpeech error: ", err.response?.data);
|
|
98
|
-
reject(err)
|
|
110
|
+
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI textToSpeech API failed with err:', err));
|
|
99
111
|
});
|
|
100
112
|
});
|
|
101
113
|
|
|
@@ -104,45 +116,53 @@ class AiService {
|
|
|
104
116
|
|
|
105
117
|
|
|
106
118
|
async speechToTextElevenLabs(fileUrl, model, language, API_KEY) {
|
|
107
|
-
|
|
119
|
+
let start_time = new Date();
|
|
108
120
|
winston.debug("[AiService] ELEVEN Labs speechToText url: "+ fileUrl);
|
|
109
|
-
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
110
|
-
winston.error("[AiService] err: ", err)
|
|
111
|
-
return null; // fallback per evitare undefined
|
|
112
|
-
})
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
return new Promise(async (resolve, reject) => {
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
126
|
+
winston.error("[AiService] err: ", err)
|
|
127
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl));
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (!file) {
|
|
131
|
+
winston.debug('[AiService] ELEVEN Labs speechToText file NOT EXIST: . . . return')
|
|
132
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null'));
|
|
133
|
+
}
|
|
120
134
|
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
const formData = new FormData();
|
|
137
|
+
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
138
|
+
formData.append('model_id', "scribe_v1");
|
|
139
|
+
formData.append('language_code', language)
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
axios({
|
|
142
|
+
url: `${this.ELEVENLABS_ENDPOINT}/v1/speech-to-text`,
|
|
143
|
+
headers: {
|
|
144
|
+
...formData.getHeaders(),
|
|
145
|
+
"xi-api-key": API_KEY
|
|
146
|
+
},
|
|
147
|
+
data: formData,
|
|
148
|
+
method: 'POST'
|
|
149
|
+
}).then((resbody) => {
|
|
150
|
+
resolve(resbody.data.text);
|
|
151
|
+
let end_time = new Date();
|
|
152
|
+
winston.verbose(`-----> [AiService] ELEVEN Labs speechToText time elapsed: ${end_time - start_time} ms`);
|
|
153
|
+
}).catch((err) => {
|
|
154
|
+
reject(new ServiceError('AISERVICE_FAILED', 'ElevenLabs /speech-to-text API failed with err:', err));
|
|
155
|
+
})
|
|
156
|
+
} catch (error) {
|
|
157
|
+
winston.error("[AiService] ElevenLabs STT error", err.message);
|
|
158
|
+
reject(new ServiceError('AISERVICE_FAILED', 'ElevenLabs STT service failed with err:', err));
|
|
159
|
+
}
|
|
140
160
|
|
|
141
161
|
})
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
async textToSpeechElevenLabs(text, voice_id, model, language_code, API_KEY){
|
|
145
|
-
|
|
165
|
+
let start_time = new Date();
|
|
146
166
|
const data = {
|
|
147
167
|
model_id: model,
|
|
148
168
|
text: text,
|
|
@@ -161,11 +181,13 @@ class AiService {
|
|
|
161
181
|
data: data,
|
|
162
182
|
method: "POST",
|
|
163
183
|
}).then( async (response) => {
|
|
164
|
-
resolve(response?.data)
|
|
184
|
+
resolve(response?.data)
|
|
185
|
+
let end_time = new Date();
|
|
186
|
+
winston.verbose(`-----> [AiService] ELEVEN Labs textToSpeech time elapsed: ${end_time - start_time} ms`);
|
|
165
187
|
})
|
|
166
188
|
.catch((err) => {
|
|
167
189
|
winston.error("[AiService] ELEVEN Labs textToSpeech error: ", err);
|
|
168
|
-
reject(err)
|
|
190
|
+
reject(new ServiceError('AISERVICE_FAILED', 'ElevenLabs textToSpeech API failed with err:', err));
|
|
169
191
|
});
|
|
170
192
|
});
|
|
171
193
|
|
|
@@ -194,7 +216,7 @@ class AiService {
|
|
|
194
216
|
}
|
|
195
217
|
}).catch((err) => {
|
|
196
218
|
winston.error("[AiService] checkQuoteAvailability error: ", err.response?.data);
|
|
197
|
-
reject(err);
|
|
219
|
+
reject(new ServiceError('AISERVICE_FAILED', 'checkQuoteAvailability API failed with err:', err));
|
|
198
220
|
})
|
|
199
221
|
|
|
200
222
|
})
|
|
@@ -7,6 +7,9 @@ const path = require('path');
|
|
|
7
7
|
/*UTILS*/
|
|
8
8
|
const fileUtils = require('../fileUtils.js')
|
|
9
9
|
|
|
10
|
+
/*ERROR HANDLER*/
|
|
11
|
+
const { ServiceError } = require('../errors');
|
|
12
|
+
|
|
10
13
|
class UploadService {
|
|
11
14
|
|
|
12
15
|
constructor(config) {
|
|
@@ -69,7 +72,7 @@ class UploadService {
|
|
|
69
72
|
|
|
70
73
|
}).catch((err) => {
|
|
71
74
|
console.log('err', err)
|
|
72
|
-
reject(err);
|
|
75
|
+
reject(new ServiceError('UPLOADSERVICE_FAILED', 'UploadService /files/users API failed with err:', err) );
|
|
73
76
|
}).finally(() => {
|
|
74
77
|
// Sempre eseguito
|
|
75
78
|
if (fs.existsSync(tempFilePath)) {
|
package/tiledesk/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const NON_SPEECH_TOKENS = require('./constants').NON_SPEECH_TOKENS
|
|
1
2
|
|
|
2
3
|
function getNumber(phoneNumber){
|
|
3
4
|
if(phoneNumber.startsWith('+')){
|
|
@@ -12,5 +13,23 @@ function buildQueryString(query) {
|
|
|
12
13
|
return params.toString() ? `?${params.toString()}` : '';
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
function normalizeSTT(text) {
|
|
17
|
+
if (!text) return null;
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
const cleaned = text.trim().toLowerCase();
|
|
20
|
+
|
|
21
|
+
// solo token non verbali
|
|
22
|
+
if (NON_SPEECH_TOKENS.includes(cleaned)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// solo simboli o rumore
|
|
27
|
+
if (!/[a-zàèéìòù]/i.test(cleaned)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return cleaned;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
module.exports = {getNumber, buildQueryString, normalizeSTT}
|