@tiledesk/tiledesk-voice-twilio-connector 0.1.14 → 0.1.15
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 +240 -134
- package/package.json +2 -1
- package/routes/manageApp.js +11 -1
- package/template/configure.html +26 -8
- package/template/css/configure.css +12 -1
- package/tiledesk/TiledeskChannel.js +18 -83
- package/tiledesk/TiledeskTwilioTranslator.js +114 -50
- package/tiledesk/VoiceChannel.js +0 -1
- package/tiledesk/constants.js +11 -1
- package/tiledesk/fileUtils.js +54 -0
- package/tiledesk/services/AiService.js +93 -0
- package/tiledesk/services/IntegrationService.js +82 -0
- package/tiledesk/services/UploadService.js +79 -0
- package/tiledesk/services/speech_voice-twilio-393892661914_CAa837741db2a05b0cfa946d034c0c4048.wav +0 -0
- package/tiledesk/services/speech_voice-twilio-393892661914_CAcb69e06f3ea491f99778d58ddce7d70d.wav +0 -0
package/template/configure.html
CHANGED
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
<input type="text" readonly class="form-control copy-form custom-input" name="proxy_url" id="proxy_url" value="{{ proxy_url}}">
|
|
181
181
|
<span style="margin-left: 10px;">
|
|
182
182
|
<i class="fa fa-question-circle custom-tooltip">
|
|
183
|
-
<span class="custom-tooltiptext">This is the endpoint to be reached by
|
|
183
|
+
<span class="custom-tooltiptext">This is the endpoint to be reached by Twilio. Copy it to the Voice "Configuration URL" section in Configure Tab of your number. Please set "HTTP POST" as the method of the API call</span>
|
|
184
184
|
</i>
|
|
185
185
|
</span>
|
|
186
186
|
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
<input type="password" required class="form-control custom-input" name="auth_token" value="{{ auth_token }}" placeholder="Enter your Auth Token">
|
|
232
232
|
<span style="margin-left: 10px;">
|
|
233
233
|
<i class="fa fa-question-circle custom-tooltip">
|
|
234
|
-
<span class="custom-tooltiptext">
|
|
234
|
+
<span class="custom-tooltiptext">This is the Authorization Token that can be found in your Twilio console account</span>
|
|
235
235
|
</i>
|
|
236
236
|
</span>
|
|
237
237
|
</div>
|
|
@@ -258,6 +258,21 @@
|
|
|
258
258
|
</div>
|
|
259
259
|
</div>
|
|
260
260
|
|
|
261
|
+
<!-- OPEN AI -->
|
|
262
|
+
<!--
|
|
263
|
+
<div class="form-group" style="width: 522px;">
|
|
264
|
+
<div style="display: flex; flex-direction: row; align-items: center; gap: 5px">
|
|
265
|
+
<input class="form-control custom-checkbox" type="checkbox" id="enable_openai" name="enable_openai" {{#if enable_openai}}checked{{/if}}>
|
|
266
|
+
<label class="input-label no-margin" for="enable_openai">Enable Openai Provider<span style="font-weight: normal;">(Optional)</span></label>
|
|
267
|
+
<span style="margin-left: 10px;">
|
|
268
|
+
<i class="fa fa-question-circle custom-tooltip">
|
|
269
|
+
<span class="custom-tooltiptext">Only check if you want to use TTS and STT from OpenAi provider (by default Twilio provider is used)</span>
|
|
270
|
+
</i>
|
|
271
|
+
</span>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
-->
|
|
275
|
+
|
|
261
276
|
<input type="hidden" name="project_id" value="{{ project_id }}" />
|
|
262
277
|
<input type="hidden" name="token" value="{{ token }}" />
|
|
263
278
|
|
|
@@ -272,7 +287,8 @@
|
|
|
272
287
|
</div>
|
|
273
288
|
{{/if}}
|
|
274
289
|
</form>
|
|
275
|
-
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
276
292
|
|
|
277
293
|
|
|
278
294
|
|
|
@@ -288,12 +304,12 @@
|
|
|
288
304
|
{{#if subscription_id}}
|
|
289
305
|
<div>
|
|
290
306
|
{{#if brand_name}}
|
|
291
|
-
<p style="color: rgb(140, 140, 140);">The
|
|
307
|
+
<p style="color: rgb(140, 140, 140);">The Twilio Voice Connector App is configured on your {{ brand_name }} project.</p>
|
|
292
308
|
{{else}}
|
|
293
|
-
<p style="color: rgb(140, 140, 140);">The
|
|
309
|
+
<p style="color: rgb(140, 140, 140);">The Twilio Voice Connector App is configured on your Tildesk project.</p>
|
|
294
310
|
{{/if}}
|
|
295
311
|
|
|
296
|
-
<p>By clicking on disconnect you will no longer be able to receive messages from the
|
|
312
|
+
<p>By clicking on disconnect you will no longer be able to receive messages from the Twilio Voice Connector channel</p>
|
|
297
313
|
<form action="./disconnect" method="post">
|
|
298
314
|
<input type="hidden" name="project_id" value="{{ project_id }}">
|
|
299
315
|
<input type="hidden" name="token" value="{{ token }}">
|
|
@@ -304,9 +320,9 @@
|
|
|
304
320
|
{{else}}
|
|
305
321
|
<!-- App not configured -->
|
|
306
322
|
{{#if brand_name}}
|
|
307
|
-
<p style="color: rgb(140, 140, 140);">The
|
|
323
|
+
<p style="color: rgb(140, 140, 140);">The Twilio Voice Connector App is not yet configured on your {{ brand_name }} project.</p>
|
|
308
324
|
{{else}}
|
|
309
|
-
<p style="color: rgb(140, 140, 140);">The
|
|
325
|
+
<p style="color: rgb(140, 140, 140);">The Twilio Voice Connector App is not yet configured on your Tildesk project.</p>
|
|
310
326
|
{{/if}}
|
|
311
327
|
|
|
312
328
|
{{/if}}
|
|
@@ -347,6 +363,8 @@
|
|
|
347
363
|
window.parent.postMessage(msg, '*');
|
|
348
364
|
}
|
|
349
365
|
|
|
366
|
+
|
|
367
|
+
|
|
350
368
|
</script>
|
|
351
369
|
|
|
352
370
|
|
|
@@ -111,6 +111,10 @@ ul {
|
|
|
111
111
|
font-weight: 600;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
.no-margin{
|
|
115
|
+
margin: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
|
|
115
119
|
.btn:focus {
|
|
116
120
|
outline: none !important;
|
|
@@ -175,6 +179,13 @@ ul {
|
|
|
175
179
|
border-color: #0ba2dc;
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
|
|
183
|
+
.custom-checkbox{
|
|
184
|
+
height: 25px;
|
|
185
|
+
width: 25px;
|
|
186
|
+
margin: 0px !important;
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
.custom-tooltip {
|
|
179
190
|
position: relative;
|
|
180
191
|
display: inline-block;
|
|
@@ -419,4 +430,4 @@ ul {
|
|
|
419
430
|
align-items: center;
|
|
420
431
|
justify-content: space-evenly;
|
|
421
432
|
/*justify-content: center;*/
|
|
422
|
-
}
|
|
433
|
+
}
|
|
@@ -3,10 +3,10 @@ const jwt = require("jsonwebtoken");
|
|
|
3
3
|
const { v4: uuidv4 } = require("uuid");
|
|
4
4
|
const { promisify } = require('util');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
-
const FormData = require('form-data');
|
|
7
6
|
|
|
8
7
|
/*UTILS*/
|
|
9
8
|
const utils = require('./utils-message.js')
|
|
9
|
+
const fileUtils = require('./fileUtils.js')
|
|
10
10
|
const TYPE_MESSAGE = require('./constants').TYPE_MESSAGE
|
|
11
11
|
const MESSAGE_TYPE_MINE = require('./constants').MESSAGE_TYPE_MINE
|
|
12
12
|
const MESSAGE_TYPE_OTHERS = require('./constants').MESSAGE_TYPE_OTHERS
|
|
@@ -95,23 +95,24 @@ class TiledeskChannel {
|
|
|
95
95
|
data: {},
|
|
96
96
|
method: "POST",
|
|
97
97
|
}).then( async (response) => {
|
|
98
|
-
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
//response.data.token = await this.fixToken(response.data.token);
|
|
102
|
-
let token = await this.fixToken(response.data.token);
|
|
103
|
-
|
|
104
|
-
let data = {
|
|
105
|
-
token: token,
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
return data
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
.catch((err) => {
|
|
112
|
-
winston.error("[TiledeskChannel] sign in error: ", err.response);
|
|
98
|
+
if (!response.data) {
|
|
113
99
|
return null;
|
|
114
|
-
}
|
|
100
|
+
}
|
|
101
|
+
//response.data.token = await this.fixToken(response.data.token);
|
|
102
|
+
let token = await this.fixToken(response.data.token);
|
|
103
|
+
|
|
104
|
+
let data = {
|
|
105
|
+
token: token,
|
|
106
|
+
_id: response.data.user._id
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return data
|
|
110
|
+
|
|
111
|
+
})
|
|
112
|
+
.catch((err) => {
|
|
113
|
+
winston.error("[TiledeskChannel] sign in error: ", err.response);
|
|
114
|
+
return null;
|
|
115
|
+
});
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
async generateConversation(ani, callId){
|
|
@@ -186,72 +187,6 @@ class TiledeskChannel {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
|
|
189
|
-
async speechToText(fileUrl, model){
|
|
190
|
-
|
|
191
|
-
winston.debug("[TiledeskChannel] speechToText url: "+ fileUrl);
|
|
192
|
-
/*const response = await axios({
|
|
193
|
-
url: fileUrl,
|
|
194
|
-
method: 'GET',
|
|
195
|
-
responseType: 'stream',
|
|
196
|
-
}).catch((err) => {
|
|
197
|
-
winston.error("[TiledeskChannel] speechToText GET STREAM error: ", err);
|
|
198
|
-
return null;
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if(!response){
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
*/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const stream = await axios.get(fileUrl, {
|
|
209
|
-
responseType: 'arraybuffer'
|
|
210
|
-
}).catch((err) => {
|
|
211
|
-
winston.error("[TiledeskChannel] speechToText GET STREAM error: ", err);
|
|
212
|
-
return null;
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
const base64 = Buffer.from(stream.data, 'binary').toString('base64');
|
|
216
|
-
return 'text'
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
/*
|
|
220
|
-
stream.on('data', data => {
|
|
221
|
-
console.log('stream data', data);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
stream.on('end', () => {
|
|
225
|
-
console.log("stream done");
|
|
226
|
-
});
|
|
227
|
-
*/
|
|
228
|
-
|
|
229
|
-
/*
|
|
230
|
-
const formFile = new FormData();
|
|
231
|
-
formFile.append("file", stream.data, { filename: fileUrl.split('/').pop() + '.wav', contentType: 'audio/wav'});// Specifica il tipo MIME del file WAV});
|
|
232
|
-
formFile.append("model", model);
|
|
233
|
-
|
|
234
|
-
console.log('dataaaaaaa', formFile)
|
|
235
|
-
|
|
236
|
-
return axios({
|
|
237
|
-
url: "https://api.openai.com/v1/audio/transcriptions",
|
|
238
|
-
headers: {
|
|
239
|
-
'Content-Type': 'multipart/form-data',
|
|
240
|
-
'Authorization': "Bearer " + GPT_KEY,
|
|
241
|
-
...formFile.getHeaders(),
|
|
242
|
-
},
|
|
243
|
-
data: formFile,
|
|
244
|
-
method: 'POST'
|
|
245
|
-
}).then((response) => {
|
|
246
|
-
winston.debug("[TiledeskChannel] speechToText response : ", response.data);
|
|
247
|
-
return response.data.text;
|
|
248
|
-
}).catch((err) => {
|
|
249
|
-
winston.error("[TiledeskChannel] speechToText error: ", err);
|
|
250
|
-
return null;
|
|
251
|
-
})
|
|
252
|
-
*/
|
|
253
|
-
}
|
|
254
|
-
|
|
255
190
|
/** ADD MESSAGE TO REDIS QUEUE **/
|
|
256
191
|
async addMessageToQueue(message){
|
|
257
192
|
|
|
@@ -9,6 +9,8 @@ const WAIT_MESSAGE = require("./constants.js").WAIT_MESSAGE;
|
|
|
9
9
|
const TEXT_MESSAGE = require("./constants.js").TEXT_MESSAGE;
|
|
10
10
|
const SETTING_MESSAGE = require('./constants').SETTING_MESSAGE
|
|
11
11
|
const CHANNEL_NAME = require('./constants').CHANNEL_NAME
|
|
12
|
+
const VOICE_PROVIDER = require('./constants').VOICE_PROVIDER;
|
|
13
|
+
const OPENAI_SETTINGS = require('./constants').OPENAI_SETTINGS;
|
|
12
14
|
|
|
13
15
|
const TYPE_ACTION_VXML = require('./constants').TYPE_ACTION_VXML
|
|
14
16
|
const TYPE_MESSAGE = require('./constants').TYPE_MESSAGE
|
|
@@ -36,6 +38,14 @@ class TiledeskTwilioTranslator {
|
|
|
36
38
|
throw new Error("[TiledeskVXMLTranslator] config.APP_ID is mandatory");
|
|
37
39
|
}
|
|
38
40
|
this.BASE_URL = config.BASE_URL;
|
|
41
|
+
|
|
42
|
+
if(config.aiService){
|
|
43
|
+
this.aiService = config.aiService
|
|
44
|
+
}
|
|
45
|
+
if(config.uploadService){
|
|
46
|
+
this.uploadService = config.uploadService
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
|
|
40
50
|
this.log = false;
|
|
41
51
|
}
|
|
@@ -51,22 +61,55 @@ class TiledeskTwilioTranslator {
|
|
|
51
61
|
vxmlAttributes[key] = flowAttributes[key];
|
|
52
62
|
}
|
|
53
63
|
})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
//MANAGE VOICE SETTINGS from globals attributes
|
|
67
|
+
this.voiceProvider = VOICE_PROVIDER.TWILIO
|
|
68
|
+
if(flowAttributes.VOICE_PROVIDER){
|
|
69
|
+
this.voiceProvider = flowAttributes.VOICE_PROVIDER
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// IF VOICE_PROVIDER is TWILIO --> default values is on user account twilio settings
|
|
74
|
+
// IF VOICE_PROVIDER is OPENAI --> set default values from constants
|
|
75
|
+
if(this.voiceProvider === VOICE_PROVIDER.OPENAI){
|
|
76
|
+
vxmlAttributes.TTS_VOICE_NAME = flowAttributes.TTS_VOICE_NAME? flowAttributes.TTS_VOICE_NAME : OPENAI_SETTINGS.TTS_VOICE_NAME;
|
|
77
|
+
vxmlAttributes.TTS_MODEL = flowAttributes.TTS_MODEL? flowAttributes.TTS_MODEL : OPENAI_SETTINGS.TTS_MODEL;
|
|
78
|
+
vxmlAttributes.STT_MODEL = flowAttributes.STT_MODEL? flowAttributes.STT_MODEL : OPENAI_SETTINGS.STT_MODEL;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
54
83
|
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
55
87
|
winston.debug("[TiledeskVXMLTranslator] manageVoiceAttributes: vxmlAttributes returned:", vxmlAttributes);
|
|
56
88
|
return vxmlAttributes
|
|
57
89
|
}
|
|
90
|
+
|
|
91
|
+
|
|
58
92
|
|
|
59
|
-
async toVXML(msg, id, vxmlAttributes) {
|
|
93
|
+
async toVXML(msg, id, vxmlAttributes, sessionInfo) {
|
|
60
94
|
|
|
61
95
|
|
|
96
|
+
|
|
62
97
|
vxmlAttributes.intentName=''
|
|
63
98
|
if(msg.attributes.intentName){
|
|
64
99
|
vxmlAttributes.intentName = msg.attributes.intentName
|
|
65
100
|
}
|
|
66
|
-
|
|
101
|
+
|
|
67
102
|
vxmlAttributes = this.manageVoiceAttributes(msg, vxmlAttributes)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
this.user = sessionInfo.user
|
|
106
|
+
this.voiceSettings = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
const xml = xmlbuilder.create("Response", {});
|
|
68
110
|
//const header = this.headerVXML(xml, vxmlAttributes);
|
|
69
111
|
|
|
112
|
+
|
|
70
113
|
//MANAGE CLOSE info message
|
|
71
114
|
const isInfoSupport = utils.messageType(TYPE_MESSAGE.INFO_SUPPORT, msg)
|
|
72
115
|
winston.debug("[TiledeskVXMLTranslator] isInfoSupport:"+ isInfoSupport);
|
|
@@ -260,52 +303,54 @@ class TiledeskTwilioTranslator {
|
|
|
260
303
|
/** DONE **/
|
|
261
304
|
async speechFormVXMLConverter(rootEle, message, xmlAttributes) {
|
|
262
305
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
|
|
267
|
-
gather.att("action", this.BASE_URL + '/speechresult/' + xmlAttributes.callSid + queryUrl)
|
|
268
|
-
.att("method", "POST")
|
|
269
|
-
.att("language", xmlAttributes.voiceLanguage)
|
|
270
|
-
.att('speechTimeout', "0")
|
|
271
|
-
|
|
272
|
-
/*if(xmlAttributes && xmlAttributes.noInputTimeout){
|
|
273
|
-
gather.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
|
|
274
|
-
}*/
|
|
275
|
-
/*if(xmlAttributes && xmlAttributes.incompleteSpeechTimeout){
|
|
276
|
-
gather.att("speechTimeout", xmlAttributes.incompleteSpeechTimeout/1000 ).up();
|
|
277
|
-
}*/
|
|
278
|
-
|
|
279
|
-
const prompt = this.promptVXML(gather, message, xmlAttributes);
|
|
280
|
-
|
|
281
|
-
//const beep = gather.ele('Play', {}, "https://uccs-public.imagicle.cloud/fw/VR/confirmationBeepForVR_alternative.wav")
|
|
306
|
+
if(this.voiceProvider === VOICE_PROVIDER.TWILIO){
|
|
307
|
+
|
|
308
|
+
const gather = rootEle.ele("Gather", { input: "speech"})
|
|
282
309
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
|
|
311
|
+
gather.att("action", this.BASE_URL + '/speechresult/' + xmlAttributes.callSid + queryUrl)
|
|
312
|
+
.att("method", "POST")
|
|
313
|
+
.att("language", xmlAttributes.voiceLanguage)
|
|
314
|
+
.att('speechTimeout', "0")
|
|
315
|
+
|
|
316
|
+
//if(xmlAttributes && xmlAttributes.noInputTimeout){
|
|
317
|
+
// gather.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
|
|
318
|
+
//}
|
|
319
|
+
//if(xmlAttributes && xmlAttributes.incompleteSpeechTimeout){
|
|
320
|
+
// gather.att("speechTimeout", xmlAttributes.incompleteSpeechTimeout/1000 ).up();
|
|
321
|
+
//}
|
|
322
|
+
|
|
323
|
+
const prompt = this.promptVXML(gather, message, xmlAttributes);
|
|
324
|
+
|
|
325
|
+
const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
|
|
326
|
+
if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
|
|
327
|
+
rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
}else{
|
|
331
|
+
|
|
332
|
+
const prompt = await this.promptVXML(rootEle, message, xmlAttributes);
|
|
333
|
+
|
|
334
|
+
const record = rootEle.ele("Record", { playBeep: "false"})
|
|
335
|
+
|
|
336
|
+
let queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
|
|
337
|
+
const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
|
|
338
|
+
if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
|
|
339
|
+
queryUrl += '&'+ handleNoInputNoMatchQuery.queryNoInput
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
record
|
|
343
|
+
//.att("action", this.BASE_URL + '/record/' + xmlAttributes.callSid + queryUrl)
|
|
304
344
|
.att("method", "POST")
|
|
305
345
|
.att("trim", "trim-silence")
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
346
|
+
.att("recordingStatusCallback", this.BASE_URL + '/record/' + xmlAttributes.callSid + queryUrl)
|
|
347
|
+
.att("recordingStatusCallbackMethod", "POST")
|
|
348
|
+
|
|
349
|
+
if(xmlAttributes && xmlAttributes.noInputTimeout){
|
|
350
|
+
record.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
}
|
|
309
354
|
|
|
310
355
|
return rootEle.end({ pretty: true });
|
|
311
356
|
}
|
|
@@ -484,17 +529,25 @@ class TiledeskTwilioTranslator {
|
|
|
484
529
|
if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
|
|
485
530
|
let commands = msg.attributes.commands;
|
|
486
531
|
let i = 0;
|
|
487
|
-
new Promise((resolve, reject) => {
|
|
488
|
-
|
|
532
|
+
await new Promise((resolve, reject) => {
|
|
533
|
+
const that = this
|
|
534
|
+
async function execute(command) {
|
|
489
535
|
if (command.type === "message") {
|
|
490
536
|
//case type: TEXT
|
|
491
537
|
if(command.message.type === 'text'){
|
|
492
|
-
|
|
538
|
+
if(that.voiceProvider === VOICE_PROVIDER.OPENAI){
|
|
539
|
+
let voiceMessageUrl = await that.generateTTS(command.message.text, attributes)
|
|
540
|
+
rootEle.ele('Play', {}, voiceMessageUrl )
|
|
541
|
+
}else{
|
|
542
|
+
rootEle.ele("Say", { voice: attributes.TTS_VOICE_NAME, language: attributes.TTS_VOICE_LANGUAGE }, command.message.text);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
493
546
|
}
|
|
494
547
|
//case type: FRAME
|
|
495
548
|
if(command.message.type === 'frame' && command.message.metadata.src !== ""){
|
|
496
549
|
if(command.message.text != ''){
|
|
497
|
-
rootEle.ele("Say", { voice: attributes.
|
|
550
|
+
rootEle.ele("Say", { voice: attributes.TTS_VOICE_NAME, language: attributes.TTS_VOICE_LANGUAGE }, command.message.text);
|
|
498
551
|
}
|
|
499
552
|
rootEle.ele('Play', {}, command.message.metadata.src )
|
|
500
553
|
}
|
|
@@ -569,7 +622,18 @@ class TiledeskTwilioTranslator {
|
|
|
569
622
|
return transfer.up()
|
|
570
623
|
}
|
|
571
624
|
|
|
572
|
-
|
|
625
|
+
async generateTTS(text, attributes){
|
|
626
|
+
let GPT_KEY = this.voiceSettings?.key
|
|
627
|
+
|
|
628
|
+
let audioData = await this.aiService.textToSpeech(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, GPT_KEY).catch((err)=>{
|
|
629
|
+
console.log('errr while creating audio message', err.response?.data)
|
|
630
|
+
})
|
|
631
|
+
let fileUrl = await this.uploadService.upload(attributes.callSid, audioData, this.user).catch((err)=>{
|
|
632
|
+
console.log('errr while uploading audioData', err.response)
|
|
633
|
+
})
|
|
634
|
+
console.log('(voice) Audio Message url captured after TTS -->', fileUrl)
|
|
635
|
+
return fileUrl
|
|
636
|
+
}
|
|
573
637
|
|
|
574
638
|
|
|
575
639
|
async jsonToVxmlConverter(json) {
|
package/tiledesk/VoiceChannel.js
CHANGED
package/tiledesk/constants.js
CHANGED
|
@@ -38,7 +38,8 @@ module.exports = {
|
|
|
38
38
|
SPEECH_FORM: 'speech_form',
|
|
39
39
|
|
|
40
40
|
},
|
|
41
|
-
|
|
41
|
+
BASE_POOLING_DELAY: 250,
|
|
42
|
+
MAX_POLLING_TIME: 30000,
|
|
42
43
|
VOICE_NAME: 'Polly.Bianca-Neural',
|
|
43
44
|
VOICE_LANGUAGE: 'it-IT',
|
|
44
45
|
CALL_STATUS: {
|
|
@@ -47,5 +48,14 @@ module.exports = {
|
|
|
47
48
|
IN_PROGRESS: 'in-progress',
|
|
48
49
|
COMPLETED: 'completed',
|
|
49
50
|
FAILED: 'failed'
|
|
51
|
+
},
|
|
52
|
+
VOICE_PROVIDER: {
|
|
53
|
+
OPENAI: 'openai',
|
|
54
|
+
TWILIO: 'twilio'
|
|
55
|
+
},
|
|
56
|
+
OPENAI_SETTINGS:{
|
|
57
|
+
TTS_VOICE_NAME: 'alloy',
|
|
58
|
+
TTS_MODEL: 'tts-1',
|
|
59
|
+
STT_MODEL: 'whisper-1'
|
|
50
60
|
}
|
|
51
61
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const axios = require("axios").default;
|
|
2
|
+
const axiosRetry = require('axios-retry').default;
|
|
3
|
+
var winston = require('../winston');
|
|
4
|
+
|
|
5
|
+
/*axiosRetry(axios, {
|
|
6
|
+
retries: 3,
|
|
7
|
+
retryDelay: (...arg) => axiosRetry.exponentialDelay(...arg, 100),
|
|
8
|
+
retryCondition(error) {
|
|
9
|
+
|
|
10
|
+
if (error.response) {
|
|
11
|
+
winston.info("(retryCondition) response status: " + error.response.status);
|
|
12
|
+
switch (error.response.status) {
|
|
13
|
+
// example: retry only if status is 500 or 501
|
|
14
|
+
case 404:
|
|
15
|
+
return true;
|
|
16
|
+
default:
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
winston.info("(retryCondition) no response status. Error message: " + error.message);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
onRetry: (retryCount, error, requestConfig) => {
|
|
25
|
+
winston.info("retry count: " + retryCount);
|
|
26
|
+
winston.verbose("retry error: " + error.response.status + " " + error.response.statusText);
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
class FileUtils {
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async downloadFromUrl(url) {
|
|
35
|
+
|
|
36
|
+
return new Promise(async (resolve, reject) => {
|
|
37
|
+
await axios({
|
|
38
|
+
url: url,
|
|
39
|
+
responseType: 'arraybuffer',
|
|
40
|
+
method: 'GET'
|
|
41
|
+
}).then((resbody) => {
|
|
42
|
+
console.log('okkkkkkk')
|
|
43
|
+
resolve(resbody.data);
|
|
44
|
+
}).catch((err) => {
|
|
45
|
+
reject(err);
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var fileUtils = new FileUtils();
|
|
53
|
+
|
|
54
|
+
module.exports = fileUtils;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
var winston = require('../../winston');
|
|
2
|
+
const axios = require("axios").default;
|
|
3
|
+
const FormData = require('form-data');
|
|
4
|
+
|
|
5
|
+
/*UTILS*/
|
|
6
|
+
const fileUtils = require('../fileUtils.js')
|
|
7
|
+
|
|
8
|
+
class AiService {
|
|
9
|
+
|
|
10
|
+
constructor(config) {
|
|
11
|
+
|
|
12
|
+
if (!config) {
|
|
13
|
+
throw new Error("[AiService] config is mandatory");
|
|
14
|
+
}
|
|
15
|
+
if (!config.OPENAI_ENDPOINT) {
|
|
16
|
+
throw new Error("[AiService] config.OPENAI_ENDPOINT is mandatory");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.OPENAI_ENDPOINT = config.OPENAI_ENDPOINT;
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async speechToText(fileUrl, model, GPT_KEY) {
|
|
24
|
+
|
|
25
|
+
winston.debug("[AiService] speechToText url: "+ fileUrl);
|
|
26
|
+
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
27
|
+
winston.error("[AiService] err: ", err)
|
|
28
|
+
return null; // fallback per evitare undefined
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
|
|
34
|
+
const formData = new FormData();
|
|
35
|
+
formData.append('file', file, { filename: 'audiofile', contentType: 'audio/mpeg' });
|
|
36
|
+
formData.append('model', model);
|
|
37
|
+
|
|
38
|
+
axios({
|
|
39
|
+
url: this.OPENAI_ENDPOINT + "/audio/transcriptions",
|
|
40
|
+
headers: {
|
|
41
|
+
...formData.getHeaders(),
|
|
42
|
+
"Authorization": "Bearer " + GPT_KEY
|
|
43
|
+
},
|
|
44
|
+
data: formData,
|
|
45
|
+
method: 'POST'
|
|
46
|
+
}).then((resbody) => {
|
|
47
|
+
resolve(resbody.data.text);
|
|
48
|
+
}).catch((err) => {
|
|
49
|
+
reject(err);
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async textToSpeech(text, name, model, GPT_KEY){
|
|
56
|
+
|
|
57
|
+
winston.debug('[AiService] textToSpeech text:'+ text)
|
|
58
|
+
|
|
59
|
+
const data = {
|
|
60
|
+
model: model,
|
|
61
|
+
input: text,
|
|
62
|
+
voice: name,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
winston.debug('[AiService] textToSpeech config:', data)
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
axios({
|
|
70
|
+
url: this.OPENAI_ENDPOINT + "/audio/speech",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"Authorization": "Bearer " + GPT_KEY
|
|
74
|
+
},
|
|
75
|
+
responseType: 'arraybuffer',
|
|
76
|
+
data: data,
|
|
77
|
+
method: "POST",
|
|
78
|
+
}).then( async (response) => {
|
|
79
|
+
//console.log('[AiService] textToSpeech result', response?.data)
|
|
80
|
+
resolve(response?.data)
|
|
81
|
+
})
|
|
82
|
+
.catch((err) => {
|
|
83
|
+
winston.error("[AiService] textToSpeech error: ", err.response?.data);
|
|
84
|
+
reject(err)
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { AiService };
|