@runnerpro/backend 1.17.6 → 1.17.11

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/README.md CHANGED
@@ -1,82 +1,82 @@
1
- # modern-npm-package
2
-
3
- An npm packages for common backend logic between client and dashboard app.
4
-
5
- ## Get Started
6
-
7
- 1. Run `npm install @runnerpro/backend`
8
-
9
- ## Functions
10
-
11
- ### query
12
-
13
- Connection with the PostgresSQL database
14
-
15
- - <b>Param</b>: string with PostgreSQL query
16
- - <b>Param</b>: array of values
17
- - <b>Return</b>: promise of array of values (each item is a row of the table)
18
-
19
- ### batchQuery
20
-
21
- Connection with PostgreSQL database in batches. Use for faster query execution when we need to execute many queries.
22
-
23
- - <b>Param</b>: Array of queries
24
- - query: string with PostgreSQL query
25
- - values: array of values
26
- - <b>Param?</b>: length of promises wait together. Default 5
27
- - <b>Return</b>: promise (without values)
28
-
29
- ### sendMail
30
-
31
- Send mail with company logo and template
32
-
33
- - <b>Param</b>: { subject, title, body, to, link?, attachments?, bcc? }
34
-
35
- - subject: title of the mail
36
- - body: text inside card
37
- - to: array of mails. In DEV and SBX the value is taken from .env GMAIL_EMAIL_USER
38
- - link: button to redirect to the app. { text: text inside button, url: path (after runnerpro.com) }
39
- - attachments: array of files to attach. { filename: name and extension of file, path: path of file, cid?: identifier to insert inside the body (`<img src="cid:__cid__" />`) }
40
- - bcc: array of mails to send via bcc
41
-
42
- - <b>Return</b>: promise (without values)
43
-
44
- ### sendNotification
45
-
46
- Send web or native notification to client: TODO
47
-
48
- - <b>Param</b>: Array of queries { query: string with PostgreSQL query, values: array of values }
49
- - <b>Param?</b>: length of promises wait together. Default 5
50
- - <b>Return</b>: promise (without values)
51
-
52
- ### sleep
53
-
54
- Wait X miliseconds to execute next line
55
-
56
- - <b>Param</b>: number of miliseconds of the promise
57
- - <b>Return</b>: promise (without values)
58
-
59
- ## Deploy new version
60
-
61
- 1. Create a folder with the name of functionality and index.ts inside. Write the function and export it.
62
- 2. In main index.ts (the one inside src), import and export it
63
- 3. Change the version number of the package ([using this convenction](https://docs.npmjs.com/about-semantic-versioning))
64
- 4. Run `npm login`
65
- 5. Run `npm run publish`
66
-
67
- ### Testing
68
-
69
- 1. Install developer dependencies using the following command in your terminal `npm i -D mocha @types/mocha chai @types/chai ts-node`
70
- 1. Create a new file `.mocharc.json` in the root directory with the following contents:
71
- ```json
72
- {
73
- "extension": ["ts"],
74
- "spec": "./**/*.spec.ts",
75
- "require": "ts-node/register"
76
- }
77
- ```
78
- 1. Create a `tests` folder
79
- 1. Create an `index.spec.ts` file in the `tests` folder
80
- 1. Write unit tests in the `index.spec.ts` file to test the code in `index.ts`
81
- 1. Add a `"test"` property in the `package.json` file and give it a value of `"mocha"`
82
- 1. Run `npm test` in your terminal from the root folder of the project
1
+ # modern-npm-package
2
+
3
+ An npm packages for common backend logic between client and dashboard app.
4
+
5
+ ## Get Started
6
+
7
+ 1. Run `npm install @runnerpro/backend`
8
+
9
+ ## Functions
10
+
11
+ ### query
12
+
13
+ Connection with the PostgresSQL database
14
+
15
+ - <b>Param</b>: string with PostgreSQL query
16
+ - <b>Param</b>: array of values
17
+ - <b>Return</b>: promise of array of values (each item is a row of the table)
18
+
19
+ ### batchQuery
20
+
21
+ Connection with PostgreSQL database in batches. Use for faster query execution when we need to execute many queries.
22
+
23
+ - <b>Param</b>: Array of queries
24
+ - query: string with PostgreSQL query
25
+ - values: array of values
26
+ - <b>Param?</b>: length of promises wait together. Default 5
27
+ - <b>Return</b>: promise (without values)
28
+
29
+ ### sendMail
30
+
31
+ Send mail with company logo and template
32
+
33
+ - <b>Param</b>: { subject, title, body, to, link?, attachments?, bcc? }
34
+
35
+ - subject: title of the mail
36
+ - body: text inside card
37
+ - to: array of mails. In DEV and SBX the value is taken from .env GMAIL_EMAIL_USER
38
+ - link: button to redirect to the app. { text: text inside button, url: path (after runnerpro.com) }
39
+ - attachments: array of files to attach. { filename: name and extension of file, path: path of file, cid?: identifier to insert inside the body (`<img src="cid:__cid__" />`) }
40
+ - bcc: array of mails to send via bcc
41
+
42
+ - <b>Return</b>: promise (without values)
43
+
44
+ ### sendNotification
45
+
46
+ Send web or native notification to client: TODO
47
+
48
+ - <b>Param</b>: Array of queries { query: string with PostgreSQL query, values: array of values }
49
+ - <b>Param?</b>: length of promises wait together. Default 5
50
+ - <b>Return</b>: promise (without values)
51
+
52
+ ### sleep
53
+
54
+ Wait X miliseconds to execute next line
55
+
56
+ - <b>Param</b>: number of miliseconds of the promise
57
+ - <b>Return</b>: promise (without values)
58
+
59
+ ## Deploy new version
60
+
61
+ 1. Create a folder with the name of functionality and index.ts inside. Write the function and export it.
62
+ 2. In main index.ts (the one inside src), import and export it
63
+ 3. Change the version number of the package ([using this convenction](https://docs.npmjs.com/about-semantic-versioning))
64
+ 4. Run `npm login`
65
+ 5. Run `npm run publish`
66
+
67
+ ### Testing
68
+
69
+ 1. Install developer dependencies using the following command in your terminal `npm i -D mocha @types/mocha chai @types/chai ts-node`
70
+ 1. Create a new file `.mocharc.json` in the root directory with the following contents:
71
+ ```json
72
+ {
73
+ "extension": ["ts"],
74
+ "spec": "./**/*.spec.ts",
75
+ "require": "ts-node/register"
76
+ }
77
+ ```
78
+ 1. Create a `tests` folder
79
+ 1. Create an `index.spec.ts` file in the `tests` folder
80
+ 1. Write unit tests in the `index.spec.ts` file to test the code in `index.ts`
81
+ 1. Add a `"test"` property in the `package.json` file and give it a value of `"mocha"`
82
+ 1. Run `npm test` in your terminal from the root folder of the project
@@ -158,6 +158,8 @@ const deleteConversationMessage = (req, res, { isClient }) => __awaiter(void 0,
158
158
  const editConversationMessage = (req, res, { isClient }) => __awaiter(void 0, void 0, void 0, function* () {
159
159
  const { id } = req.params;
160
160
  const { text, idWorkout: idWorkoutBody } = req.body;
161
+ // ⭐ Verificar si el campo idWorkout fue enviado explícitamente en el body
162
+ const hasIdWorkoutInBody = 'idWorkout' in req.body;
161
163
  if (!(yield canEditOrDeleteMessage({ idMessage: id, isClient, userid: req.session.userid })))
162
164
  return res.send({ status: 'ok' });
163
165
  const [message] = yield (0, index_1.query)('SELECT [ID CLIENTE], [ID WORKOUT] FROM [CHAT MESSAGE] WHERE [ID] = ?', [id]);
@@ -177,8 +179,8 @@ const editConversationMessage = (req, res, { isClient }) => __awaiter(void 0, vo
177
179
  }
178
180
  // Si el workout no existe o no pertenece al cliente, se mantiene el idWorkout actual (o null)
179
181
  }
180
- else if (idWorkoutBody === null && !isClient) {
181
- // Permitir quitar el workout si se envía explícitamente null
182
+ else if (hasIdWorkoutInBody && !idWorkoutBody && !isClient) {
183
+ // Permitir quitar el workout si se envía explícitamente el campo con valor falsy (null, undefined, '', 0)
182
184
  idWorkout = null;
183
185
  }
184
186
  yield (0, index_1.query)('UPDATE [CHAT MESSAGE] SET [TEXT] = ?, [TEXT PREFERRED LANGUAGE] = ?, [ID WORKOUT] = ?, [EDITADO] = TRUE WHERE [ID] = ?', [
@@ -472,10 +474,6 @@ const sendFile = (req, res, { sendNotification, firebaseMessaging, isClient, buc
472
474
  yield bucket.file(`Chat/${file.id}`).save(file.data);
473
475
  }
474
476
  fs_1.default.unlinkSync(filePath);
475
- // ✅ Procesar archivo multimedia en background (transcripción de audio / descripción de imagen)
476
- if (req.file.mimetype.startsWith('audio/') || req.file.mimetype.startsWith('image/')) {
477
- (0, mediaProcessing_1.processMediaFile)(idFile, fileData, req.file.mimetype);
478
- }
479
477
  if (!isClient) {
480
478
  let textFile = 'Archivo adjunto';
481
479
  if (Number(type) === 4)
@@ -494,6 +492,10 @@ const sendFile = (req, res, { sendNotification, firebaseMessaging, isClient, buc
494
492
  });
495
493
  yield updateSenderView({ userid, idCliente, idMessage: idFile });
496
494
  }
495
+ // Procesar archivo multimedia en background (transcripción de audio / descripción de imagen)
496
+ if (req.file.mimetype.startsWith('audio/') || req.file.mimetype.startsWith('image/')) {
497
+ (0, mediaProcessing_1.processMediaFile)(idFile, fileData, req.file.mimetype);
498
+ }
497
499
  });
498
500
  const getThumbnailFromVideo = (videoPath, duration) => __awaiter(void 0, void 0, void 0, function* () {
499
501
  const targetWidth = 500;
@@ -32,18 +32,18 @@ const getList = (req, res, { query, isClient }) => __awaiter(void 0, void 0, voi
32
32
  else {
33
33
  const { page } = req.query;
34
34
  const limit = 20;
35
- const list = yield query(`
36
- SELECT [C].[ID], [C].[NAME],
37
- [CM].[DATE], [CM].[TEXT],
38
- CASE WHEN [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE] THEN 0
39
- ELSE 1 END AS [READ]
40
- FROM [CLIENTE] AS [C]
41
- LEFT JOIN (
42
- SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
43
- FROM [CHAT MESSAGE]
44
- ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
45
- ORDER BY CASE WHEN [CM].[DATE] IS NULL THEN 1 ELSE 0 END, [CM].[DATE] DESC
46
- LIMIT ? OFFSET ?
35
+ const list = yield query(`
36
+ SELECT [C].[ID], [C].[NAME],
37
+ [CM].[DATE], [CM].[TEXT],
38
+ CASE WHEN [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE] THEN 0
39
+ ELSE 1 END AS [READ]
40
+ FROM [CLIENTE] AS [C]
41
+ LEFT JOIN (
42
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
43
+ FROM [CHAT MESSAGE]
44
+ ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
45
+ ORDER BY CASE WHEN [CM].[DATE] IS NULL THEN 1 ELSE 0 END, [CM].[DATE] DESC
46
+ LIMIT ? OFFSET ?
47
47
  `, [limit, limit * page]);
48
48
  res.send(list);
49
49
  }
@@ -16,11 +16,11 @@ const saveResponseTime = (idCliente) => __awaiter(void 0, void 0, void 0, functi
16
16
  let firstNotReadMessage;
17
17
  // eslint-disable-next-line no-constant-condition
18
18
  while (true) {
19
- const [lastMessage] = yield (0, index_1.query)(`
20
- SELECT * FROM [CHAT MESSAGE]
21
- WHERE [ID CLIENTE] = ? AND [ID] < ?
22
- ORDER BY [DATE] DESC
23
- LIMIT 1
19
+ const [lastMessage] = yield (0, index_1.query)(`
20
+ SELECT * FROM [CHAT MESSAGE]
21
+ WHERE [ID CLIENTE] = ? AND [ID] < ?
22
+ ORDER BY [DATE] DESC
23
+ LIMIT 1
24
24
  `, [idCliente, (lastNotReadMessage === null || lastNotReadMessage === void 0 ? void 0 : lastNotReadMessage.id) || 2147483646]);
25
25
  // Si no hay más mensajes || el mensaje no es del cliente || ya tiene tiempo de respuesta => salimos del bucle
26
26
  if (!lastMessage || (lastMessage.idSender && lastMessage.idCliente !== lastMessage.idSender) || lastMessage.tiempoRespuesta)
@@ -12,22 +12,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getCountNotificaciones = void 0;
13
13
  const getCountNotificaciones = ({ query, isClient, idClient }) => __awaiter(void 0, void 0, void 0, function* () {
14
14
  if (isClient) {
15
- const [{ chat }] = yield query(`
16
- SELECT COUNT([ID]) AS [CHAT]
17
- FROM [CHAT MESSAGE]
18
- WHERE [ID CLIENTE] = ? AND [READ] = FALSE AND [ID SENDER] != [ID CLIENTE]
15
+ const [{ chat }] = yield query(`
16
+ SELECT COUNT([ID]) AS [CHAT]
17
+ FROM [CHAT MESSAGE]
18
+ WHERE [ID CLIENTE] = ? AND [READ] = FALSE AND [ID SENDER] != [ID CLIENTE]
19
19
  `, [idClient]);
20
20
  return chat;
21
21
  }
22
22
  else {
23
- const [{ chat }] = yield query(`
24
- SELECT COUNT([C].[ID]) AS [CHAT]
25
- FROM [CLIENTE] AS [C]
26
- LEFT JOIN (
27
- SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
28
- FROM [CHAT MESSAGE]
29
- ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
30
- WHERE [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE]
23
+ const [{ chat }] = yield query(`
24
+ SELECT COUNT([C].[ID]) AS [CHAT]
25
+ FROM [CLIENTE] AS [C]
26
+ LEFT JOIN (
27
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
28
+ FROM [CHAT MESSAGE]
29
+ ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
30
+ WHERE [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE]
31
31
  `);
32
32
  return chat;
33
33
  }
@@ -10,29 +10,37 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.reprocessRecentMedia = exports.processMediaFile = exports.describeImage = exports.transcribeAudio = void 0;
13
+ const speech_1 = require("@google-cloud/speech");
13
14
  const index_1 = require("../prompt/index");
14
15
  const index_2 = require("../db/index");
15
16
  const index_3 = require("../err/index");
16
- // ✅ PROMPTS PARA PROCESAMIENTO DE ARCHIVOS MULTIMEDIA
17
- const AUDIO_TRANSCRIPTION_PROMPT = `Transcribe el audio de forma literal y completa en español.
18
- Si el audio está en otro idioma, tradúcelo al español.
19
- Devuelve ÚNICAMENTE la transcripción, sin comentarios adicionales.
20
- Si no puedes entender el audio o está vacío, responde: "[Audio no reconocible]"`;
21
- const IMAGE_DESCRIPTION_PROMPT = `Analiza esta imagen en el contexto de una aplicación de entrenamiento deportivo (running, ciclismo, natación, etc).
22
-
23
- Identifica y describe:
24
- 1. **Tipo de imagen**: captura de Garmin/Strava/reloj deportivo, foto de lesión/dolor, gráfica de entrenamiento, selfie deportivo, foto de equipamiento, etc.
25
- 2. **Información relevante**: si es una captura de datos, extrae los valores importantes (ritmo, distancia, frecuencia cardíaca, zonas, etc.)
26
- 3. **Contexto útil para el entrenador**: cualquier información que ayude a entender el estado del atleta o su entrenamiento.
27
-
28
- Devuelve una descripción concisa y útil en español (máximo 300 palabras).
29
- Si la imagen no está relacionada con deporte/fitness, descríbela brevemente.`;
17
+ // ✅ PROMPT PARA DESCRIPCIÓN DE IMÁGENES (Gemini)
18
+ const IMAGE_DESCRIPTION_PROMPT = `Describe esta imagen de forma directa y completa en un solo párrafo continuo, sin introducciones ni listas.
19
+ Si la imagen es de contenido deportivo (captura de Garmin/Strava/reloj deportivo, foto de lesión/dolor, gráfica de entrenamiento, selfie deportivo, foto de equipamiento, datos de entrenamiento, etc.), incluye el tipo de imagen, los datos relevantes si es una captura (ritmo, distancia, frecuencia cardíaca, zonas, etc.), y cualquier contexto útil para entender el estado del atleta o su entrenamiento.
20
+ Si la imagen NO es deportiva, describe su contenido de forma general pero útil para entender el contexto de la conversación.
21
+ Devuelve ÚNICAMENTE la descripción en español, sin comentarios adicionales ni formato de lista. Máximo 300 palabras.`;
22
+ // Mapeo de MIME types a encoding de Speech-to-Text
23
+ const AUDIO_ENCODING_MAP = {
24
+ 'audio/mpeg': 'MP3',
25
+ 'audio/mp3': 'MP3',
26
+ 'audio/wav': 'LINEAR16',
27
+ 'audio/wave': 'LINEAR16',
28
+ 'audio/x-wav': 'LINEAR16',
29
+ 'audio/ogg': 'OGG_OPUS',
30
+ 'audio/webm': 'WEBM_OPUS',
31
+ 'audio/flac': 'FLAC',
32
+ 'audio/x-flac': 'FLAC',
33
+ 'audio/mp4': 'MP3',
34
+ 'audio/m4a': 'MP3',
35
+ 'audio/aac': 'MP3',
36
+ };
30
37
  /**
31
- * Transcribe un archivo de audio usando Gemini (Vertex AI)
38
+ * Transcribe un archivo de audio usando Google Cloud Speech-to-Text
39
+ * Servicio especializado en reconocimiento de voz con soporte para 125+ idiomas
32
40
  *
33
41
  * @param fileBuffer - Buffer del archivo de audio
34
42
  * @param mimetype - Tipo MIME del archivo (ej: 'audio/mpeg', 'audio/wav')
35
- * @returns Promise<string> - Transcripción del audio
43
+ * @returns Promise<string> - Transcripción del audio en español
36
44
  *
37
45
  * @example
38
46
  * ```typescript
@@ -41,16 +49,37 @@ Si la imagen no está relacionada con deporte/fitness, descríbela brevemente.`;
41
49
  * ```
42
50
  */
43
51
  const transcribeAudio = (fileBuffer, mimetype) => __awaiter(void 0, void 0, void 0, function* () {
52
+ var _a;
44
53
  try {
45
- const result = yield (0, index_1.sendPrompt)(AUDIO_TRANSCRIPTION_PROMPT, 'PRO', undefined, {
46
- buffer: fileBuffer,
47
- mimetype,
54
+ const speechClient = new speech_1.SpeechClient();
55
+ // Determinar el encoding del audio
56
+ const encoding = AUDIO_ENCODING_MAP[mimetype] || 'MP3';
57
+ // @ts-ignore - Tipos de Speech-to-Text son complejos
58
+ const [response] = yield speechClient.recognize({
59
+ audio: {
60
+ content: fileBuffer.toString('base64'),
61
+ },
62
+ config: {
63
+ // @ts-ignore - El encoding se determina dinámicamente según el mimetype
64
+ encoding: encoding,
65
+ sampleRateHertz: 16000,
66
+ languageCode: 'es-ES', // Español de España
67
+ alternativeLanguageCodes: ['es-MX', 'es-AR', 'en-US'], // Alternativas: México, Argentina, Inglés
68
+ enableAutomaticPunctuation: true, // Puntuación automática
69
+ model: 'latest_long', // Modelo más reciente para audios largos
70
+ useEnhanced: true, // Modelo mejorado
71
+ },
48
72
  });
49
- return result || '[Audio vacío]';
73
+ // Concatenar todas las transcripciones
74
+ const transcription = (_a = response.results) === null || _a === void 0 ? void 0 : _a.map((result) => { var _a, _b; return (_b = (_a = result.alternatives) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.transcript; }).filter(Boolean).join(' ').trim();
75
+ if (!transcription) {
76
+ return '[Audio sin contenido reconocible]';
77
+ }
78
+ return transcription;
50
79
  }
51
80
  catch (error) {
52
81
  // eslint-disable-next-line no-console
53
- console.error('[transcribeAudio] Error:', (error === null || error === void 0 ? void 0 : error.message) || error);
82
+ console.error('[transcribeAudio] Error Speech-to-Text:', (error === null || error === void 0 ? void 0 : error.message) || error);
54
83
  (0, index_3.err)(null, null, error, null);
55
84
  return '[Error al transcribir audio]';
56
85
  }
@@ -88,7 +117,8 @@ const describeImage = (fileBuffer, mimetype) => __awaiter(void 0, void 0, void 0
88
117
  exports.describeImage = describeImage;
89
118
  /**
90
119
  * Procesa un archivo multimedia (audio o imagen) y guarda el resultado en la base de datos
91
- * Esta función se ejecuta de forma asíncrona después de guardar el mensaje
120
+ * - Audio: usa Google Cloud Speech-to-Text
121
+ * - Imagen: usa Gemini (Vertex AI)
92
122
  *
93
123
  * @param idMessage - ID del mensaje en CHAT MESSAGE
94
124
  * @param fileBuffer - Buffer del archivo
@@ -140,10 +170,10 @@ const reprocessRecentMedia = (bucket) => __awaiter(void 0, void 0, void 0, funct
140
170
  let errors = 0;
141
171
  try {
142
172
  // ✅ Buscar mensajes de los últimos 2 días con MIMETYPE de audio/imagen y sin FILE TEXT
143
- const messages = yield (0, index_2.query)(`SELECT "ID", "MIMETYPE" FROM "CHAT MESSAGE"
144
- WHERE "DATE" >= NOW() - INTERVAL '2 days'
145
- AND "FILE TEXT" IS NULL
146
- AND ("MIMETYPE" LIKE 'audio/%' OR "MIMETYPE" LIKE 'image/%')
173
+ const messages = yield (0, index_2.query)(`SELECT "ID", "MIMETYPE" FROM "CHAT MESSAGE"
174
+ WHERE "DATE" >= NOW() - INTERVAL '2 days'
175
+ AND "FILE TEXT" IS NULL
176
+ AND ("MIMETYPE" LIKE 'audio/%' OR "MIMETYPE" LIKE 'image/%')
147
177
  ORDER BY "DATE" DESC`, []);
148
178
  // eslint-disable-next-line no-console
149
179
  console.log(`[reprocessRecentMedia] Encontrados ${messages.length} mensajes para procesar`);
@@ -45,72 +45,72 @@ const sendMail = ({ subject, title, body, to, link, attachments, bcc }) => __awa
45
45
  });
46
46
  exports.sendMail = sendMail;
47
47
  function getBodyHTML(title, body, link) {
48
- return `
49
- <html lang="es">
50
- <head>
51
- <meta charset="UTF-8">
52
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
53
- <title>Correo con estilo</title>
54
- <style>
55
- /* Estilos generales */
56
- body {
57
- margin: 0;
58
- padding: 0;
59
- }
60
-
61
- /* Estilos específicos del botón */
62
- .button {
63
- display: inline-block;
64
- padding: 12px 26px;
65
- font-size: 17px;
66
- text-align: center;
67
- text-decoration: none;
68
- background-color: #ea5b1b;
69
- color: #ffffff !important;
70
- margin: 40px 0 20px 0;
71
- font-family: 'Sofia Sans', 'Roboto', sans-serif;
72
- font-weight: bold;
73
- border-radius: 4px;
74
- border: 2px solid #ea5b1b;
75
- }
76
- </style>
77
- </head>
78
- <body style="background: #f0f0f0; padding: 0 0 40px 0">
79
- <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0">
80
- <tr>
81
- <td align="center" style="padding:0;">
82
- <table role="presentation" style="width:100%;border-collapse:collapse;border-spacing:0;text-align:left;">
83
- <tr>
84
- <td align="center" style="padding:5px 0 0 0;background:#ea5b1b;">
85
- <img src="cid:logo" alt="" width="220" style="height:auto;display:block;">
86
- </td>
87
- </tr>
88
- <tr>
89
- <td style="padding:20px 24px 0px 24px; max-width: 600px" align="center">
90
- <h1 style="font-size:22px;text-align:center;margin:16px 0 6px 0;font-family:'Sofia Sans', 'Roboto', sans-serif;font-style: italic">
91
- ${title || ''}
92
- </h1>
93
- <p style="text-align:left;margin:0 0 12px 0;font-size:15px;line-height:24px;font-family:'Sofia Sans', 'Roboto', sans-serif;">
94
- ${body.split('\n').join('<br>')}
95
- </p>
48
+ return `
49
+ <html lang="es">
50
+ <head>
51
+ <meta charset="UTF-8">
52
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
53
+ <title>Correo con estilo</title>
54
+ <style>
55
+ /* Estilos generales */
56
+ body {
57
+ margin: 0;
58
+ padding: 0;
59
+ }
60
+
61
+ /* Estilos específicos del botón */
62
+ .button {
63
+ display: inline-block;
64
+ padding: 12px 26px;
65
+ font-size: 17px;
66
+ text-align: center;
67
+ text-decoration: none;
68
+ background-color: #ea5b1b;
69
+ color: #ffffff !important;
70
+ margin: 40px 0 20px 0;
71
+ font-family: 'Sofia Sans', 'Roboto', sans-serif;
72
+ font-weight: bold;
73
+ border-radius: 4px;
74
+ border: 2px solid #ea5b1b;
75
+ }
76
+ </style>
77
+ </head>
78
+ <body style="background: #f0f0f0; padding: 0 0 40px 0">
79
+ <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0">
80
+ <tr>
81
+ <td align="center" style="padding:0;">
82
+ <table role="presentation" style="width:100%;border-collapse:collapse;border-spacing:0;text-align:left;">
83
+ <tr>
84
+ <td align="center" style="padding:5px 0 0 0;background:#ea5b1b;">
85
+ <img src="cid:logo" alt="" width="220" style="height:auto;display:block;">
86
+ </td>
87
+ </tr>
88
+ <tr>
89
+ <td style="padding:20px 24px 0px 24px; max-width: 600px" align="center">
90
+ <h1 style="font-size:22px;text-align:center;margin:16px 0 6px 0;font-family:'Sofia Sans', 'Roboto', sans-serif;font-style: italic">
91
+ ${title || ''}
92
+ </h1>
93
+ <p style="text-align:left;margin:0 0 12px 0;font-size:15px;line-height:24px;font-family:'Sofia Sans', 'Roboto', sans-serif;">
94
+ ${body.split('\n').join('<br>')}
95
+ </p>
96
96
  ${link
97
- ? `
98
- <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0;">
99
- <tr>
100
- <td align="center" style="padding:0;">
101
- <a href="${link.external ? '' : process.env.FRONTEND_URL}${link.url}" class="button" style="color:white">${link.text}</a>
102
- </td>
103
- </tr>
104
- </table>
97
+ ? `
98
+ <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0;">
99
+ <tr>
100
+ <td align="center" style="padding:0;">
101
+ <a href="${link.external ? '' : process.env.FRONTEND_URL}${link.url}" class="button" style="color:white">${link.text}</a>
102
+ </td>
103
+ </tr>
104
+ </table>
105
105
  `
106
- : ''}
107
- </td>
108
- </tr>
109
- </table>
110
- </td>
111
- </tr>
112
- </table>
113
- </body>
114
- </html>
106
+ : ''}
107
+ </td>
108
+ </tr>
109
+ </table>
110
+ </td>
111
+ </tr>
112
+ </table>
113
+ </body>
114
+ </html>
115
115
  `;
116
116
  }
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../../../src/chat/api/conversation.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,iBAAiB,0BAA2B,GAAG,SA0BpD,CAAC;AA0EF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,uBAAuB,qCAAkC,GAAG,iBAqCjE,CAAC;AAmGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,WAAW,0EAAuE,GAAG,kBAkE1F,CAAC;AAEF,QAAA,MAAM,gBAAgB;;;;mBAqBrB,CAAC;AA8RF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../../../src/chat/api/conversation.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,iBAAiB,0BAA2B,GAAG,SA0BpD,CAAC;AA0EF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,uBAAuB,qCAAkC,GAAG,iBAwCjE,CAAC;AAmGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,WAAW,0EAAuE,GAAG,kBAkE1F,CAAC;AAEF,QAAA,MAAM,gBAAgB;;;;mBAqBrB,CAAC;AA8RF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC"}
@@ -1,10 +1,11 @@
1
1
  /// <reference types="node" />
2
2
  /**
3
- * Transcribe un archivo de audio usando Gemini (Vertex AI)
3
+ * Transcribe un archivo de audio usando Google Cloud Speech-to-Text
4
+ * Servicio especializado en reconocimiento de voz con soporte para 125+ idiomas
4
5
  *
5
6
  * @param fileBuffer - Buffer del archivo de audio
6
7
  * @param mimetype - Tipo MIME del archivo (ej: 'audio/mpeg', 'audio/wav')
7
- * @returns Promise<string> - Transcripción del audio
8
+ * @returns Promise<string> - Transcripción del audio en español
8
9
  *
9
10
  * @example
10
11
  * ```typescript
@@ -30,7 +31,8 @@ declare const transcribeAudio: (fileBuffer: Buffer, mimetype: string) => Promise
30
31
  declare const describeImage: (fileBuffer: Buffer, mimetype: string) => Promise<string>;
31
32
  /**
32
33
  * Procesa un archivo multimedia (audio o imagen) y guarda el resultado en la base de datos
33
- * Esta función se ejecuta de forma asíncrona después de guardar el mensaje
34
+ * - Audio: usa Google Cloud Speech-to-Text
35
+ * - Imagen: usa Gemini (Vertex AI)
34
36
  *
35
37
  * @param idMessage - ID del mensaje en CHAT MESSAGE
36
38
  * @param fileBuffer - Buffer del archivo
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mediaProcessing/index.ts"],"names":[],"mappings":";AAqBA;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,eAAe,eAAsB,MAAM,YAAY,MAAM,KAAG,QAAQ,MAAM,CAanF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,aAAa,eAAsB,MAAM,YAAY,MAAM,KAAG,QAAQ,MAAM,CAajF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,QAAA,MAAM,gBAAgB,cAAqB,MAAM,cAAc,MAAM,YAAY,MAAM,KAAG,QAAQ,IAAI,CAkBrG,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,oBAAoB,WAAkB,GAAG;eAAwB,MAAM;YAAU,MAAM;EAwD5F,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mediaProcessing/index.ts"],"names":[],"mappings":";AA2BA;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,eAAe,eAAsB,MAAM,YAAY,MAAM,KAAG,QAAQ,MAAM,CA0CnF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,aAAa,eAAsB,MAAM,YAAY,MAAM,KAAG,QAAQ,MAAM,CAajF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,QAAA,MAAM,gBAAgB,cAAqB,MAAM,cAAc,MAAM,YAAY,MAAM,KAAG,QAAQ,IAAI,CAkBrG,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,oBAAoB,WAAkB,GAAG;eAAwB,MAAM;YAAU,MAAM;EAwD5F,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,81 +1,82 @@
1
- {
2
- "name": "@runnerpro/backend",
3
- "version": "1.17.6",
4
- "description": "A collection of common backend functions",
5
- "exports": {
6
- ".": "./lib/cjs/index.js"
7
- },
8
- "types": "./lib/cjs/types/index.d.ts",
9
- "main": "./lib/cjs/index.js",
10
- "files": [
11
- "lib/**/*"
12
- ],
13
- "scripts": {
14
- "clean": "del-cli ./lib",
15
- "build": "npm run clean && tsc -p ./configs/tsconfig.cjs.json",
16
- "deploy": "npm run build && npm publish",
17
- "semantic-release": "semantic-release",
18
- "lint": "eslint --ext .ts --ignore-path .gitignore .",
19
- "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
20
- "prepare": "husky",
21
- "test": "jest"
22
- },
23
- "release": {
24
- "branches": [
25
- "main"
26
- ]
27
- },
28
- "publishConfig": {
29
- "access": "public"
30
- },
31
- "repository": {
32
- "type": "git",
33
- "url": "https://gitlab.com/runner-pro/runnerpro-backend.git"
34
- },
35
- "author": "Runner Pro",
36
- "license": "MIT",
37
- "devDependencies": {
38
- "@eslint/js": "^9.6.0",
39
- "@types/eslint__js": "^8.42.3",
40
- "@types/jest": "^30.0.0",
41
- "@types/nodemailer": "^6.4.15",
42
- "@types/pg": "^8.11.3",
43
- "del-cli": "5.1.0",
44
- "eslint": "^8.57.0",
45
- "eslint-config-prettier": "^9.1.0",
46
- "eslint-plugin-exception-handling": "^1.0.2",
47
- "eslint-plugin-sonarjs": "^1.0.3",
48
- "husky": "^9.0.11",
49
- "jest": "^29.7.0",
50
- "semantic-release": "23.0.2",
51
- "ts-jest": "^29.4.6",
52
- "ts-node": "10.9.2",
53
- "typescript": "5.3.3",
54
- "typescript-eslint": "^7.15.0"
55
- },
56
- "peerDependencies": {
57
- "@napi-rs/canvas": "^0.1.53",
58
- "@runnerpro/common": "^1.5.10",
59
- "axios": "^1.6.7",
60
- "image-size": "^1.0.2",
61
- "jimp": "^0.22.10",
62
- "nodemailer": "6.9.9",
63
- "pg": "8.11.3",
64
- "uuidv4": "^6.2.13"
65
- },
66
- "dependencies": {
67
- "@google-cloud/storage": "^7.11.3",
68
- "@google-cloud/translate": "^8.3.0",
69
- "@google-cloud/vertexai": "^1.4.0",
70
- "@notionhq/client": "^2.2.15",
71
- "exifr": "^7.1.3",
72
- "ffmpeg-static": "^5.2.0",
73
- "ffprobe-static": "^3.1.0",
74
- "firebase-admin": "^11.10.1",
75
- "fluent-ffmpeg": "^2.1.3",
76
- "googleapis": "^144.0.0",
77
- "multer": "^1.4.5-lts.1",
78
- "oauth-signature": "1.5.0",
79
- "socket.io": "^4.7.2"
80
- }
81
- }
1
+ {
2
+ "name": "@runnerpro/backend",
3
+ "version": "1.17.11",
4
+ "description": "A collection of common backend functions",
5
+ "exports": {
6
+ ".": "./lib/cjs/index.js"
7
+ },
8
+ "types": "./lib/cjs/types/index.d.ts",
9
+ "main": "./lib/cjs/index.js",
10
+ "files": [
11
+ "lib/**/*"
12
+ ],
13
+ "scripts": {
14
+ "clean": "del-cli ./lib",
15
+ "build": "npm run clean && tsc -p ./configs/tsconfig.cjs.json",
16
+ "deploy": "npm run build && npm publish",
17
+ "semantic-release": "semantic-release",
18
+ "lint": "eslint --ext .ts --ignore-path .gitignore .",
19
+ "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
20
+ "prepare": "husky",
21
+ "test": "jest"
22
+ },
23
+ "release": {
24
+ "branches": [
25
+ "main"
26
+ ]
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://gitlab.com/runner-pro/runnerpro-backend.git"
34
+ },
35
+ "author": "Runner Pro",
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@eslint/js": "^9.6.0",
39
+ "@types/eslint__js": "^8.42.3",
40
+ "@types/jest": "^30.0.0",
41
+ "@types/nodemailer": "^6.4.15",
42
+ "@types/pg": "^8.11.3",
43
+ "del-cli": "5.1.0",
44
+ "eslint": "^8.57.0",
45
+ "eslint-config-prettier": "^9.1.0",
46
+ "eslint-plugin-exception-handling": "^1.0.2",
47
+ "eslint-plugin-sonarjs": "^1.0.3",
48
+ "husky": "^9.0.11",
49
+ "jest": "^29.7.0",
50
+ "semantic-release": "23.0.2",
51
+ "ts-jest": "^29.4.6",
52
+ "ts-node": "10.9.2",
53
+ "typescript": "5.3.3",
54
+ "typescript-eslint": "^7.15.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@napi-rs/canvas": "^0.1.53",
58
+ "@runnerpro/common": "^1.5.10",
59
+ "axios": "^1.6.7",
60
+ "image-size": "^1.0.2",
61
+ "jimp": "^0.22.10",
62
+ "nodemailer": "6.9.9",
63
+ "pg": "8.11.3",
64
+ "uuidv4": "^6.2.13"
65
+ },
66
+ "dependencies": {
67
+ "@google-cloud/speech": "^7.2.1",
68
+ "@google-cloud/storage": "^7.11.3",
69
+ "@google-cloud/translate": "^8.3.0",
70
+ "@google-cloud/vertexai": "^1.4.0",
71
+ "@notionhq/client": "^2.2.15",
72
+ "exifr": "^7.1.3",
73
+ "ffmpeg-static": "^5.2.0",
74
+ "ffprobe-static": "^3.1.0",
75
+ "firebase-admin": "^11.10.1",
76
+ "fluent-ffmpeg": "^2.1.3",
77
+ "googleapis": "^144.0.0",
78
+ "multer": "^1.4.5-lts.1",
79
+ "oauth-signature": "1.5.0",
80
+ "socket.io": "^4.7.2"
81
+ }
82
+ }