@tiledesk/tiledesk-voice-twilio-connector 0.1.27 → 0.2.0-rc3

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.
Files changed (50) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +44 -0
  3. package/index.js +7 -1529
  4. package/package.json +23 -22
  5. package/src/app.js +146 -0
  6. package/src/config/index.js +32 -0
  7. package/src/controllers/VoiceController.js +488 -0
  8. package/src/controllers/VoiceController.original.js +811 -0
  9. package/src/middlewares/httpLogger.js +31 -0
  10. package/src/models/KeyValueStore.js +78 -0
  11. package/src/routes/manageApp.js +298 -0
  12. package/src/routes/voice.js +22 -0
  13. package/src/services/AiService.js +219 -0
  14. package/src/services/AiService.sdk.js +367 -0
  15. package/src/services/IntegrationService.js +74 -0
  16. package/src/services/MessageService.js +133 -0
  17. package/src/services/README_SDK.md +107 -0
  18. package/src/services/SessionService.js +143 -0
  19. package/src/services/SpeechService.js +134 -0
  20. package/src/services/TiledeskMessageBuilder.js +135 -0
  21. package/src/services/TwilioService.js +122 -0
  22. package/src/services/UploadService.js +78 -0
  23. package/src/services/channels/TiledeskChannel.js +269 -0
  24. package/{tiledesk → src/services/channels}/VoiceChannel.js +17 -56
  25. package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
  26. package/src/services/index.js +45 -0
  27. package/src/services/translators/TiledeskTwilioTranslator.js +509 -0
  28. package/{tiledesk/TiledeskTwilioTranslator.js → src/services/translators/TiledeskTwilioTranslator.original.js} +119 -202
  29. package/src/utils/fileUtils.js +24 -0
  30. package/src/utils/logger.js +32 -0
  31. package/{tiledesk → src/utils}/utils-message.js +6 -21
  32. package/logs/app.log +0 -3082
  33. package/routes/manageApp.js +0 -419
  34. package/tiledesk/KVBaseMongo.js +0 -101
  35. package/tiledesk/TiledeskChannel.js +0 -363
  36. package/tiledesk/TiledeskSubscriptionClient.js +0 -135
  37. package/tiledesk/fileUtils.js +0 -55
  38. package/tiledesk/services/AiService.js +0 -230
  39. package/tiledesk/services/IntegrationService.js +0 -81
  40. package/tiledesk/services/UploadService.js +0 -88
  41. /package/{winston.js → src/config/logger.js} +0 -0
  42. /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
  43. /package/{template → src/template}/configure.html +0 -0
  44. /package/{template → src/template}/css/configure.css +0 -0
  45. /package/{template → src/template}/css/error.css +0 -0
  46. /package/{template → src/template}/css/style.css +0 -0
  47. /package/{template → src/template}/error.html +0 -0
  48. /package/{tiledesk → src/utils}/constants.js +0 -0
  49. /package/{tiledesk → src/utils}/errors.js +0 -0
  50. /package/{tiledesk → src/utils}/utils.js +0 -0
@@ -0,0 +1,31 @@
1
+ const logger = require('../utils/logger');
2
+
3
+ let lastRequestTime = Date.now();
4
+
5
+ const httpLogger = (req, res, next) => {
6
+ const startTime = Date.now();
7
+ const timeSinceLastRequest = startTime - lastRequestTime;
8
+ lastRequestTime = startTime;
9
+
10
+ // Hook into response finish to calculate processing time
11
+ res.on('finish', () => {
12
+ const duration = Date.now() - startTime;
13
+ const bodySize = req.headers['content-length'] || (req.body ? JSON.stringify(req.body).length : 0);
14
+
15
+ const logData = {
16
+ method: req.method,
17
+ path: req.path,
18
+ query: req.query,
19
+ bodySize: bodySize,
20
+ statusCode: res.statusCode,
21
+ durationMs: duration,
22
+ timeSinceLastRequestMs: timeSinceLastRequest
23
+ };
24
+
25
+ logger.info(`HTTP: ${JSON.stringify(logData)}`);
26
+ });
27
+
28
+ next();
29
+ };
30
+
31
+ module.exports = httpLogger;
@@ -0,0 +1,78 @@
1
+ const mongodb = require("mongodb");
2
+ var winston = require('../utils/logger');
3
+
4
+ class KVBaseMongo {
5
+
6
+ /**
7
+ * Constructor for KVBaseMongo object
8
+ *
9
+ * @example
10
+ * const { KVBaseMongo } = require('./KVBaseMongo');
11
+ * let db = new KVBaseMongo("kvstore");
12
+ *
13
+ * @param {KVBASE_COLLECTION} The name of the Mongodb collection used as key-value store. Mandatory.
14
+ */
15
+ constructor(KVBASE_COLLECTION) {
16
+ if (!KVBASE_COLLECTION) {
17
+ throw new Error('KVBASE_COLLECTION (the name of the Mongodb collection used as key-value store) is mandatory.');
18
+ }
19
+ this.KV_COLLECTION = KVBASE_COLLECTION;
20
+ winston.debug("KV_COLLECTION: " + this.KV_COLLECTION)
21
+ }
22
+
23
+ async connect(MONGODB_URI) {
24
+ try {
25
+ const client = await mongodb.MongoClient.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
26
+ this.db = client.db();
27
+ await this.db.collection(this.KV_COLLECTION).createIndex(
28
+ { "key": 1 }, { unique: true }
29
+ );
30
+ //winston.debug("[mongodb] db: ", this.db);
31
+ } catch (err) {
32
+ winston.error(err);
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ async set(k, v) {
38
+ try {
39
+ await this.db.collection(this.KV_COLLECTION).updateOne({key: k}, { $set: { value: v, key: k } }, { upsert: true });
40
+ } catch (err) {
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ async reuseConnection(db) {
46
+ this.db = db;
47
+ await this.db.collection(this.KV_COLLECTION).createIndex(
48
+ { "key": 1 }, { unique: true }
49
+ );
50
+ }
51
+
52
+ async get(k) {
53
+ winston.debug("Searching on Collection " + this.KV_COLLECTION + ' for key: '+ k);
54
+ try {
55
+ const doc = await this.db.collection(this.KV_COLLECTION).findOne({ key: k });
56
+ if (doc) {
57
+ winston.debug("Doc found with key: " + doc.key);
58
+ return doc.value;
59
+ } else {
60
+ winston.debug("No Doc found!");
61
+ return null;
62
+ }
63
+ } catch (err) {
64
+ winston.error("Error reading mongodb value", err);
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ async remove(k) {
70
+ try {
71
+ await this.db.collection(this.KV_COLLECTION).deleteOne({key: k});
72
+ } catch (err) {
73
+ throw err;
74
+ }
75
+ }
76
+ }
77
+
78
+ module.exports = { KVBaseMongo };
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ const express = require("express");
3
+ const bodyParser = require("body-parser")
4
+ const router = express.Router();
5
+ const winston = require("../utils/logger");
6
+ const pjson = require('../../package.json');
7
+ const fs = require('fs').promises;
8
+ const path = require('path');
9
+ const handlebars = require('handlebars');
10
+
11
+ // tiledesk clients
12
+ const { TiledeskChannel } = require("../services/channels/TiledeskChannel")
13
+ const { TiledeskSubscriptionClient } = require('../services/clients/TiledeskSubscriptionClient');
14
+
15
+ //constant
16
+ const CHANNEL_NAME = require('../utils/constants').CHANNEL_NAME;
17
+
18
+ router.use(bodyParser.json());
19
+ router.use(express.urlencoded({ extended: true }));
20
+ router.use(express.static(path.join(__dirname, '..', 'template')));
21
+
22
+ // Handlebars helpers
23
+ handlebars.registerHelper('isEqual', (a, b) => a == b);
24
+ handlebars.registerHelper('json', (a) => JSON.stringify(a));
25
+
26
+ module.exports = (services) => {
27
+ const { db, redisClient, config } = services;
28
+ const API_URL = config.API_URL;
29
+ const BASE_URL = config.BASE_URL;
30
+ const BRAND_NAME = config.BRAND_NAME || "Tiledesk";
31
+
32
+ // Helper to render template
33
+ async function renderTemplate(templateName, data) {
34
+ const templatePath = path.join(__dirname, '..', 'template', templateName);
35
+ try {
36
+ const html = await fs.readFile(templatePath, 'utf-8');
37
+ const template = handlebars.compile(html);
38
+ return template(data);
39
+ } catch (err) {
40
+ winston.error("Error rendering template:", err);
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ router.get("/", async (req, res) => {
46
+ res.send("Welcome to Tiledesk-VOICE connector (manage ROUTE)");
47
+ });
48
+
49
+ router.get('/configure', async (req, res) => {
50
+ winston.debug("(voice) /configure :params", req.query);
51
+
52
+ let project_id = req.query.project_id;
53
+ let token = req.query.token;
54
+ let popup_view = req.query.view === 'popup';
55
+
56
+ if (!project_id || !token) {
57
+ const html = await renderTemplate('error.html', {
58
+ app_version: pjson.version,
59
+ error_message: "Query params project_id and token are required."
60
+ });
61
+ return res.send(html);
62
+ }
63
+
64
+ try {
65
+ let CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
66
+ let settings = await db.get(CONTENT_KEY);
67
+ winston.debug("(voice) settings: ", settings);
68
+
69
+ const tdChannel = new TiledeskChannel({
70
+ API_URL: API_URL,
71
+ redis_client: redisClient
72
+ })
73
+
74
+ let departments = await tdChannel.getDepartments(token, project_id).catch((err) => {
75
+ winston.error("Error getting departments", err.response);
76
+ return [];
77
+ });
78
+
79
+ let proxy_url = BASE_URL + "/webhook/" + project_id;
80
+ let status_url = BASE_URL + "/twilio/status";
81
+
82
+ if (!settings) {
83
+ try {
84
+ const subscription = await subscribe(token, project_id, API_URL, BASE_URL, db);
85
+ settings = {
86
+ project_id: project_id,
87
+ token: token,
88
+ subscriptionId: subscription._id,
89
+ secret: subscription.secret
90
+ };
91
+ } catch(e) {
92
+ winston.error("Auto-subscribe failed", e);
93
+ const html = await renderTemplate('error.html', {
94
+ app_version: pjson.version,
95
+ error_message: "Auto-subscribe failed: " + e.message
96
+ });
97
+ return res.send(html);
98
+ }
99
+ }
100
+
101
+ const html = await renderTemplate('configure.html', {
102
+ app_version: pjson.version,
103
+ project_id: project_id,
104
+ token: token,
105
+ proxy_url: proxy_url,
106
+ status_url: status_url,
107
+ subscription_id: settings.subscriptionId,
108
+ department_id: settings.department_id,
109
+ account_sid: settings.account_sid,
110
+ auth_token: settings.auth_token,
111
+ departments: departments,
112
+ brand_name: BRAND_NAME,
113
+ popup_view: popup_view
114
+ });
115
+
116
+ return res.send(html);
117
+
118
+ } catch (err) {
119
+ winston.error("Error in /configure", err);
120
+ const html = await renderTemplate('error.html', {
121
+ app_version: pjson.version,
122
+ error_message: "Internal Server Error: " + err.message
123
+ });
124
+ return res.status(500).send(html);
125
+ }
126
+ })
127
+
128
+ router.post('/update', async (req, res) => {
129
+ winston.debug("(voice) /update", req.body);
130
+
131
+ let project_id = req.body.project_id;
132
+ let token = req.body.token;
133
+ let department_id = req.body.department;
134
+ let account_sid = req.body.account_sid;
135
+ let auth_token = req.body.auth_token;
136
+
137
+ if (!project_id || !token) {
138
+ return res.status(400).send("project_id and token are required");
139
+ }
140
+
141
+ let CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
142
+ let settings = await db.get(CONTENT_KEY);
143
+
144
+ let proxy_url = BASE_URL + "/webhook/" + project_id;
145
+ let status_url = BASE_URL + "/twilio/status";
146
+
147
+ // get departments
148
+ const tdChannel = new TiledeskChannel({
149
+ API_URL: API_URL,
150
+ redis_client: redisClient
151
+ })
152
+
153
+ let departments = await tdChannel.getDepartments(token, project_id).catch((err) => {
154
+ winston.error("Error getting departments", err.response);
155
+ return [];
156
+ });
157
+
158
+ try {
159
+ if (settings) {
160
+ settings.department_id = department_id;
161
+ settings.account_sid = account_sid;
162
+ settings.auth_token = auth_token;
163
+ await db.set(CONTENT_KEY, settings);
164
+ } else {
165
+ const subscription = await subscribe(token, project_id, API_URL, BASE_URL, db);
166
+ settings = {
167
+ project_id: project_id,
168
+ token: token,
169
+ subscriptionId: subscription._id,
170
+ secret: subscription.secret,
171
+ department_id: department_id,
172
+ account_sid: account_sid,
173
+ auth_token: auth_token
174
+ };
175
+ await db.set(CONTENT_KEY, settings);
176
+ }
177
+
178
+ const html = await renderTemplate('configure.html', {
179
+ app_version: pjson.version,
180
+ project_id: project_id,
181
+ token: token,
182
+ proxy_url: proxy_url,
183
+ status_url: status_url,
184
+ show_success_modal: true,
185
+ subscription_id: settings.subscriptionId,
186
+ department_id: settings.department_id,
187
+ account_sid: settings.account_sid,
188
+ auth_token: settings.auth_token,
189
+ departments: departments,
190
+ brand_name: BRAND_NAME
191
+ });
192
+ return res.send(html);
193
+
194
+ } catch (error) {
195
+ winston.error("Update failed", error);
196
+ const html = await renderTemplate('configure.html', {
197
+ app_version: pjson.version,
198
+ project_id: project_id,
199
+ token: token,
200
+ proxy_url: proxy_url,
201
+ status_url: status_url,
202
+ show_error_modal: true,
203
+ departments: departments,
204
+ brand_name: BRAND_NAME
205
+ });
206
+ return res.send(html);
207
+ }
208
+ })
209
+
210
+ router.post('/disconnect', async (req, res) => {
211
+ winston.debug("(voice) /disconnect")
212
+
213
+ let project_id = req.body.project_id;
214
+ let token = req.body.token;
215
+ let subscriptionId = req.body.subscription_id;
216
+
217
+ if (!project_id || !token) {
218
+ return res.status(400).send("project_id and token are required");
219
+ }
220
+
221
+ let CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
222
+ await db.remove(CONTENT_KEY);
223
+ winston.debug("(voice) Content deleted.");
224
+
225
+ let proxy_url = BASE_URL + "/webhook/" + project_id;
226
+ let status_url = BASE_URL + "/twilio/status";
227
+
228
+ // get departments
229
+ const tdChannel = new TiledeskChannel({
230
+ API_URL: API_URL,
231
+ redis_client: redisClient
232
+ })
233
+
234
+ let departments = await tdChannel.getDepartments(token, project_id).catch((err) => {
235
+ winston.error("Error getting departments", err.response);
236
+ return [];
237
+ });
238
+
239
+ try {
240
+ if (subscriptionId) {
241
+ await unsubscribe(token, project_id, subscriptionId, API_URL);
242
+ }
243
+ } catch (err) {
244
+ winston.error("(voice) unsubscribe error: " + err);
245
+ }
246
+
247
+ const html = await renderTemplate('configure.html', {
248
+ app_version: pjson.version,
249
+ project_id: project_id,
250
+ token: token,
251
+ proxy_url: proxy_url,
252
+ status_url: status_url,
253
+ departments: departments,
254
+ brand_name: BRAND_NAME
255
+ });
256
+ return res.send(html);
257
+ })
258
+
259
+ return router;
260
+ };
261
+
262
+ async function subscribe(token, project_id, API_URL, BASE_URL, db){
263
+ const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
264
+
265
+ const CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
266
+ const subscription_info = {
267
+ target: BASE_URL + "/tiledesk",
268
+ event: 'message.create.request.channel.'+ CHANNEL_NAME
269
+ }
270
+
271
+ try {
272
+ const subscription = await tdClient.subscribe(subscription_info);
273
+ winston.debug("(voice) Subscription: ", subscription)
274
+
275
+ let settings = {
276
+ project_id: project_id,
277
+ token: token,
278
+ subscriptionId: subscription._id,
279
+ secret: subscription.secret
280
+ }
281
+
282
+ await db.set(CONTENT_KEY, settings);
283
+ return subscription;
284
+ } catch (error) {
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ async function unsubscribe(token, project_id, subscriptionId, API_URL){
290
+ const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
291
+ try {
292
+ const data = await tdClient.unsubscribe(subscriptionId);
293
+ winston.debug("(voice) Subscription: ", data)
294
+ return data;
295
+ } catch (error) {
296
+ throw error;
297
+ }
298
+ }
@@ -0,0 +1,22 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const VoiceController = require('../controllers/VoiceController');
4
+
5
+ module.exports = (services) => {
6
+ const voiceController = new VoiceController(services);
7
+
8
+ router.get('/', (req, res) => voiceController.index(req, res));
9
+ router.post('/tiledesk', (req, res) => voiceController.tiledesk(req, res));
10
+ router.post('/webhook/:id_project', (req, res) => voiceController.webhook(req, res));
11
+ router.post('/nextblock/:callSid', (req, res) => voiceController.nextblock(req, res));
12
+ router.post('/speechresult/:callSid', (req, res) => voiceController.speechresult(req, res));
13
+ router.post('/record/action/:callSid', (req, res) => voiceController.recordAction(req, res));
14
+ router.post('/record/callback/:callSid', (req, res) => voiceController.recordCallback(req, res));
15
+ router.post('/menublock/:callSid', (req, res) => voiceController.menublock(req, res));
16
+ router.post('/handle/:callSid/:event', (req, res) => voiceController.handleEvent(req, res));
17
+ router.post('/event/:callSid/:event', (req, res) => voiceController.event(req, res));
18
+ router.post('/twilio/status', (req, res) => voiceController.twilioStatus(req, res));
19
+ router.post('/twilio/fail', (req, res) => voiceController.twilioFail(req, res));
20
+
21
+ return router;
22
+ };
@@ -0,0 +1,219 @@
1
+ var winston = require('../utils/logger');
2
+ const axios = require("axios").default;
3
+ const FormData = require('form-data');
4
+
5
+ /*ERROR HANDLER*/
6
+ const { ServiceError } = require('../utils/errors');
7
+
8
+ /*UTILS*/
9
+ const fileUtils = require('../utils/fileUtils.js')
10
+
11
+ class AiService {
12
+
13
+ constructor(config) {
14
+
15
+ if (!config) {
16
+ throw new Error("[AiService] config is mandatory");
17
+ }
18
+ if (!config.OPENAI_ENDPOINT) {
19
+ throw new Error("[AiService] config.OPENAI_ENDPOINT is mandatory");
20
+ }
21
+ if(!config.ELEVENLABS_ENDPOINT){
22
+ throw new Error("[AiService] config.ELEVENLABS_ENDPOINT is mandatory");
23
+ }
24
+ if (!config.API_URL) {
25
+ throw new Error("[AiService] config.API_URL is mandatory");
26
+ }
27
+
28
+
29
+ this.OPENAI_ENDPOINT = config.OPENAI_ENDPOINT;
30
+ this.ELEVENLABS_ENDPOINT = config.ELEVENLABS_ENDPOINT;
31
+ this.API_URL = config.API_URL;
32
+
33
+ }
34
+
35
+ async speechToText(fileUrl, model, GPT_KEY) {
36
+ let start_time = new Date();
37
+ winston.debug("[AiService] speechToText url: "+ fileUrl);
38
+
39
+ try {
40
+ let file;
41
+ try {
42
+ file = await fileUtils.downloadFromUrl(fileUrl);
43
+ } catch (err) {
44
+ winston.error("[AiService] err while downloadFromUrl: ", err);
45
+ throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl);
46
+ }
47
+
48
+ if (!file) {
49
+ winston.debug('[AiService] OPENAI speechToText file NOT EXIST: . . . return');
50
+ throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null');
51
+ }
52
+
53
+ const formData = new FormData();
54
+ formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
55
+ formData.append('model', model);
56
+
57
+ const resbody = await axios({
58
+ url: `${this.OPENAI_ENDPOINT}/audio/transcriptions`,
59
+ headers: {
60
+ ...formData.getHeaders(),
61
+ "Authorization": "Bearer " + GPT_KEY
62
+ },
63
+ data: formData,
64
+ method: 'POST'
65
+ });
66
+
67
+ let end_time = new Date();
68
+ winston.verbose(`-----> [AiService] OpenAI speechToText time elapsed: ${end_time - start_time} ms`);
69
+ return resbody.data.text;
70
+
71
+ } catch (err) {
72
+ if (err instanceof ServiceError) throw err;
73
+ winston.error("[AiService] OpenAI STT error", err.message);
74
+ throw new ServiceError('AISERVICE_FAILED', 'OpenAI STT service failed with err:', err);
75
+ }
76
+ }
77
+
78
+ async textToSpeech(text, name, model, GPT_KEY){
79
+ let start_time = new Date();
80
+ winston.debug('[AiService] textToSpeech text:'+ text)
81
+
82
+ const data = {
83
+ model: model,
84
+ input: text,
85
+ voice: name,
86
+ };
87
+
88
+ winston.debug('[AiService] textToSpeech config:', data)
89
+
90
+ try {
91
+ const response = await axios({
92
+ url: `${this.OPENAI_ENDPOINT}/audio/speech`,
93
+ headers: {
94
+ "Content-Type": "application/json",
95
+ "Authorization": "Bearer " + GPT_KEY
96
+ },
97
+ responseType: 'arraybuffer',
98
+ data: data,
99
+ method: "POST",
100
+ });
101
+
102
+ let end_time = new Date();
103
+ winston.verbose(`-----> [AiService] textToSpeech time elapsed: ${end_time - start_time} ms`);
104
+ return response?.data;
105
+ } catch (err) {
106
+ winston.error("[AiService] textToSpeech error: ", err.response?.data);
107
+ throw new ServiceError('AISERVICE_FAILED', 'OpenAI textToSpeech API failed with err:', err);
108
+ }
109
+ }
110
+
111
+
112
+
113
+ async speechToTextElevenLabs(fileUrl, model, language, API_KEY) {
114
+ let start_time = new Date();
115
+ winston.debug("[AiService] ELEVEN Labs speechToText url: "+ fileUrl);
116
+
117
+ try {
118
+ let file;
119
+ try {
120
+ file = await fileUtils.downloadFromUrl(fileUrl);
121
+ } catch (err) {
122
+ winston.error("[AiService] err: ", err);
123
+ throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl);
124
+ }
125
+
126
+ if (!file) {
127
+ winston.debug('[AiService] ELEVEN Labs speechToText file NOT EXIST: . . . return');
128
+ throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null');
129
+ }
130
+
131
+ const formData = new FormData();
132
+ formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
133
+ formData.append('model_id', "scribe_v1");
134
+ formData.append('language_code', language);
135
+
136
+ const resbody = await axios({
137
+ url: `${this.ELEVENLABS_ENDPOINT}/v1/speech-to-text`,
138
+ headers: {
139
+ ...formData.getHeaders(),
140
+ "xi-api-key": API_KEY
141
+ },
142
+ data: formData,
143
+ method: 'POST'
144
+ });
145
+
146
+ let end_time = new Date();
147
+ winston.verbose(`-----> [AiService] ELEVEN Labs speechToText time elapsed: ${end_time - start_time} ms`);
148
+ return resbody.data.text;
149
+
150
+ } catch (err) {
151
+ if (err instanceof ServiceError) throw err;
152
+ winston.error("[AiService] ElevenLabs STT error", err.message);
153
+ throw new ServiceError('AISERVICE_FAILED', 'ElevenLabs STT service failed with err:', err);
154
+ }
155
+ }
156
+
157
+ async textToSpeechElevenLabs(text, voice_id, model, language_code, API_KEY){
158
+ let start_time = new Date();
159
+ const data = {
160
+ model_id: model,
161
+ text: text,
162
+ language_code: language_code
163
+ };
164
+ winston.debug('[AiService] ELEVEN Labs textToSpeech config:', data);
165
+
166
+ try {
167
+ const response = await axios({
168
+ url: `${this.ELEVENLABS_ENDPOINT}/v1/text-to-speech/${voice_id}?output_format=mp3_44100_128`,
169
+ headers: {
170
+ "Content-Type": "application/json",
171
+ "xi-api-key": API_KEY
172
+ },
173
+ responseType: 'arraybuffer',
174
+ data: data,
175
+ method: "POST",
176
+ });
177
+
178
+ let end_time = new Date();
179
+ winston.verbose(`-----> [AiService] ELEVEN Labs textToSpeech time elapsed: ${end_time - start_time} ms`);
180
+ return response?.data;
181
+ } catch (err) {
182
+ winston.error("[AiService] ELEVEN Labs textToSpeech error: ", err);
183
+ throw new ServiceError('AISERVICE_FAILED', 'ElevenLabs textToSpeech API failed with err:', err);
184
+ }
185
+ }
186
+
187
+
188
+
189
+ async checkQuoteAvailability(projectId, token) {
190
+
191
+ winston.debug("[AiService] checkQuoteAvailability for project: "+ projectId);
192
+
193
+ try {
194
+ const resbody = await axios({
195
+ url: `${this.API_URL}/${projectId}/quotes/tokens`,
196
+ headers: {
197
+ 'Content-Type': 'application/json',
198
+ 'Authorization': token
199
+ },
200
+ method: 'GET'
201
+ });
202
+
203
+ if (resbody && resbody.data?.isAvailable === true) {
204
+ return true;
205
+ } else {
206
+ return false;
207
+ }
208
+ } catch (err) {
209
+ winston.error("[AiService] checkQuoteAvailability error: ", err.response?.data);
210
+ throw new ServiceError('AISERVICE_FAILED', 'checkQuoteAvailability API failed with err:', err);
211
+ }
212
+ }
213
+
214
+
215
+
216
+
217
+ }
218
+
219
+ module.exports = { AiService };