@tiledesk/tiledesk-voice-twilio-connector 0.1.14-rc7 → 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.
@@ -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 WhatsApp. Copy it to the Facebook Developer console in "Callback URL" in Whatsapp Configuration.</span>
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">Choose the verification token end copy it to the Facebook Developer console in "Verify Token" in Whatsapp Configuration</span>
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 SMS Connector App is configured on your {{ brand_name }} project.</p>
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 SMS Connector App is configured on your Tildesk project.</p>
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 SMS Connector channel</p>
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 SMS Connector App is not yet configured on your {{ brand_name }} project.</p>
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 SMS Connector App is not yet configured on your Tildesk project.</p>
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
- if (!response.data) {
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
- const xml = xmlbuilder.create("Response", {});
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,50 +303,54 @@ class TiledeskTwilioTranslator {
260
303
  /** DONE **/
261
304
  async speechFormVXMLConverter(rootEle, message, xmlAttributes) {
262
305
 
263
-
264
- const gather = rootEle.ele("Gather", { input: "speech"})
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 handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
282
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
283
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
284
- }
285
-
286
-
287
- /*let queryUrl = '?intentName='+ xmlAttributes.intentName + "&previousIntentTimestamp="+Date.now();
288
- const prompt = this.promptVXML(rootEle, message, xmlAttributes);
289
-
290
- const record = rootEle.ele("Record", { playBeep: "false"})
291
-
292
- if(xmlAttributes && xmlAttributes.noInputTimeout){
293
- record.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
294
- }
295
-
296
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
297
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
298
- queryUrl += '&'+ handleNoInputNoMatchQuery.queryNoInput
299
- }
300
-
301
- record.att("action", this.BASE_URL + '/record/' + xmlAttributes.callSid + queryUrl)
306
+ if(this.voiceProvider === VOICE_PROVIDER.TWILIO){
307
+
308
+ const gather = rootEle.ele("Gather", { input: "speech"})
309
+
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)
302
344
  .att("method", "POST")
303
345
  .att("trim", "trim-silence")
304
-
305
- */
306
-
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
+ }
307
354
 
308
355
  return rootEle.end({ pretty: true });
309
356
  }
@@ -482,17 +529,25 @@ class TiledeskTwilioTranslator {
482
529
  if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
483
530
  let commands = msg.attributes.commands;
484
531
  let i = 0;
485
- new Promise((resolve, reject) => {
486
- function execute(command) {
532
+ await new Promise((resolve, reject) => {
533
+ const that = this
534
+ async function execute(command) {
487
535
  if (command.type === "message") {
488
536
  //case type: TEXT
489
537
  if(command.message.type === 'text'){
490
- rootEle.ele("Say", { voice: attributes.voiceName, language: attributes.voiceLanguage }, command.message.text);
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
+
491
546
  }
492
547
  //case type: FRAME
493
548
  if(command.message.type === 'frame' && command.message.metadata.src !== ""){
494
549
  if(command.message.text != ''){
495
- rootEle.ele("Say", { voice: attributes.voiceName, language: attributes.voiceLanguage }, command.message.text);
550
+ rootEle.ele("Say", { voice: attributes.TTS_VOICE_NAME, language: attributes.TTS_VOICE_LANGUAGE }, command.message.text);
496
551
  }
497
552
  rootEle.ele('Play', {}, command.message.metadata.src )
498
553
  }
@@ -567,7 +622,18 @@ class TiledeskTwilioTranslator {
567
622
  return transfer.up()
568
623
  }
569
624
 
570
-
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
+ }
571
637
 
572
638
 
573
639
  async jsonToVxmlConverter(json) {
@@ -51,7 +51,6 @@ class VoiceChannel {
51
51
  }
52
52
 
53
53
 
54
-
55
54
  async getNextDelayTimeForCallId(callId){
56
55
  const index = await this.redis_client.get('tiledesk:vxml:'+callId + ':delayIndex');
57
56
  if(index){
@@ -38,8 +38,9 @@ module.exports = {
38
38
  SPEECH_FORM: 'speech_form',
39
39
 
40
40
  },
41
- WAIT_BASE_DELAY_TIME: 250,
42
- VOICE_NAME: 'Polly.Bianca',
41
+ BASE_POOLING_DELAY: 250,
42
+ MAX_POLLING_TIME: 30000,
43
+ VOICE_NAME: 'Polly.Bianca-Neural',
43
44
  VOICE_LANGUAGE: 'it-IT',
44
45
  CALL_STATUS: {
45
46
  INITIATED: 'initiated',
@@ -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 };