@tiledesk/tiledesk-voice-twilio-connector 0.1.26-rc9 → 0.1.27
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 +328 -294
- package/logs/app.log +92 -0
- package/package.json +2 -1
- package/routes/manageApp.js +8 -8
- package/tiledesk/KVBaseMongo.js +1 -1
- package/tiledesk/TiledeskChannel.js +4 -4
- package/tiledesk/TiledeskTwilioTranslator.js +80 -45
- package/tiledesk/VoiceChannel.js +59 -35
- package/tiledesk/constants.js +17 -1
- package/tiledesk/errors.js +28 -0
- package/tiledesk/services/AiService.js +91 -74
- package/tiledesk/services/UploadService.js +4 -1
- package/tiledesk/services/voiceEventEmitter.js +6 -0
- package/tiledesk/utils.js +29 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class SttError extends Error {
|
|
2
|
+
constructor(code, message, extra = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.code = code; // es. 'AUDIO_DOWNLOAD_FAILED'
|
|
5
|
+
this.extra = extra; // opzionale, eventuali dati aggiuntivi
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class RedisError extends Error {
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class ServiceError extends Error {
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// esporta tutte insieme
|
|
24
|
+
module.exports = {
|
|
25
|
+
SttError,
|
|
26
|
+
RedisError,
|
|
27
|
+
ServiceError
|
|
28
|
+
};
|
|
@@ -2,6 +2,9 @@ var winston = require('../../winston');
|
|
|
2
2
|
const axios = require("axios").default;
|
|
3
3
|
const FormData = require('form-data');
|
|
4
4
|
|
|
5
|
+
/*ERROR HANDLER*/
|
|
6
|
+
const { ServiceError } = require('../errors');
|
|
7
|
+
|
|
5
8
|
/*UTILS*/
|
|
6
9
|
const fileUtils = require('../fileUtils.js')
|
|
7
10
|
|
|
@@ -30,44 +33,52 @@ class AiService {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
async speechToText(fileUrl, model, GPT_KEY) {
|
|
33
|
-
|
|
36
|
+
let start_time = new Date();
|
|
34
37
|
winston.debug("[AiService] speechToText url: "+ fileUrl);
|
|
35
|
-
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
36
|
-
winston.error("[AiService] err while downloadFromUrl: ", err)
|
|
37
|
-
return null; // fallback per evitare undefined
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
if (!file) {
|
|
41
|
-
winston.error('file non esisteeeeeeee')
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
38
|
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
|
|
39
|
+
return new Promise(async (resolve, reject) => {
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
try {
|
|
42
|
+
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
43
|
+
winston.error("[AiService] err while downloadFromUrl: ", err)
|
|
44
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl));
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!file) {
|
|
48
|
+
winston.debug('[AiService] OPENAI speechToText file NOT EXIST: . . . return')
|
|
49
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null'));
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
const formData = new FormData();
|
|
53
|
+
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
54
|
+
formData.append('model', model);
|
|
55
|
+
|
|
56
|
+
axios({
|
|
57
|
+
url: `${this.OPENAI_ENDPOINT}/audio/transcriptions`,
|
|
58
|
+
headers: {
|
|
59
|
+
...formData.getHeaders(),
|
|
60
|
+
"Authorization": "Bearer " + GPT_KEY
|
|
61
|
+
},
|
|
62
|
+
data: formData,
|
|
63
|
+
method: 'POST'
|
|
64
|
+
}).then((resbody) => {
|
|
65
|
+
resolve(resbody.data.text);
|
|
66
|
+
let end_time = new Date();
|
|
67
|
+
winston.verbose(`-----> [AiService] OpenAI speechToText time elapsed: ${end_time - start_time} ms`);
|
|
68
|
+
}).catch((err) => {
|
|
69
|
+
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI /audio/transcriptions API failed with err:', err));
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
winston.error("[AiService] OpenAI STT error", err.message);
|
|
73
|
+
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI STT service failed with err:', err));
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
|
|
66
77
|
})
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
async textToSpeech(text, name, model, GPT_KEY){
|
|
70
|
-
|
|
81
|
+
let start_time = new Date();
|
|
71
82
|
winston.debug('[AiService] textToSpeech text:'+ text)
|
|
72
83
|
|
|
73
84
|
const data = {
|
|
@@ -76,12 +87,11 @@ 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) => {
|
|
83
93
|
axios({
|
|
84
|
-
url: this.OPENAI_ENDPOINT
|
|
94
|
+
url: `${this.OPENAI_ENDPOINT}/audio/speech`,
|
|
85
95
|
headers: {
|
|
86
96
|
"Content-Type": "application/json",
|
|
87
97
|
"Authorization": "Bearer " + GPT_KEY
|
|
@@ -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,60 +116,63 @@ class AiService {
|
|
|
104
116
|
|
|
105
117
|
|
|
106
118
|
async speechToTextElevenLabs(fileUrl, model, language, API_KEY) {
|
|
107
|
-
|
|
119
|
+
let start_time = new Date();
|
|
108
120
|
winston.debug("[AiService] ELEVEN Labs speechToText url: "+ fileUrl);
|
|
109
|
-
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
110
|
-
winston.error("[AiService] err: ", err)
|
|
111
|
-
return null; // fallback per evitare undefined
|
|
112
|
-
})
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
return new Promise(async (resolve, reject) => {
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
126
|
+
winston.error("[AiService] err: ", err)
|
|
127
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl));
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (!file) {
|
|
131
|
+
winston.debug('[AiService] ELEVEN Labs speechToText file NOT EXIST: . . . return')
|
|
132
|
+
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null'));
|
|
133
|
+
}
|
|
120
134
|
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
const formData = new FormData();
|
|
137
|
+
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
138
|
+
formData.append('model_id', "scribe_v1");
|
|
139
|
+
formData.append('language_code', language)
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
}
|
|
142
160
|
|
|
143
161
|
})
|
|
144
162
|
}
|
|
145
163
|
|
|
146
|
-
async textToSpeechElevenLabs(text, voice_id, model, API_KEY){
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
async textToSpeechElevenLabs(text, voice_id, model, language_code, API_KEY){
|
|
165
|
+
let start_time = new Date();
|
|
149
166
|
const data = {
|
|
150
167
|
model_id: model,
|
|
151
168
|
text: text,
|
|
152
|
-
|
|
169
|
+
language_code: language_code
|
|
153
170
|
};
|
|
154
|
-
|
|
155
|
-
|
|
156
171
|
winston.debug('[AiService] ELEVEN Labs textToSpeech config:', data)
|
|
157
172
|
|
|
158
173
|
return new Promise((resolve, reject) => {
|
|
159
174
|
axios({
|
|
160
|
-
url: this.ELEVENLABS_ENDPOINT
|
|
175
|
+
url: `${this.ELEVENLABS_ENDPOINT}/v1/text-to-speech/${voice_id}?output_format=mp3_44100_128`,
|
|
161
176
|
headers: {
|
|
162
177
|
"Content-Type": "application/json",
|
|
163
178
|
"xi-api-key": API_KEY
|
|
@@ -166,11 +181,13 @@ class AiService {
|
|
|
166
181
|
data: data,
|
|
167
182
|
method: "POST",
|
|
168
183
|
}).then( async (response) => {
|
|
169
|
-
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`);
|
|
170
187
|
})
|
|
171
188
|
.catch((err) => {
|
|
172
189
|
winston.error("[AiService] ELEVEN Labs textToSpeech error: ", err);
|
|
173
|
-
reject(err)
|
|
190
|
+
reject(new ServiceError('AISERVICE_FAILED', 'ElevenLabs textToSpeech API failed with err:', err));
|
|
174
191
|
});
|
|
175
192
|
});
|
|
176
193
|
|
|
@@ -185,7 +202,7 @@ class AiService {
|
|
|
185
202
|
return new Promise((resolve, reject) => {
|
|
186
203
|
|
|
187
204
|
axios({
|
|
188
|
-
url: this.API_URL
|
|
205
|
+
url: `${this.API_URL}/${projectId}/quotes/tokens`,
|
|
189
206
|
headers: {
|
|
190
207
|
'Content-Type': 'application/json',
|
|
191
208
|
'Authorization': token
|
|
@@ -199,7 +216,7 @@ class AiService {
|
|
|
199
216
|
}
|
|
200
217
|
}).catch((err) => {
|
|
201
218
|
winston.error("[AiService] checkQuoteAvailability error: ", err.response?.data);
|
|
202
|
-
reject(err);
|
|
219
|
+
reject(new ServiceError('AISERVICE_FAILED', 'checkQuoteAvailability API failed with err:', err));
|
|
203
220
|
})
|
|
204
221
|
|
|
205
222
|
})
|
|
@@ -7,6 +7,9 @@ const path = require('path');
|
|
|
7
7
|
/*UTILS*/
|
|
8
8
|
const fileUtils = require('../fileUtils.js')
|
|
9
9
|
|
|
10
|
+
/*ERROR HANDLER*/
|
|
11
|
+
const { ServiceError } = require('../errors');
|
|
12
|
+
|
|
10
13
|
class UploadService {
|
|
11
14
|
|
|
12
15
|
constructor(config) {
|
|
@@ -69,7 +72,7 @@ class UploadService {
|
|
|
69
72
|
|
|
70
73
|
}).catch((err) => {
|
|
71
74
|
console.log('err', err)
|
|
72
|
-
reject(err);
|
|
75
|
+
reject(new ServiceError('UPLOADSERVICE_FAILED', 'UploadService /files/users API failed with err:', err) );
|
|
73
76
|
}).finally(() => {
|
|
74
77
|
// Sempre eseguito
|
|
75
78
|
if (fs.existsSync(tempFilePath)) {
|
package/tiledesk/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const NON_SPEECH_TOKENS = require('./constants').NON_SPEECH_TOKENS
|
|
2
|
+
const removeMarkdown = require('remove-markdown');
|
|
1
3
|
|
|
2
4
|
function getNumber(phoneNumber){
|
|
3
5
|
if(phoneNumber.startsWith('+')){
|
|
@@ -12,5 +14,31 @@ function buildQueryString(query) {
|
|
|
12
14
|
return params.toString() ? `?${params.toString()}` : '';
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
function normalizeSTT(text) {
|
|
18
|
+
if (!text) return null;
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
const cleaned = text.trim().toLowerCase();
|
|
21
|
+
|
|
22
|
+
// solo token non verbali
|
|
23
|
+
if (NON_SPEECH_TOKENS.includes(cleaned)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// solo simboli o rumore
|
|
28
|
+
if (!/[a-zàèéìòù]/i.test(cleaned)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return cleaned;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function markdownToTwilioSpeech(text) {
|
|
36
|
+
return removeMarkdown(text)
|
|
37
|
+
.replace(/:/g, ': ')
|
|
38
|
+
.replace(/\n+/g, '. ')
|
|
39
|
+
.replace(/\s+/g, ' ')
|
|
40
|
+
.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
module.exports = {getNumber, buildQueryString, normalizeSTT, markdownToTwilioSpeech}
|