@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.
@@ -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,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 + "/audio/speech",
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
- 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
- console.log('dataaaaaa', resbody)
137
- resolve(resbody.data.text);
138
- }).catch((err) => {
139
- console.log('errrrrrr', err?.response)
140
- reject(err);
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
- output_format: "mp3_44100_128",
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 + "/v1/text-to-speech/"+ voice_id,
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 + "/" + projectId + "/quotes/tokens",
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)) {
@@ -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,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
- module.exports = {getNumber, buildQueryString}
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}