@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 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
- let key = await integrationService.getKeyFromIntegrations(project_id, 'openai', settings.token)
169
- if (!key) {
170
- winston.debug("(voice) - Key not found in Integrations. Searching in kb settings...");
171
- key = await integrationService.getKeyFromKbSettings(project_id, settings.token);
172
- }
173
- if (!key) {
174
- winston.debug("(voice) - Retrieve public gptkey")
175
- key = GPT_KEY;
176
- publicKey = true;
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
- integrations.push({type: 'openai', key: key, publicKey: publicKey})
180
-
181
- let eleven_labs = await integrationService.getKeyFromIntegrations(project_id, 'elevenlabs', settings.token)
182
- if (eleven_labs) {
183
- winston.debug("(voice) - Key found in Integrations: "+ eleven_labs);
184
- integrations.push({type: 'elevenlabs', key: eleven_labs, publicKey: false})
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} : ${(end_call-start_call)}[ms]`)
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.verbose("(voice) VXML to SEND: "+ messageToVXML);
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.verbose("(voice) VXML to SEND: "+ messageToVXML);
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: ', new Date(), 'with text:', req.body.Digits)
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.verbose("(voice) VXML to SEND: "+ messageToVXML);
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.verbose("(voice) VXML to SEND: "+ messageToVXML);
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.verbose("(voice) VXML to SEND: "+ messageToVXML);
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 redis_client.del('tiledesk:voice:'+callSid+':session');
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 = {}, text = null;
1115
- switch(attributes.VOICE_PROVIDER){
1116
- case VOICE_PROVIDER.OPENAI:
1117
- let GPT_KEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
1118
- let publicKey = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.publicKey
1119
- if(publicKey){
1120
- let keep_going = await aiService.checkQuoteAvailability(sessionInfo.project_id, settings.token).catch((err)=>{
1121
- winston.error('errr while checkQuoteAvailability for project:', sessionInfo.project_id, err.response?.data)
1122
- })
1123
- winston.debug('(voice) checkQuoteAvailability return: '+ keep_going);
1124
- if(!keep_going){
1125
- //no token is available --> close conversation
1126
- return tiledeskMessage= {
1127
- //text:'\\close',
1128
- text:'/close',
1129
- senderFullname: sessionInfo.from,
1130
- type: 'text',
1131
- channel: { name: CHANNEL_NAME },
1132
- attributes: {
1133
- subtype: "info",
1134
- action: 'close'+JSON.stringify({event: 'quota_exceeded'}),
1135
- payload: {
1136
- catchEvent: 'quota_exceeded'
1137
- },
1138
- timestamp: 'xxxxxx'
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
- text = await aiService.speechToText(audioFileUrl, attributes.STT_MODEL, GPT_KEY).catch((err)=>{
1146
- winston.error('errr while transcript', err.response?.data)
1147
- })
1148
- tiledeskMessage= {
1149
- text: text,
1150
- senderFullname: sessionInfo.from,
1151
- type: 'text',
1152
- channel: { name: CHANNEL_NAME }
1153
- };
1154
- break;
1155
- case VOICE_PROVIDER.ELEVENLABS:
1156
- let ELEVENLABS_APIKEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
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
- break;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-voice-twilio-connector",
3
- "version": "0.1.26-rc12",
3
+ "version": "0.1.26-rc13",
4
4
  "description": "Tiledesk VOICE Twilio connector",
5
5
  "license": "MIT",
6
6
  "author": "Gabriele Panico",
@@ -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 + '/speechresult/' + xmlAttributes.callSid + queryUrl)
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
- //.att("action", this.BASE_URL + '/record/' + xmlAttributes.callSid + queryUrl)
359
+ .att("action", this.BASE_URL + '/record/action/' + xmlAttributes.callSid + queryUrl)
350
360
  .att("method", "POST")
351
361
  .att("trim", "trim-silence")
352
- .att("recordingStatusCallback", this.BASE_URL + '/record/' + xmlAttributes.callSid + queryUrl)
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
- record.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
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 += '&button_action='+ handleNoInputNoMatchQuery.queryNoMatch.split('button_action=')[1]
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 = 'intentName='+ querystring.encode(attributes.intentName) + '&previousIntentTimestamp='+Date.now() + '&button_action='+button_noIput.action.substring(1);
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 = 'intentName='+ querystring.encode(attributes.intentName) + '&previousIntentTimestamp='+Date.now() + '&button_action='+button_noMatch.action.substring(1); //remove '#' from intentId because is not a valid char for XML lang
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
- switch(this.voiceProvider){
635
- case VOICE_PROVIDER.OPENAI:
636
- let GPT_KEY = this.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
637
- audioData = await this.aiService.textToSpeech(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, GPT_KEY).catch((err)=>{
638
- console.log('errr while creating audio message', err.response?.data)
639
- })
640
- break;
641
- case VOICE_PROVIDER.ELEVENLABS:
642
- let ELEVENLABS_APIKEY = this.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
643
- audioData = await this.aiService.textToSpeechElevenLabs(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, attributes.TTS_VOICE_LANGUAGE, ELEVENLABS_APIKEY).catch((err)=>{
644
- console.log('errr while creating elevenlabs audio message', err.response?.data)
645
- })
646
- break;
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
 
@@ -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, {'EX': 86400});
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, {'EX': 86400});
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(flowAttributes), {'EX': 86400});
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(flowAttributes), {'EX': 86400});
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
 
@@ -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
- const formData = new FormData();
49
- formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
50
- formData.append('model', model);
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
- axios({
53
- url: `${this.OPENAI_ENDPOINT}/audio/transcriptions`,
54
- headers: {
55
- ...formData.getHeaders(),
56
- "Authorization": "Bearer " + GPT_KEY
57
- },
58
- data: formData,
59
- method: 'POST'
60
- }).then((resbody) => {
61
- resolve(resbody.data.text);
62
- }).catch((err) => {
63
- reject(err);
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
- if (!file) {
115
- winston.debug('[AiService] ELEVEN Labs speechToText file NOT EXIST: . . . return')
116
- return;
117
- }
118
-
119
- return new Promise((resolve, reject) => {
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
- const formData = new FormData();
123
- formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
124
- formData.append('model_id', "scribe_v1");
125
- formData.append('language_code', language)
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
- axios({
128
- url: `${this.ELEVENLABS_ENDPOINT}/v1/speech-to-text`,
129
- headers: {
130
- ...formData.getHeaders(),
131
- "xi-api-key": API_KEY
132
- },
133
- data: formData,
134
- method: 'POST'
135
- }).then((resbody) => {
136
- resolve(resbody.data.text);
137
- }).catch((err) => {
138
- reject(err);
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)) {
@@ -0,0 +1,6 @@
1
+ const EventEmitter = require('events');
2
+ class VoiceEventEmitter extends EventEmitter {}
3
+
4
+ const voiceEventEmitter = new VoiceEventEmitter();
5
+
6
+ module.exports = voiceEventEmitter;
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
- module.exports = {getNumber, buildQueryString}
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}