@tiledesk/tiledesk-voice-twilio-connector 0.1.22 → 0.1.24
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 +98 -28
- package/logs/app.log +2950 -0
- package/package.json +3 -2
- package/tiledesk/TiledeskTwilioTranslator.js +20 -7
- package/tiledesk/VoiceChannel.js +52 -1
- package/tiledesk/constants.js +4 -3
- package/tiledesk/fileUtils.js +3 -2
- package/tiledesk/services/AiService.js +93 -5
- package/tiledesk/services/IntegrationService.js +3 -4
- package/tiledesk/services/UploadService.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiledesk/tiledesk-voice-twilio-connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "Tiledesk VOICE Twilio connector",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Gabriele Panico",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"redlock": "^4.2.0",
|
|
39
39
|
"twilio": "^5.2.1",
|
|
40
40
|
"uuid": "^8.3.2",
|
|
41
|
-
"winston": "^3.3.3"
|
|
41
|
+
"winston": "^3.3.3",
|
|
42
|
+
"xmlbuilder": "^15.1.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"mocha": "^8.2.1"
|
|
@@ -102,7 +102,7 @@ class TiledeskTwilioTranslator {
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
this.user = sessionInfo.user
|
|
105
|
-
this.
|
|
105
|
+
this.integrations = sessionInfo.integrations
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
const xml = xmlbuilder.create("Response", {});
|
|
@@ -310,7 +310,7 @@ class TiledeskTwilioTranslator {
|
|
|
310
310
|
gather.att("action", this.BASE_URL + '/speechresult/' + xmlAttributes.callSid + queryUrl)
|
|
311
311
|
.att("method", "POST")
|
|
312
312
|
.att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
|
|
313
|
-
.att('speechTimeout', "
|
|
313
|
+
.att('speechTimeout', "auto")
|
|
314
314
|
|
|
315
315
|
//if(xmlAttributes && xmlAttributes.noInputTimeout){
|
|
316
316
|
// gather.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
|
|
@@ -534,7 +534,7 @@ class TiledeskTwilioTranslator {
|
|
|
534
534
|
if (command.type === "message") {
|
|
535
535
|
//case type: TEXT
|
|
536
536
|
if(command.message.type === 'text'){
|
|
537
|
-
if(that.voiceProvider
|
|
537
|
+
if(that.voiceProvider !== VOICE_PROVIDER.TWILIO){
|
|
538
538
|
let voiceMessageUrl = await that.generateTTS(command.message.text, attributes)
|
|
539
539
|
rootEle.ele('Play', {}, voiceMessageUrl )
|
|
540
540
|
}else{
|
|
@@ -622,11 +622,24 @@ class TiledeskTwilioTranslator {
|
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
async generateTTS(text, attributes){
|
|
625
|
-
let GPT_KEY = this.voiceSettings?.key
|
|
626
625
|
|
|
627
|
-
let audioData =
|
|
628
|
-
|
|
629
|
-
|
|
626
|
+
let audioData = null;
|
|
627
|
+
switch(this.voiceProvider){
|
|
628
|
+
case VOICE_PROVIDER.OPENAI:
|
|
629
|
+
let GPT_KEY = this.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
|
|
630
|
+
audioData = await this.aiService.textToSpeech(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, GPT_KEY).catch((err)=>{
|
|
631
|
+
console.log('errr while creating audio message', err.response?.data)
|
|
632
|
+
})
|
|
633
|
+
break;
|
|
634
|
+
case VOICE_PROVIDER.ELEVENLABS:
|
|
635
|
+
let ELEVENLABS_APIKEY = this.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
|
|
636
|
+
audioData = await this.aiService.textToSpeechElevenLabs(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, ELEVENLABS_APIKEY).catch((err)=>{
|
|
637
|
+
console.log('errr while creating elevenlabs audio message', err.response?.data)
|
|
638
|
+
})
|
|
639
|
+
break;
|
|
640
|
+
|
|
641
|
+
}
|
|
642
|
+
|
|
630
643
|
let fileUrl = await this.uploadService.upload(attributes.callSid, audioData, this.user).catch((err)=>{
|
|
631
644
|
console.log('errr while uploading audioData', err.response)
|
|
632
645
|
})
|
package/tiledesk/VoiceChannel.js
CHANGED
|
@@ -10,7 +10,8 @@ const TYPE_MESSAGE = require('./constants').TYPE_MESSAGE
|
|
|
10
10
|
const MESSAGE_TYPE_MINE = require('./constants').MESSAGE_TYPE_MINE
|
|
11
11
|
const MESSAGE_TYPE_OTHERS = require('./constants').MESSAGE_TYPE_OTHERS
|
|
12
12
|
const CHANNEL_NAME = require('./constants').CHANNEL_NAME
|
|
13
|
-
|
|
13
|
+
const VOICE_PROVIDER = require('./constants').VOICE_PROVIDER;
|
|
14
|
+
const OPENAI_SETTINGS = require('./constants').OPENAI_SETTINGS;
|
|
14
15
|
|
|
15
16
|
const winston = require("../winston");
|
|
16
17
|
|
|
@@ -89,6 +90,56 @@ class VoiceChannel {
|
|
|
89
90
|
//if index is not present: set to default (0)
|
|
90
91
|
await this.redis_client.set('tiledesk:vxml:'+callId + ':delayIndex', 0, {'EX': 86400});
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async saveSettingsForCallId(attributes, callId){
|
|
96
|
+
|
|
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
|
+
}
|
|
120
|
+
|
|
121
|
+
const index = await this.redis_client.get('tiledesk:vxml:'+callId + ':attributes');
|
|
122
|
+
winston.debug('saveSettingsForCallId: attributes found -->'+index)
|
|
123
|
+
if(index){
|
|
124
|
+
//set index to default (0)
|
|
125
|
+
await this.redis_client.set('tiledesk:vxml:'+callId + ':attributes', JSON.stringify(flowAttributes), {'EX': 86400});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
//if index is not present: set to default (0)
|
|
129
|
+
await this.redis_client.set('tiledesk:vxml:'+callId + ':attributes', JSON.stringify(flowAttributes), {'EX': 86400});
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async getSettingsForCallId(callId){
|
|
135
|
+
const attributes = await this.redis_client.get('tiledesk:vxml:'+callId + ':attributes');
|
|
136
|
+
if(attributes){
|
|
137
|
+
return JSON.parse(attributes)
|
|
138
|
+
}
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
92
143
|
|
|
93
144
|
|
|
94
145
|
|
package/tiledesk/constants.js
CHANGED
|
@@ -38,8 +38,8 @@ module.exports = {
|
|
|
38
38
|
SPEECH_FORM: 'speech_form',
|
|
39
39
|
|
|
40
40
|
},
|
|
41
|
-
BASE_POOLING_DELAY:
|
|
42
|
-
MAX_POLLING_TIME:
|
|
41
|
+
BASE_POOLING_DELAY: 500,
|
|
42
|
+
MAX_POLLING_TIME: 50000,
|
|
43
43
|
VOICE_NAME: 'Polly.Danielle',
|
|
44
44
|
VOICE_LANGUAGE: 'en-US',
|
|
45
45
|
CALL_STATUS: {
|
|
@@ -51,7 +51,8 @@ module.exports = {
|
|
|
51
51
|
},
|
|
52
52
|
VOICE_PROVIDER: {
|
|
53
53
|
OPENAI: 'openai',
|
|
54
|
-
TWILIO: 'twilio'
|
|
54
|
+
TWILIO: 'twilio',
|
|
55
|
+
ELEVENLABS: 'elevenlabs'
|
|
55
56
|
},
|
|
56
57
|
OPENAI_SETTINGS:{
|
|
57
58
|
TTS_VOICE_NAME: 'alloy',
|
package/tiledesk/fileUtils.js
CHANGED
|
@@ -39,8 +39,9 @@ class FileUtils {
|
|
|
39
39
|
responseType: 'arraybuffer',
|
|
40
40
|
method: 'GET'
|
|
41
41
|
}).then((resbody) => {
|
|
42
|
-
|
|
43
|
-
resolve(
|
|
42
|
+
const buffer = Buffer.from(resbody.data, 'binary');
|
|
43
|
+
resolve(buffer);
|
|
44
|
+
//resolve(resbody.data);
|
|
44
45
|
}).catch((err) => {
|
|
45
46
|
reject(err);
|
|
46
47
|
})
|
|
@@ -15,11 +15,16 @@ class AiService {
|
|
|
15
15
|
if (!config.OPENAI_ENDPOINT) {
|
|
16
16
|
throw new Error("[AiService] config.OPENAI_ENDPOINT is mandatory");
|
|
17
17
|
}
|
|
18
|
+
if(!config.ELEVENLABS_ENDPOINT){
|
|
19
|
+
throw new Error("[AiService] config.ELEVENLABS_ENDPOINT is mandatory");
|
|
20
|
+
}
|
|
18
21
|
if (!config.API_URL) {
|
|
19
22
|
throw new Error("[AiService] config.API_URL is mandatory");
|
|
20
23
|
}
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
this.OPENAI_ENDPOINT = config.OPENAI_ENDPOINT;
|
|
27
|
+
this.ELEVENLABS_ENDPOINT = config.ELEVENLABS_ENDPOINT;
|
|
23
28
|
this.API_URL = config.API_URL;
|
|
24
29
|
|
|
25
30
|
}
|
|
@@ -28,15 +33,20 @@ class AiService {
|
|
|
28
33
|
|
|
29
34
|
winston.debug("[AiService] speechToText url: "+ fileUrl);
|
|
30
35
|
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
31
|
-
winston.error("[AiService] err: ", err)
|
|
36
|
+
winston.error("[AiService] err while downloadFromUrl: ", err)
|
|
32
37
|
return null; // fallback per evitare undefined
|
|
33
38
|
})
|
|
34
39
|
|
|
40
|
+
if (!file) {
|
|
41
|
+
winston.error('file non esisteeeeeeee')
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
35
44
|
|
|
36
45
|
return new Promise((resolve, reject) => {
|
|
37
|
-
|
|
46
|
+
|
|
47
|
+
|
|
38
48
|
const formData = new FormData();
|
|
39
|
-
formData.append('file', file, { filename: 'audiofile', contentType: 'audio/
|
|
49
|
+
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
40
50
|
formData.append('model', model);
|
|
41
51
|
|
|
42
52
|
axios({
|
|
@@ -91,10 +101,87 @@ class AiService {
|
|
|
91
101
|
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async speechToTextElevenLabs(fileUrl, model, language, API_KEY) {
|
|
107
|
+
|
|
108
|
+
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
|
+
|
|
114
|
+
if (!file) {
|
|
115
|
+
winston.debug('[AiService] ELEVEN Labs speechToText file NOT EXIST: . . . return')
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
|
|
121
|
+
|
|
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)
|
|
126
|
+
|
|
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
|
+
console.log('dataaaaaa', resbody)
|
|
137
|
+
resolve(resbody.data.text);
|
|
138
|
+
}).catch((err) => {
|
|
139
|
+
console.log('errrrrrr', err?.response)
|
|
140
|
+
reject(err);
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async textToSpeechElevenLabs(text, voice_id, model, API_KEY){
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
const data = {
|
|
150
|
+
model_id: model,
|
|
151
|
+
text: text,
|
|
152
|
+
output_format: "mp3_44100_128",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
winston.debug('[AiService] ELEVEN Labs textToSpeech config:', data)
|
|
157
|
+
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
axios({
|
|
160
|
+
url: this.ELEVENLABS_ENDPOINT + "/v1/text-to-speech/"+ voice_id,
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
"xi-api-key": API_KEY
|
|
164
|
+
},
|
|
165
|
+
responseType: 'arraybuffer',
|
|
166
|
+
data: data,
|
|
167
|
+
method: "POST",
|
|
168
|
+
}).then( async (response) => {
|
|
169
|
+
resolve(response?.data)
|
|
170
|
+
})
|
|
171
|
+
.catch((err) => {
|
|
172
|
+
winston.error("[AiService] ELEVEN Labs textToSpeech error: ", err);
|
|
173
|
+
reject(err)
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
94
181
|
async checkQuoteAvailability(projectId, token) {
|
|
95
182
|
|
|
96
183
|
winston.debug("[AiService] checkQuoteAvailability for project: "+ projectId);
|
|
97
|
-
|
|
184
|
+
|
|
98
185
|
return new Promise((resolve, reject) => {
|
|
99
186
|
|
|
100
187
|
axios({
|
|
@@ -105,12 +192,13 @@ class AiService {
|
|
|
105
192
|
},
|
|
106
193
|
method: 'GET'
|
|
107
194
|
}).then((resbody) => {
|
|
108
|
-
if (resbody && resbody.isAvailable === true) {
|
|
195
|
+
if (resbody && resbody.data?.isAvailable === true) {
|
|
109
196
|
resolve(true)
|
|
110
197
|
} else {
|
|
111
198
|
resolve(false)
|
|
112
199
|
}
|
|
113
200
|
}).catch((err) => {
|
|
201
|
+
winston.error("[AiService] checkQuoteAvailability error: ", err.response?.data);
|
|
114
202
|
reject(err);
|
|
115
203
|
})
|
|
116
204
|
|
|
@@ -24,7 +24,7 @@ class IntegrationService {
|
|
|
24
24
|
|
|
25
25
|
async getKeyFromIntegrations(id_project, integration_name, token){
|
|
26
26
|
|
|
27
|
-
winston.debug('[IntegrationService] getKeyFromIntegrations id_project:'
|
|
27
|
+
winston.debug('[IntegrationService] getKeyFromIntegrations id_project:'+ id_project + ' ' + integration_name)
|
|
28
28
|
|
|
29
29
|
return await axios({
|
|
30
30
|
url: this.API_URL + "/"+ id_project + "/integration/name/" + integration_name,
|
|
@@ -35,10 +35,10 @@ class IntegrationService {
|
|
|
35
35
|
data: {},
|
|
36
36
|
method: "GET",
|
|
37
37
|
}).then( async (response) => {
|
|
38
|
-
if (!response.data || response.data?.value) {
|
|
38
|
+
if (!response.data || !response.data?.value) {
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
return response.data?.value?.apikey
|
|
43
43
|
})
|
|
44
44
|
.catch((err) => {
|
|
@@ -69,7 +69,6 @@ class IntegrationService {
|
|
|
69
69
|
return response.data?.gptkey
|
|
70
70
|
})
|
|
71
71
|
.catch((err) => {
|
|
72
|
-
winston.error("[IntegrationService] getKeyFromKbSettings error: ", err.response?.data);
|
|
73
72
|
return null;
|
|
74
73
|
});
|
|
75
74
|
|
|
@@ -47,7 +47,8 @@ class UploadService {
|
|
|
47
47
|
|
|
48
48
|
//const formData = new FormData();
|
|
49
49
|
//formData.append('file', file, { filename: 'audiofile_'+user._id+'_'+id+'.mp3', contentType: 'audio/mpeg' });
|
|
50
|
-
|
|
50
|
+
user.token = 'JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NWM1ZjExNjlmYWYyZDA0Y2Q3ZGE1MjciLCJlbWFpbCI6ImdhYnJpZWxlQHRpbGVkZXNrLmNvbSIsImZpcnN0bmFtZSI6IkdhYnJpZWxlIiwibGFzdG5hbWUiOiJQYW5pY28iLCJlbWFpbHZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3NDgyNTY2MTUsImF1ZCI6Imh0dHBzOi8vdGlsZWRlc2suY29tIiwiaXNzIjoiaHR0cHM6Ly90aWxlZGVzay5jb20iLCJzdWIiOiJ1c2VyIiwianRpIjoiNWUyZDhhYmUtYzQ0YS00MjJiLWE3MjUtYWYwMjcxNDgyZTczIn0.AcT1tNbE3AcfctJXfOsfUbytRNUQlhBqPUctxzXMjehZOS2ORJThWaPqPxrvqTTIyeOU2l6eoTw8_tqfRJGlp6X4m9KLio87axGl1z3WYBgh8bSMIkAw2zSIUuJmpjBuT8EZdjXZClXRUAliAvAoFRgCmhWJ1tODVvBynLiSb37sB_zscqWH5L5eF1vdt6HHizEO4HbGABQS00I2hEPn99ssC9Y3W4_UhDcitZG80ACwS_Bpl6uk8OxAFybZ1DHHkBS1AK-lCO2P2JJCFRyM33mcvTgb9B6pADETzgJT2qfgOU4-1Pm0l55Mij1LS-h7QTj95DTFQMM7DD6elP0WcA'
|
|
51
|
+
|
|
51
52
|
axios({
|
|
52
53
|
url: this.API_URL + "/files/users",
|
|
53
54
|
headers: {
|